Commit 314dee430ed0f90e0e0ef9be3ff6e2b8d9c5529f

Authored by YunaiV
1 parent 40e1a879

bugfix:微信公众号登录报 access token 的问题

yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/YudaoAuthRequestFactory.java
... ... @@ -4,6 +4,7 @@ import cn.hutool.core.util.EnumUtil;
4 4 import cn.hutool.core.util.ReflectUtil;
5 5 import cn.iocoder.yudao.framework.social.core.enums.AuthExtendSource;
6 6 import cn.iocoder.yudao.framework.social.core.request.AuthWeChatMiniAppRequest;
  7 +import cn.iocoder.yudao.framework.social.core.request.AuthWeChatMpRequest;
7 8 import com.xingyuv.jushauth.cache.AuthStateCache;
8 9 import com.xingyuv.jushauth.config.AuthConfig;
9 10 import com.xingyuv.jushauth.config.AuthSource;
... ... @@ -13,6 +14,8 @@ import com.xingyuv.justauth.autoconfigure.JustAuthProperties;
13 14  
14 15 import java.lang.reflect.Method;
15 16  
  17 +import static com.xingyuv.jushauth.config.AuthDefaultSource.WECHAT_MP;
  18 +
16 19 /**
17 20 * 第三方授权拓展 request 工厂类
18 21 * 为使得拓展配置 {@link AuthConfig} 和默认配置齐平,所以自定义本工厂类
... ... @@ -55,6 +58,12 @@ public class YudaoAuthRequestFactory extends AuthRequestFactory {
55 58 }
56 59  
57 60 protected AuthRequest getExtendRequest(String source) {
  61 + // TODO 芋艿:临时兼容 justauth 迁移的类型不对问题;
  62 + if (WECHAT_MP.name().equalsIgnoreCase(source)) {
  63 + AuthConfig config = properties.getType().get(WECHAT_MP.name());
  64 + return new AuthWeChatMpRequest(config, authStateCache);
  65 + }
  66 +
58 67 AuthExtendSource authExtendSource;
59 68 try {
60 69 authExtendSource = EnumUtil.fromString(AuthExtendSource.class, source.toUpperCase());
... ...
yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMpRequest.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.social.core.request;
  2 +
  3 +import com.alibaba.fastjson.JSONObject;
  4 +import com.xingyuv.jushauth.cache.AuthStateCache;
  5 +import com.xingyuv.jushauth.config.AuthConfig;
  6 +import com.xingyuv.jushauth.config.AuthDefaultSource;
  7 +import com.xingyuv.jushauth.enums.AuthResponseStatus;
  8 +import com.xingyuv.jushauth.enums.AuthUserGender;
  9 +import com.xingyuv.jushauth.enums.scope.AuthWechatMpScope;
  10 +import com.xingyuv.jushauth.exception.AuthException;
  11 +import com.xingyuv.jushauth.model.AuthCallback;
  12 +import com.xingyuv.jushauth.model.AuthResponse;
  13 +import com.xingyuv.jushauth.model.AuthToken;
  14 +import com.xingyuv.jushauth.model.AuthUser;
  15 +import com.xingyuv.jushauth.request.AuthDefaultRequest;
  16 +import com.xingyuv.jushauth.utils.AuthScopeUtils;
  17 +import com.xingyuv.jushauth.utils.GlobalAuthUtils;
  18 +import com.xingyuv.jushauth.utils.HttpUtils;
  19 +import com.xingyuv.jushauth.utils.UrlBuilder;
  20 +
  21 +/**
  22 + * 微信公众平台登录
  23 + *
  24 + * @author yangkai.shen (https://xkcoding.com)
  25 + * @since 1.1.0
  26 + */
  27 +public class AuthWeChatMpRequest extends AuthDefaultRequest {
  28 + public AuthWeChatMpRequest(AuthConfig config) {
  29 + super(config, AuthDefaultSource.WECHAT_MP);
  30 + }
  31 +
  32 + public AuthWeChatMpRequest(AuthConfig config, AuthStateCache authStateCache) {
  33 + super(config, AuthDefaultSource.WECHAT_MP, authStateCache);
  34 + }
  35 +
  36 + /**
  37 + * 微信的特殊性,此时返回的信息同时包含 openid 和 access_token
  38 + *
  39 + * @param authCallback 回调返回的参数
  40 + * @return 所有信息
  41 + */
  42 + @Override
  43 + protected AuthToken getAccessToken(AuthCallback authCallback) {
  44 + return this.getToken(accessTokenUrl(authCallback.getCode()));
  45 + }
  46 +
  47 + @Override
  48 + protected AuthUser getUserInfo(AuthToken authToken) {
  49 + String openId = authToken.getOpenId();
  50 +
  51 + String response = doGetUserInfo(authToken);
  52 + JSONObject object = JSONObject.parseObject(response);
  53 +
  54 + this.checkResponse(object);
  55 +
  56 + String location = String.format("%s-%s-%s", object.getString("country"), object.getString("province"), object.getString("city"));
  57 +
  58 + if (object.containsKey("unionid")) {
  59 + authToken.setUnionId(object.getString("unionid"));
  60 + }
  61 +
  62 + return AuthUser.builder()
  63 + .rawUserInfo(object)
  64 + .username(object.getString("nickname"))
  65 + .nickname(object.getString("nickname"))
  66 + .avatar(object.getString("headimgurl"))
  67 + .location(location)
  68 + .uuid(openId)
  69 + .gender(AuthUserGender.getWechatRealGender(object.getString("sex")))
  70 + .token(authToken)
  71 + .source(source.toString())
  72 + .build();
  73 + }
  74 +
  75 + @Override
  76 + public AuthResponse refresh(AuthToken oldToken) {
  77 + return AuthResponse.builder()
  78 + .code(AuthResponseStatus.SUCCESS.getCode())
  79 + .data(this.getToken(refreshTokenUrl(oldToken.getRefreshToken())))
  80 + .build();
  81 + }
  82 +
  83 + /**
  84 + * 检查响应内容是否正确
  85 + *
  86 + * @param object 请求响应内容
  87 + */
  88 + private void checkResponse(JSONObject object) {
  89 + if (object.containsKey("errcode")) {
  90 + throw new AuthException(object.getIntValue("errcode"), object.getString("errmsg"));
  91 + }
  92 + }
  93 +
  94 + /**
  95 + * 获取token,适用于获取access_token和刷新token
  96 + *
  97 + * @param accessTokenUrl 实际请求token的地址
  98 + * @return token对象
  99 + */
  100 + private AuthToken getToken(String accessTokenUrl) {
  101 + String response = new HttpUtils(config.getHttpConfig()).get(accessTokenUrl).getBody();
  102 + JSONObject accessTokenObject = JSONObject.parseObject(response);
  103 +
  104 + this.checkResponse(accessTokenObject);
  105 +
  106 + return AuthToken.builder()
  107 + .accessToken(accessTokenObject.getString("access_token"))
  108 + .refreshToken(accessTokenObject.getString("refresh_token"))
  109 + .expireIn(accessTokenObject.getIntValue("expires_in"))
  110 + .openId(accessTokenObject.getString("openid"))
  111 + .scope(accessTokenObject.getString("scope"))
  112 + .build();
  113 + }
  114 +
  115 + /**
  116 + * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
  117 + *
  118 + * @param state state 验证授权流程的参数,可以防止csrf
  119 + * @return 返回授权地址
  120 + * @since 1.9.3
  121 + */
  122 + @Override
  123 + public String authorize(String state) {
  124 + return UrlBuilder.fromBaseUrl(source.authorize())
  125 + .queryParam("appid", config.getClientId())
  126 + .queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri()))
  127 + .queryParam("response_type", "code")
  128 + .queryParam("scope", this.getScopes(",", false, AuthScopeUtils.getDefaultScopes(AuthWechatMpScope.values())))
  129 + .queryParam("state", getRealState(state).concat("#wechat_redirect"))
  130 + .build();
  131 + }
  132 +
  133 + /**
  134 + * 返回获取accessToken的url
  135 + *
  136 + * @param code 授权码
  137 + * @return 返回获取accessToken的url
  138 + */
  139 + @Override
  140 + protected String accessTokenUrl(String code) {
  141 + return UrlBuilder.fromBaseUrl(source.accessToken())
  142 + .queryParam("appid", config.getClientId())
  143 + .queryParam("secret", config.getClientSecret())
  144 + .queryParam("code", code)
  145 + .queryParam("grant_type", "authorization_code")
  146 + .build();
  147 + }
  148 +
  149 + /**
  150 + * 返回获取userInfo的url
  151 + *
  152 + * @param authToken 用户授权后的token
  153 + * @return 返回获取userInfo的url
  154 + */
  155 + @Override
  156 + protected String userInfoUrl(AuthToken authToken) {
  157 + return UrlBuilder.fromBaseUrl(source.userInfo())
  158 + .queryParam("access_token", authToken.getAccessToken())
  159 + .queryParam("openid", authToken.getOpenId())
  160 + .queryParam("lang", "zh_CN")
  161 + .build();
  162 + }
  163 +
  164 + /**
  165 + * 返回获取userInfo的url
  166 + *
  167 + * @param refreshToken getAccessToken方法返回的refreshToken
  168 + * @return 返回获取userInfo的url
  169 + */
  170 + @Override
  171 + protected String refreshTokenUrl(String refreshToken) {
  172 + return UrlBuilder.fromBaseUrl(source.refresh())
  173 + .queryParam("appid", config.getClientId())
  174 + .queryParam("grant_type", "refresh_token")
  175 + .queryParam("refresh_token", refreshToken)
  176 + .build();
  177 + }
  178 +}
... ...
yudao-server/src/main/resources/application-local.yaml
... ... @@ -165,8 +165,10 @@ debug: false
165 165 --- #################### 微信公众号、小程序相关配置 ####################
166 166 wx:
167 167 mp: # 公众号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档
168   - app-id: wx041349c6f39b268b
169   - secret: 5abee519483bc9f8cb37ce280e814bd0
  168 +# app-id: wx041349c6f39b268b
  169 +# secret: 5abee519483bc9f8cb37ce280e814bd0
  170 + app-id: wx5b23ba7a5589ecbb # 测试号
  171 + secret: 2a7b3b20c537e52e74afd395eb85f61f
170 172 # 存储配置,解决 AccessToken 的跨节点的共享
171 173 config-storage:
172 174 type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
... ... @@ -221,6 +223,11 @@ justauth:
221 223 client-secret: ${wx.miniapp.secret}
222 224 ignore-check-redirect-uri: true
223 225 ignore-check-state: true # 微信小程序,不会使用到 state,所以不进行校验
  226 + WECHAT_MP: # 微信公众号
  227 + client-id: ${wx.mp.app-id}
  228 + client-secret: ${wx.mp.secret}
  229 + ignore-check-redirect-uri: true
  230 + ignore-check-state: true # 微信公众号,未调用后端的 getSocialAuthorizeUrl 方法,所以无法进行 state 校验 TODO 芋艿:后续考虑支持
224 231  
225 232 cache:
226 233 type: REDIS
... ...