文章目录
  1. 1. 场景一.用户转发链接导致openid为空
    1. 1.1. 报错日志
    2. 1.2. 错误分析
  2. 2. 场景二.支付宝侧接口直接返回uid为空
    1. 2.1. 错误分析
  3. 3. 解决方法
    1. 3.1. 1.工具类完善空值校验
    2. 3.2. 2.业务层增加拦截器强制重定向
  4. 4. 总结

最近开发的一个应用需要走支付宝的oauth2网关获取用户的openid,并基于该id开展业务。上线之后陆续发现一些问题,问题的最终效果均为用户openid为空。
对应的场景和解决方案如下。

场景一.用户转发链接导致openid为空

用户之间为了方便会转发url,然后进入对应的业务场景页面。在这个过程中携带了自身的authcode,新用户访问该url时使用了之前用户的authcode,该authcode
已经失效,导致支付宝侧返回”null”,从而出现库中存在openid为null的情况。

报错日志

ErrorScene^_^40002^_^isv.code-invalid^_^127.0.0.1^_^Linux^_^2018-05-30 14:00:39^_^ProtocalMustParams:
charset=UTF-8&method=alipay.system.oauth.token&sign=DLpPq+Txuhw6gtBZc1DOlHNc2zYH/U8qj9DI9r8XanZ+/Ag81
mw7Z8imxaZtfmNKKUXThCLFF2mNMXTMkdqU6A1TsI1
CYMSiOcAZdeAlelN+ep9wwB+Ah5V2fbxLZNstsiZUiQILyHCyUF+L17SyeF6WZ4YTPSsF0QAgto1cjzskyD2Hlh9W6NnUNjYB+3G2
7wdCFc2LVKzRakvyaODsV
12hypatSKoJ2Jxo7bmoQ7YNQRAnJCcSQjSSulHwR/JiIZFR34cvYUrTSpAvPuHUxRsmoK5wECoeKAsSVppg8alnkap1mAZmnapRCHW
lkXondoMqB1wIiJP3U93
v3/catA==&version=1.0&app_id=2018050302624711&sign_type=RSA2&timestamp=2018-05-30 14:00:39^_^ProtocalO
ptParams:alipay_sdk=
alipay-sdk-java-dynamicVersionNo&format=json^_^ApplicationParams:grant_type=authorization_code&code=b
b26c59671c149e8829e250ba64fXX82^_^Body:{"error_response":{"code":"40002","msg":"Invalid Arguments",
"sub_code":"isv.code-invalid","sub_msg":"授权码code无效"},

错误分析

这种场景判定为bug,authcode转openid的位置应当对返回值进行校验,如果返回为null则前端需要强制重定向至授权url重新获取openid。

场景二.支付宝侧接口直接返回uid为空

这里真是一个教训,代码逻辑应当遵循一个准则:不要相信外界的任何响应!对于支付宝侧的响应过于信任,在解析返回报文时没有做空校验,在
网络连接正常的情况下,支付宝测的接口竟然也会返回“null”字符串,并且没有报错。

错误分析

核心授权代码没有对返回值做校验,这里怪我,直接参考了之前业务的授权相关代码,没有做更进一步的测试和review就上线了,教训和经验就是:
对于核心的封装代码包,要做充分的测试和review,并随时做好重写的准备。

解决方法

1.工具类完善空值校验

针对上述两个典型的问题,我对核心授权代码进行了进一步的修复和改写。代码如下

..............
try{
    AlipayClient aliClient = new DefaultAlipayClient(alipayGateway,
            alipayAuthParam.getAppId(),
            alipayAuthParam.getAppPrivateKey(),
            format,
            charset,
            alipayAuthParam.getAlipayPublicKey(),
            signType);
    AlipaySystemOauthTokenResponse oauthTokenResponse = aliClient.execute(request);
    if (oauthTokenResponse.isSuccess()) {
        LOGGER.info("获取到的appid=" + oauthTokenResponse.getUserId() + ",支付宝userid为=" + oauthTokenRespons
        e.getAlipayUserId());
        LOGGER.info("accessToken:" + oauthTokenResponse.getAccessToken()
                + ":有效时间" + oauthTokenResponse.getExpiresIn()
                + ":::ReExpiresIn=" + oauthTokenResponse.getReExpiresIn()
                + ":::RefreshToken=" + oauthTokenResponse.getRefreshToken());
        return oauthTokenResponse.getAlipayUserId() + ","
                + oauthTokenResponse.getAccessToken() + ","
                + oauthTokenResponse.getRefreshToken() + ","
                + oauthTokenResponse.getUserId();
    } else {
        LOGGER.info("支付宝userid为=null, authcode=" + authCode);
        return null;
    }
}catch(AlipayApiException e) {
        // 处理异常
        LOGGER.error("从支付宝获取用户openId信息异常");
        e.printStackTrace();
        return null;
}...................                    

根据官方文档官方api地址 添加了返回值是否成功的校验,(之前的胖友竟然没有写这段代
码,怪我没有review)

if (oauthTokenResponse.isSuccess()) {

其实官方已经考虑到这个问题了,在返回的包体增加了成功标志,只要返回true就可以放心的使用实体去解析返回报文了。
并且对异常情况进行了返空的处理。从而可以在业务层对空值进行具体的处理。

2.业务层增加拦截器强制重定向

在1的前提下,在业务层对认证失败的做了强制重定向,具体逻辑如下

.................
if (StringUtils.isNotEmpty(authCode)) {
    try {
        userId = AlipayAuthClient.getInstance().getAlipayUserId(authCode,
                new AlipayAuthParam(APPID, ALIPAY_PUBLIC_KEY, APP_PRIVATE_KEY),
                "PROD");
        LOGGER.info("[page]首页获取到的,userId={},sessionId={}",
                userId, request.getSession().getId());
        if (userId == null) {
            LOGGER.info("[page]首页获取到的,userId=null,sessionId={}",
                    request.getSession().getId());
            /**再次重定向*/
            return "redirect:" + CommonConstant.REAL_URL;
        }
    } catch (Exception e) {
        LOGGER.info("用户id为空,重定向到首页继续获取授权码,sessionId=" + request.getSession().getId());
        /**再次重定向*/
        return "redirect:" + CommonConstant.REAL_URL;
    }
................

只要返回uid为空的情况就对其进行重定向操作,常量CommonConstant.REAL_URL表示完整的授权url。
这样可以保证从分享链接进来的用户必然要带着有效的uid,否则会强制重定向直到uid有效为止。

除了上述的逻辑,增加了全局的拦截器,因为有用户会重复使用一个链接进行访问,而处于方便和保护用户隐私考量,
所有用户相关的信息都保存在了session中。当session超时,用户再来的时候会找不到自身的信息,这时候如果不做处理,
就会有产生uid为空的风险。因此增加的全局session拦截器在每次用户访问任意页面路由的时候均会进行一次session有效性校验,
如果session中uid为空则强制重定向到授权页,保证其能再次获取到自己的有效uid。代码如下:

public class SessionInterceptor implements HandlerInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(SessionInterceptor.class);

    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, 
        Object o) throws Exception {
                    HttpSession session = httpServletRequest.getSession();
                    LOGGER.info("[会话拦截器]进入SessionInterceptor会话拦截器,开始进行前置校验,sessionId=" + 
                    session.getId());

                if (session.getAttribute("user_id") == null) {
                    LOGGER.error("[会话拦截器]-[正式环境]session中用户信息为空,开始执行重定向操作,跳转至支付宝
                    授权页面:url=" + CommonConstant.REAL_URL);
                    /**重定向到认证页面防止authcode为空*/
                    httpServletResponse.sendRedirect(CommonConstant.REAL_URL);
                    return false;
                } else {
                    session.setAttribute("user_id", session.getAttribute("user_id"));
                    return true;
                }

            }
    ............    

代码逻辑比较简单,从会话中尝试取出用户的user_id的值,如果取值为空则触发强制重定向。否则刷新会话信息并继续执行剩余逻辑。

总结

本文分析了支付宝oauth2认证前提下导致用户id为空的几种场景并给出相应解决方案,总的来说就是几条原则:

  1. 问题分析要基于日志,还原用户场景,从根源解决问题;
  2. 不信任任何外部的输入输出信息;
  3. 对于封装的sdk代码最好跑一下测试用例或者自测,自己封装的库应当有较为全面的测试用例,不给别人挖坑也避免掉进前辈挖的“坑”。
  4. 及时修复库bug,随时做好重写的准备。
文章目录
  1. 1. 场景一.用户转发链接导致openid为空
    1. 1.1. 报错日志
    2. 1.2. 错误分析
  2. 2. 场景二.支付宝侧接口直接返回uid为空
    1. 2.1. 错误分析
  3. 3. 解决方法
    1. 3.1. 1.工具类完善空值校验
    2. 3.2. 2.业务层增加拦截器强制重定向
  4. 4. 总结
Fork me on GitHub