支付宝oauth2获取用户信息那些坑
最近开发的一个应用需要走支付宝的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×tamp=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为空的几种场景并给出相应解决方案,总的来说就是几条原则:
- 问题分析要基于日志,还原用户场景,从根源解决问题;
- 不信任任何外部的输入输出信息;
- 对于封装的sdk代码最好跑一下测试用例或者自测,自己封装的库应当有较为全面的测试用例,不给别人挖坑也避免掉进前辈挖的“坑”。
- 及时修复库bug,随时做好重写的准备。