Commit 314dee430ed0f90e0e0ef9be3ff6e2b8d9c5529f
1 parent
40e1a879
bugfix:微信公众号登录报 access token 的问题
Showing
3 changed files
with
196 additions
and
2 deletions
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 | ... | ... |