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,6 +4,7 @@ import cn.hutool.core.util.EnumUtil; | ||
| 4 | import cn.hutool.core.util.ReflectUtil; | 4 | import cn.hutool.core.util.ReflectUtil; |
| 5 | import cn.iocoder.yudao.framework.social.core.enums.AuthExtendSource; | 5 | import cn.iocoder.yudao.framework.social.core.enums.AuthExtendSource; |
| 6 | import cn.iocoder.yudao.framework.social.core.request.AuthWeChatMiniAppRequest; | 6 | import cn.iocoder.yudao.framework.social.core.request.AuthWeChatMiniAppRequest; |
| 7 | +import cn.iocoder.yudao.framework.social.core.request.AuthWeChatMpRequest; | ||
| 7 | import com.xingyuv.jushauth.cache.AuthStateCache; | 8 | import com.xingyuv.jushauth.cache.AuthStateCache; |
| 8 | import com.xingyuv.jushauth.config.AuthConfig; | 9 | import com.xingyuv.jushauth.config.AuthConfig; |
| 9 | import com.xingyuv.jushauth.config.AuthSource; | 10 | import com.xingyuv.jushauth.config.AuthSource; |
| @@ -13,6 +14,8 @@ import com.xingyuv.justauth.autoconfigure.JustAuthProperties; | @@ -13,6 +14,8 @@ import com.xingyuv.justauth.autoconfigure.JustAuthProperties; | ||
| 13 | 14 | ||
| 14 | import java.lang.reflect.Method; | 15 | import java.lang.reflect.Method; |
| 15 | 16 | ||
| 17 | +import static com.xingyuv.jushauth.config.AuthDefaultSource.WECHAT_MP; | ||
| 18 | + | ||
| 16 | /** | 19 | /** |
| 17 | * 第三方授权拓展 request 工厂类 | 20 | * 第三方授权拓展 request 工厂类 |
| 18 | * 为使得拓展配置 {@link AuthConfig} 和默认配置齐平,所以自定义本工厂类 | 21 | * 为使得拓展配置 {@link AuthConfig} 和默认配置齐平,所以自定义本工厂类 |
| @@ -55,6 +58,12 @@ public class YudaoAuthRequestFactory extends AuthRequestFactory { | @@ -55,6 +58,12 @@ public class YudaoAuthRequestFactory extends AuthRequestFactory { | ||
| 55 | } | 58 | } |
| 56 | 59 | ||
| 57 | protected AuthRequest getExtendRequest(String source) { | 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 | AuthExtendSource authExtendSource; | 67 | AuthExtendSource authExtendSource; |
| 59 | try { | 68 | try { |
| 60 | authExtendSource = EnumUtil.fromString(AuthExtendSource.class, source.toUpperCase()); | 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,8 +165,10 @@ debug: false | ||
| 165 | --- #################### 微信公众号、小程序相关配置 #################### | 165 | --- #################### 微信公众号、小程序相关配置 #################### |
| 166 | wx: | 166 | wx: |
| 167 | mp: # 公众号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档 | 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 | # 存储配置,解决 AccessToken 的跨节点的共享 | 172 | # 存储配置,解决 AccessToken 的跨节点的共享 |
| 171 | config-storage: | 173 | config-storage: |
| 172 | type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 | 174 | type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 |
| @@ -221,6 +223,11 @@ justauth: | @@ -221,6 +223,11 @@ justauth: | ||
| 221 | client-secret: ${wx.miniapp.secret} | 223 | client-secret: ${wx.miniapp.secret} |
| 222 | ignore-check-redirect-uri: true | 224 | ignore-check-redirect-uri: true |
| 223 | ignore-check-state: true # 微信小程序,不会使用到 state,所以不进行校验 | 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 | cache: | 232 | cache: |
| 226 | type: REDIS | 233 | type: REDIS |