Spring Security-18-开发微信登录

上文中实现了整个QQ登录的流程,本文来实现以下微信的授权登录, 总体逻辑是和QQ一样的,部分地方有所区别

ServiceProvider构建

构建 ServiceProvider 需要 APIOAuth2Operations

微信API

新建接口 WeChat

1
2
3
4
5
6
7
8
public interface WeChat {

/**
* 获取微信的用户信息
* @return {@link WeixinUserInfo}
*/
WeixinUserInfo getUserInfo(String openId);
}

这里和QQ有所区别, QQ 是获取到token之后,再去获取openId,然后再去获取微信的用户信息, 而微信在获取token的时候,就会把 openId 返回来,所以这里直接传过来就好了

然后是实现类 WeChatImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Slf4j
public class WeChatImpl extends AbstractOAuth2ApiBinding implements WeChat {


private ObjectMapper objectMapper = new ObjectMapper();

/**
* 获取用户信息的url
*/
private static final String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";

public WeChatImpl(String accessToken) {
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
}

/**
* 默认注册的StringHttpMessageConverter字符集为ISO-8859-1,而微信返回的是UTF-8的,所以覆盖了原来的方法。
*/
@Override
protected List<HttpMessageConverter<?>> getMessageConverters() {
List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
messageConverters.remove(0);
messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return messageConverters;
}

@Override
public WeixinUserInfo getUserInfo(String openId) {
String url = URL_GET_USER_INFO + openId;
String responseStr = getRestTemplate().getForObject(url, String.class);

log.info("获取微信信息的用户返回数据:{}", responseStr);

if(StringUtils.contains(responseStr, "errcode")) {
return null;
}

WeixinUserInfo userInfo = null;
try {
userInfo = objectMapper.readValue(responseStr, WeixinUserInfo.class);
} catch (IOException e) {
log.info("微信用户信息转换失败:{}", e);
}

return userInfo;
}
}

这里和QQ大致一样, 重写了一个方法是 getMessageConverters(), 设置了一下编码格式,否则可能返回来是乱码

API 到这里就完成了,然后是 OAuth2Operations

OAuth2Operations

创建 WeChatOAuth2Template 然后继承 OAuth2Template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
@Slf4j
public class WeChatOAuth2Template extends OAuth2Template {


private String clientId;

private String clientSecret;

private String accessTokenUrl;

private ObjectMapper objectMapper = new ObjectMapper();

/**
* 刷新token的url
*/
private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";

public WeChatOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true);

this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessTokenUrl = accessTokenUrl;
}

/**
* 构建获取授权码的请求。也就是引导用户跳转到微信的地址。
*/
@Override
public String buildAuthenticateUrl(OAuth2Parameters parameters) {
String url = super.buildAuthenticateUrl(parameters);
url = url + "&appid=" + clientId + "&scope=snsapi_login";
return url;
}

@Override
public String buildAuthorizeUrl(OAuth2Parameters parameters) {
return buildAuthenticateUrl(parameters);
}

/**
* 微信返回的contentType是html/text,添加相应的HttpMessageConverter来处理。
*/
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}


/**
* 获取accesstoken
* @param authorizationCode
* @param redirectUri
* @param additionalParameters
* @return
*/
@Override
public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri, MultiValueMap<String, String> additionalParameters) {
// 拼接url
StringBuilder accessTokenRequestUrl = new StringBuilder();
accessTokenRequestUrl.append("?appid=").append(clientId);
accessTokenRequestUrl.append("&secret=").append(clientSecret);
accessTokenRequestUrl.append("&code=").append(authorizationCode);
accessTokenRequestUrl.append("&grant_type=authorization_code");
accessTokenRequestUrl.append("&redirect_uri=").append(redirectUri);

return getAccessToken(accessTokenRequestUrl);
}

/**
* 刷新token的方法
* @param refreshToken
* @param additionalParameters
* @return
*/
@Override
public AccessGrant refreshAccess(String refreshToken, MultiValueMap<String, String> additionalParameters) {

StringBuilder refreshTokenUrl = new StringBuilder(REFRESH_TOKEN_URL);

refreshTokenUrl.append("?appid="+clientId);
refreshTokenUrl.append("&grant_type=refresh_token");
refreshTokenUrl.append("&refresh_token="+refreshToken);

return getAccessToken(refreshTokenUrl);
}

private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {
log.info("获取微信token,请求:{}", accessTokenRequestUrl);

String url = accessTokenUrl + accessTokenRequestUrl;
String responseStr = getRestTemplate().getForObject(url , String.class);

log.info("获取微信token,返回:{}", responseStr);

// 转成一个map
Map<String, Object> result = null;

try {
result = objectMapper.readValue(responseStr, Map.class);
} catch (IOException e) {
log.error("获取微信token,转换失败:{}", e);
}

// 返回错误码时直接返回空
if(StringUtils.isNotBlank(MapUtils.getString(result, "errcode"))){
String errcode = MapUtils.getString(result, "errcode");
String errmsg = MapUtils.getString(result, "errmsg");
throw new RuntimeException("获取access token失败, errcode:" + errcode + ", errmsg:" + errmsg);
}

// 构建 AccessGrant 对象返回
WeChatAccessGrant weChatAccessGrant = new WeChatAccessGrant(
MapUtils.getString(result, "access_token"),
MapUtils.getString(result, "scope"),
MapUtils.getString(result, "refresh_token"),
MapUtils.getLong(result, "expires_in")
);
weChatAccessGrant.setOpenId(MapUtils.getString(result, "openid"));

return weChatAccessGrant;
}
}

这里重写的方法比较多,主要是获取token的方法, 在标准的 OAuth 流程中, APPID和 appSecret的参数名是 client_idclient_secret 这个是 OAuth2Template 里面写死的参数名, 但是在微信的请求中,参数名是 appId 所以我们只能重写一下她的获取token的方法

这里用到的一个实体类是 WeChatAccessGrant 这个是我们自定义的,继承自 AccessGrant 因为微信返回来的数据多了一个 openId

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class WeChatAccessGrant extends AccessGrant {

private String openId;

public WeChatAccessGrant(){
super("");
}

public WeChatAccessGrant(String accessToken, String scope, String refreshToken, Long expiresIn) {
super(accessToken, scope, refreshToken, expiresIn);
}

}

ServiceProvider

APIOAuth2Operations 都有了,就可以构建 ServiceProvider 了,新建 WeChatServiceProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class WeChatServiceProvider extends AbstractOAuth2ServiceProvider<WeChat> {

/**
* 微信获取授权码的url
*/
private static final String URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";
/**
* 微信获取accessToken的url
*/
private static final String URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";

public WeChatServiceProvider(String appId, String appSecret) {
super(new WeChatOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
}

@Override
public WeChat getApi(String accessToken) {
return new WeChatImpl(accessToken);
}
}

和 QQ 基本类似

ApiAdapter

构建 ConnectionFactory 需要 ServiceProviderApiAdapter , 下面看一下 ApiAdapter ,新建 WeChatAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class WeChatAdapter implements ApiAdapter<WeChat> {

private String openId;

public WeChatAdapter() {}

public WeChatAdapter(String openId){
this.openId = openId;
}

@Override
public boolean test(WeChat api) {
return true;
}

@Override
public void setConnectionValues(WeChat api, ConnectionValues values) {
WeixinUserInfo userInfo = api.getUserInfo(openId);

values.setProviderUserId(userInfo.getOpenid());
values.setImageUrl(userInfo.getHeadimgurl() != null ? userInfo.getHeadimgurl() : null);
values.setProfileUrl(null);
values.setDisplayName(userInfo.getNickname());
}

@Override
public UserProfile fetchUserProfile(WeChat api) {
return null;
}

@Override
public void updateStatus(WeChat api, String message) {

}
}

这里和QQ基本都是一样的,也不做过多解释

ConnectionFactory

新建 WeChatConnectionFactory 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class WeChatConnectionFactory  extends OAuth2ConnectionFactory<WeChat> {


public WeChatConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new WeChatServiceProvider(appId, appSecret), new WeChatAdapter());
}

@Override
public Connection<WeChat> createConnection(AccessGrant accessGrant) {
return new OAuth2Connection<WeChat>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),
accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));
}

@Override
public Connection<WeChat> createConnection(ConnectionData data) {
return new OAuth2Connection<WeChat>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
}

/**
* 获取第三方用户的openId
* 由于微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取
*/
@Override
protected String extractProviderUserId(AccessGrant accessGrant) {
if(accessGrant instanceof WeChatAccessGrant) {
return ((WeChatAccessGrant)accessGrant).getOpenId();
}
return null;
}

/**
* 把openid传给 WeChatAdapter
* @param providerUserId
* @return
*/
private ApiAdapter<WeChat> getApiAdapter(String providerUserId) {
return new WeChatAdapter(providerUserId);
}

private OAuth2ServiceProvider<WeChat> getOAuth2ServiceProvider() {
return (OAuth2ServiceProvider<WeChat>) getServiceProvider();
}
}

这里跟QQ不同的地方也是因为 微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取

配置类

最后是微信的配置类, 新建 WeChatProperties

1
2
3
4
5
6
7
8
9
@Data
public class WeChatProperties extends SocialProperties {

/**
* 第三方id,用来决定发起第三方登录的url,默认是 weixin。
*/
private String providerId = "weixin";

}

这里也和QQ是一样的,然后放到 SocialProperties 里面

最后是微信的自动配置 新建 WeChatAutoConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@ConditionalOnProperty(prefix = "core.security.social.weChat", name = "app-id")
public class WeChatAutoConfig extends SocialAutoConfigurerAdapter {

@Autowired
private SecurityProperties securityProperties;

@Override
protected ConnectionFactory<?> createConnectionFactory() {
WeChatProperties weChat = securityProperties.getSocial().getWeChat();
return new WeChatConnectionFactory(weChat.getProviderId(), weChat.getAppId(), weChat.getAppSecret());
}
}

这里也和QQ是一样的

配置文件配置

application.yml 中配置微信的appid等信息

1
2
3
4
5
6
7
core:
security:
social:
weChat:
app-id: xxx
app-secret: xxx
providerId: xxx

页面配置

加入微信登录的跳转

1
<a href = '/qqLogin/weChat'>微信登录</a>

这里的前缀还是 qqLogin 因为我们配置的 filterProcessesUrl/qqLogin

到这里全部的功能就完成了

总结

  • API 部分: 拿到token之后获取用户信息的接口
  • OAuth2Template 部分: 通过 appid 等信息去获取token
  • WeChatServiceProvider: 把 OAuth2Template 用到的一些 clientId, clientSecret 等等信息传过去
  • WeChatConnectionFactory 创建 Connection 对象,以及指定api适配器
  • WeChatAutoConfig 读取用户的配置

本文代码传送门