微信服务号的开发中,涉及到一个比较核心的环节–用户授权登录,这样我们开发者的服务器才能够获取到微信用户的信息,方便从用户维度开展业务。微信开发者文档对登录流程有较为详细的介绍,本文着重从具体实施方案入手,并借助静默授权接口优化登录用户体验。

微信网页授权登录是典型的Auth2授权登录流程,第三方不需要微信用户的注册、登录、注册等流程,不需要存储用户密码,只需要从微信获取一个许可令牌,就可以获取微信用户信息以构建自身的用户体系。这种方式开发成本低,安全性高,而且借助微信平台易于扩展用户群体。

主体流程

本方案中使用springmvc技术框架对相关请求进行拦截处理。方案流程图如下所示:

步骤1至4主要是用于检查当前session中是否已存在open_id。open_id是微信用户在公众号内的唯一标识。session保存用户的会话状态信息,在会话有效期内,只在用户的第一个请求完成授权登录,并将获取到的open_id缓存在session中,这样用户的后续请求将不受登录流程影响,保持快速响应。该流程可通过继承org.springframework.web.servlet.handler.HandlerInterceptorAdapter实现一个拦截器来进行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PageInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(true);
String openId = (String) session.getAttribute(WechatConstant.OPEN_ID);
if(openId == null) {
String state = String.valueOf((int) (Math.random() * 1000));
session.setAttribute(WechatConstant.ORIGINAL_URL, requestUrl); // session中记录用户请求url
session.setAttribute(WechatConstant.STATE, state); // 记录state参数,方便回调后校验
// ... 略过targetUrl组装逻辑
// targetUrl为静默授权url,参见流程图
response.sendRedirect(targetUrl);// 重定向到静默授权链接
}
return true; // 进入正常业务流程
}
}

注意这里并没有直接重定向到用户手动授权登录的页面。这里是要利用静默授权来获取到用户的open_id,以检查系统中是否已存在该用户。目前很多服务号每次用户访问都粗暴地调用用户手动授权登录流程,仅仅依靠微信服务器的登录缓存来记住用户已登录过,这样我们经常在进入服务号页面时都会看到如下这种页面,用户体验较差。而本文的流程通过跳转静默授权页面,用户只会看到一个空页面一刷而过,后续或进入手动授权页面,或进入应用页面,用户体验更好。

微信服务器接下来会回调我们之前传出的重定向回调url,即redirect_uri字段。我们通过一个controller方法weChatLogin来接收回调请求,并取得code和state。接下来的第6步是个微信推荐的步骤,即从session中取出之前暂存的state和请求传来的state进行对比,若不一致,则可能存在csrf攻击,则中止流程。csrf攻击的知识不是本文讲解重点,大家可以自行google之。这个code参数比较有用,可以用来请求获取用户的open_id,这样静默授权页面的使命才算真正完成。

拿到open_id后,检查数据库中是否已存在,若存在将其放入session,同时重定向用户原始的页面请求,重新从步骤1开始,进而可进入正常业务流程。用户每次新建会话时,都会经历这个流程。若不存在,则重定向到用户手动授权链接,提示用户点击授权按钮。

该controller主体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ResponseBody
@RequestMapping(value = "/login", method = RequestMethod.GET)
public void weChatLogin(@RequestParam("code") String code, @RequestParam("state") String state,
HttpServletRequest request, HttpServletResponse response) {
String preState = (String) request.getSession().getAttribute(WechatConstant.STATE); // 从session获取发送的state
if (!state.equals(preState)) {
logger.warn("可能存在跨站攻击, preState = {}, curState = {}", preState, state);
return;
}
String originalUrl = (String) request.getSession().getAttribute(WechatConstant.ORIGINAL_URL);
String openId = weChatService.getOpenId(code); // 调取微信接口获取openId,并检查数据库中是否存在用户信息,若不存在返回null
if(openId == null) {
request.getSession().setAttribute(WechatConstant.OPEN_ID, openId); // open_id放入session
// 略过用户授权登录url组装逻辑
response.sendRedirect(url); // 重定向至用户手动授权登录页面
} else {
response.sendRedirect(originalUrl); // 重定向至用户原始请求url
}
}

用户点击确认授权按钮之后,接下来步骤11,controller方法weChatAuth接收微信重定向回调请求。再次执行state一致性检查,防范csrf攻击。利用code请求open_id和access_token,此时的access_token才是有效的,能够用来请求用户具体信息,而之前静默授权流程中获取的access_token是无效的。注意access_token有效期只有2小时左右。随后第15步正式请求用户信息并入库,再重定向用户原始请求url,整体流程结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@ResponseBody
@RequestMapping(value = "/auth", method = RequestMethod.GET)
public void weChatAuth(@RequestParam("code") String code, @RequestParam("state") String state,
HttpServletRequest request, HttpServletResponse response) {
String preState = (String) request.getSession().getAttribute(WechatConstant.STATE);
if (!state.equals(preState)) {
logger.warn("可能存在跨站攻击, preState = {}, curState = {}", preState, state);
return;
}
Map<String, Object> resultMap = weChatService.getAccessTokenAndOpenId(code);
String accessToken = (String) resultMap.get(WechatConstant.ACCESS_TOKEN);
String openId = (String) resultMap.get(WechatConstant.OPEN_ID);
weChatService.getUserInfo(accessToken, openId); // 请求用户信息,并入库
request.getSession().setAttribute(WechatConstant.OPEN_ID, openId);// openId放入session
String originalUrl = (String) request.getSession().getAttribute(WechatConstant.ORIGINAL_URL);
response.sendRedirect(originalUrl); // 重定向用户原始请求
}

注意

我们应用在获取到用户信息后,由于是一次性的操作,若用户此后更改了昵称或头像等,那我们应用就无法实时感知到。若经常性让用户进行授权登录,体验就会非常差。好的方案是系统自动去同步用户信息,而让用户无感知。获取用户信息的凭证是access_token,而步骤13中获取的access_token有效期一般只有两小时左右,因此,处理方案是将每个用户的access_token存储下来,定时(间隔小于两小时)利用refresh_token去刷新access_tok方案将用户en,确保access_token一直是有效的。用户修改昵称或头像的频率不会太高,所以建议每天利用access_token定时拉取一次用户信息并更新。另外,refresh_token的有效期大概在一周或半个月左右,因此若refresh_token过期了,就需要用户重新手动授权登录了,以获取有效的refresh_token。这种定时拉取的处理方案将用户进行重复手动授权登录的间隔延长大最大,用户体验最好。