场景

某医院官网微信公众号,需要添加一个“个人报告”菜单,点击跳转到第三方网页,提供查询缴费记录、影像报告等功能。

公众号网页授权流程

微信公众号网页授权

第三方网页有自己的身份认证框架,前端 VUE ,后端 Spring Boot 。

代码实现(前端处理回调地址)

  1. 用户在微信客户端中访问第三方网页,若后端校验当前用户未登录,返回 401 状态码和微信授权页面 url 。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @AllArgsConstructor
    @Component
    public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint
    {
    private final SecurityProperties properties;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse httpServletResponse, AuthenticationException e)
    throws IOException
    {
    httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
    httpServletResponse.setHeader("Cache-Control","no-cache");
    httpServletResponse.setStatus(200);
    httpServletResponse.setCharacterEncoding("UTF-8");
    httpServletResponse.setContentType("application/json");
    // 返回微信授权页面URL
    httpServletResponse.getWriter().println(JSONUtil.parse(ApiResponse.ofStatus(HttpStatus.UNAUTHORIZED, properties.getAuthurl())));
    httpServletResponse.getWriter().flush();
    }
    }

    微信授权页面 url:https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

    redirect_uri 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理。

    用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE 。

    redirect_uri 回调地址应该由前端处理还是后端处理?

    由前端处理,前端拿到 code 授权码后请求后端登陆地址获取 token ,可以避免使用轮询的方式获取 token 。

  2. 前端接收到 401 状态码后,跳转到微信授权页面 url ,用户同意授权后,处理授权后重定向的回调链接地址 redirect_uri/?code=CODE&state=STATE。

    code 作为换取 access_token 的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。

    前端拿到 code 和 state 参数后请求后端接口,由后端换取 access_token 并获取用户信息。

    由于公众号的secret和获取到的access_token安全级别都非常高,必须只保存在服务器,不允许传给客户端。后续刷新access_token、通过access_token获取用户信息等步骤,也必须从服务器发起。

  3. 后端根据 code 参数换取 access_token ,获取用户信息,并且通常需要对用户信息进行一些业务逻辑的处理,例如保存用户信息到数据库、授权登录等,最后返回 token 。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @SneakyThrows
    @RequestMapping("/greet")
    public ApiResponse<String> greetUser(@PathVariable String appid, @RequestParam String code, @RequestParam(required = false) String state) {
    if (!this.wxService.switchover(appid)) {
    throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
    }

    WxOAuth2AccessToken accessToken = wxService.getOAuth2Service().getAccessToken(code);
    WxOAuth2UserInfo user = wxService.getOAuth2Service().getUserInfo(accessToken, null);

    // 创建或更新用户
    WxUser wxUser = BeanUtil.copyProperties(user, WxUser.class);
    WxUser exist = wxUserService.getOne(Wrappers.<WxUser>lambdaQuery().select(WxUser::getId).eq(WxUser::getOpenid, wxUser.getOpenid()));
    if (exist == null) {
    wxUser.setId(IdUtils.getSnowflakeId());
    wxUserService.save(wxUser);
    } else {
    wxUser.setId(exist.getId());
    wxUserService.updateById(wxUser);
    }

    // 微信用户授权获取用户信息成功后,返回token
    LoginUser loginUser = LoginUser.builder().openId(user.getOpenid()).userId(wxUser.getId()).wxOAuth2UserInfo(user).build();
    String token = tokenService.createToken(loginUser);
    return ApiResponse.ofSuccess(token);
    }

后端处理回调地址(不推荐做法)

微信授权页面 url 的 redirect_uri 参数填写后端地址,由后端处理授权后重定向的回调链接地址 redirect_uri/?code=CODE&state=STATE ,由后端携带 token 参数重定向到前端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@SneakyThrows
@RequestMapping("/greet")
public void greetUser(HttpServletResponse response, @PathVariable String appid, @RequestParam String code, @RequestParam(required = false) String state) {
if (!this.wxService.switchover(appid)) {
throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
}

WxOAuth2AccessToken accessToken = wxService.getOAuth2Service().getAccessToken(code);
WxOAuth2UserInfo user = wxService.getOAuth2Service().getUserInfo(accessToken, null);

// 创建或更新用户
WxUser wxUser = BeanUtil.copyProperties(user, WxUser.class);
WxUser exist = wxUserService.getOne(Wrappers.<WxUser>lambdaQuery().select(WxUser::getId).eq(WxUser::getOpenid, wxUser.getOpenid()));
if (exist == null) {
wxUser.setId(IdUtils.getSnowflakeId());
wxUserService.save(wxUser);
} else {
wxUser.setId(exist.getId());
wxUserService.updateById(wxUser);
}

// 微信用户授权获取用户信息成功后,创建 token ,携带页面初始URL和token参数重定向到前端登录页面
LoginUser loginUser = LoginUser.builder().openId(user.getOpenid()).userId(wxUser.getId()).wxOAuth2UserInfo(user).build();
String token = tokenService.createToken(loginUser);
// response.sendRedirect("http://127.0.0.1:8081/userLogin?back=/userInfo&token=" + token);
response.sendRedirect(StrUtil.format("{}?back={}&token={}", authUrl, state, token));
}

前端打开微信授权页面 url ,用户同意授权后,先是有微信公众号服务端重定向到后端回调地址,后端处理完后再回调到前端地址,经历了两次重定向。

参考

微信公众号网页授权官方文档:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html