⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://niocoder.com/2018/01/12/Spring-Security源码分析四-Spring-Social社交登录过程/ 「龙飞」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

社交登录又称作社会化登录(Social Login),是指网站的用户可以使用腾讯QQ、人人网、开心网、新浪微博、搜狐微博、腾讯微博、淘宝、豆瓣、MSN、Google等社会化媒体账号登录该网站。

1. 前言

在上一章Spring-Security源码分析三-Spring-Social社交登录过程中,我们已经实现了使用Spring Social+Security的QQ社交登录。本章我们将实现微信的社交登录。(微信和QQ登录的大体流程相同,但存在一些细节上的差异,下面我们来简单实现一下)

1.1 准备工作

  1. 熟悉OAuth2.0协议标准,微信登录是基于OAuth2.0中的authorization_code模式的授权登录;
  2. 微信开放平台申请网站应用开发,获取appidappsecret
  3. 熟读网站应用微信登录开发指南
  4. 参考Spring-Security源码分析三-Spring-Social社交登录过程的准备工作

为了方便大家测试,博主在某宝租用了一个月的appid和appSecret

appid wxfd6965ab1fc6adb2
appsecret 66bb4566de776ac699ec1dbed0cc3dd1

1.2 目录结构

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/spring_social_weixin.png

参考

  1. api 定义api绑定的公共接口
  2. config 微信的一些配置信息
  3. connect与服务提供商建立连接所需的一些类。

1.2.1 定义返回用户信息接口

public interface Weixin {
WeixinUserInfo getUserInfo(String openId);
}

这里我们看到相对于QQ的getUserInfo微信多了一个参数openId。这是因为微信文档中在OAuth2.0的认证流程示意图第五步时,微信的openidaccess_token一起返回。而Spring Social获取access_token的类AccessGrant.java中没有openid。因此我们自己需要扩展一下Spring Social获取令牌的类(AccessGrant.java);

1.2.2 处理微信返回的access_token类(添加openid)

@Data
public class WeixinAccessGrant extends AccessGrant{

private String openId;

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

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

1.2.3 实现返回用户信息接口

public class WeiXinImpl extends AbstractOAuth2ApiBinding implements Weixin {

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

private ObjectMapper objectMapper = new ObjectMapper();

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

/**
* 获取用户信息
*
* @param openId
* @return
*/
@Override
public WeixinUserInfo getUserInfo(String openId) {
String url = WEIXIN_URL_GET_USER_INFO + openId;

String result = getRestTemplate().getForObject(url, String.class);
if(StringUtils.contains(result, "errcode")) {
return null;
}

WeixinUserInfo userInfo = null;

try{
userInfo = objectMapper.readValue(result,WeixinUserInfo.class);
}catch (Exception e){
e.printStackTrace();
}

return userInfo;
}

/**
* 使用utf-8 替换默认的ISO-8859-1编码
* @return
*/
@Override
protected List<HttpMessageConverter<?>> getMessageConverters() {
List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
messageConverters.remove(0);
messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return messageConverters;
}
}

QQ获取用户信息相比,微信的实现类中少了一步通过access_token获取openid的请求。openid由自己定义的扩展类WeixinAccessGrant中获取;

1.2.4 WeixinOAuth2Template处理微信返回的令牌信息

@Slf4j
public class WeixinOAuth2Template extends OAuth2Template {

private String clientId;

private String clientSecret;

private String accessTokenUrl;

private static final String REFRESH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";

public WeixinOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
setUseParametersForClientAuthentication(true);
this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessTokenUrl = accessTokenUrl;
}

/* (non-Javadoc)
* @see org.springframework.social.oauth2.OAuth2Template#exchangeForAccess(java.lang.String, java.lang.String, org.springframework.util.MultiValueMap)
*/
@Override
public AccessGrant exchangeForAccess(String authorizationCode, String redirectUri,
MultiValueMap<String, String> parameters) {

StringBuilder accessTokenRequestUrl = new StringBuilder(accessTokenUrl);

accessTokenRequestUrl.append("?appid="+clientId);
accessTokenRequestUrl.append("&secret="+clientSecret);
accessTokenRequestUrl.append("&code="+authorizationCode);
accessTokenRequestUrl.append("&grant_type=authorization_code");
accessTokenRequestUrl.append("&redirect_uri="+redirectUri);

return getAccessToken(accessTokenRequestUrl);
}

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);
}

@SuppressWarnings("unchecked")
private AccessGrant getAccessToken(StringBuilder accessTokenRequestUrl) {

log.info("获取access_token, 请求URL: "+accessTokenRequestUrl.toString());

String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);

log.info("获取access_token, 响应内容: "+response);

Map<String, Object> result = null;
try {
result = new ObjectMapper().readValue(response, Map.class);
} catch (Exception e) {
e.printStackTrace();
}

//返回错误码时直接返回空
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);
}

WeixinAccessGrant accessToken = new WeixinAccessGrant(
MapUtils.getString(result, "access_token"),
MapUtils.getString(result, "scope"),
MapUtils.getString(result, "refresh_token"),
MapUtils.getLong(result, "expires_in"));

accessToken.setOpenId(MapUtils.getString(result, "openid"));

return accessToken;
}

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

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

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

QQ处理令牌类相比多了三个全局变量并且复写了exchangeForAccess方法。这是因为微信在通过code获取access_token是传递的参数是appidsecret而不是标准client_idclient_secret

1.2.5 WeixinServiceProvider连接服务提供商

public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> {

/**
* 微信获取授权码的url
*/
private static final String WEIXIN_URL_AUTHORIZE = "https://open.weixin.qq.com/connect/qrconnect";
/**
* 微信获取accessToken的url(微信在获取accessToken时也已经返回openId)
*/
private static final String WEIXIN_URL_ACCESS_TOKEN = "https://api.weixin.qq.com/sns/oauth2/access_token";

public WeixinServiceProvider(String appId, String appSecret) {
super(new WeixinOAuth2Template(appId, appSecret, WEIXIN_URL_AUTHORIZE, WEIXIN_URL_ACCESS_TOKEN));
}

@Override
public Weixin getApi(String accessToken) {
return new WeiXinImpl(accessToken);
}
}

1.2.6 WeixinConnectionFactory连接服务提供商的工厂类

public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin> {

/**
* @param appId
* @param appSecret
*/
public WeixinConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter());
}

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

/* (non-Javadoc)
* @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.oauth2.AccessGrant)
*/
public Connection<Weixin> createConnection(AccessGrant accessGrant) {
return new OAuth2Connection<Weixin>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),
accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter(extractProviderUserId(accessGrant)));
}

/* (non-Javadoc)
* @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.connect.ConnectionData)
*/
public Connection<Weixin> createConnection(ConnectionData data) {
return new OAuth2Connection<Weixin>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
}

private ApiAdapter<Weixin> getApiAdapter(String providerUserId) {
return new WeixinAdapter(providerUserId);
}

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

}

1.2.7 WeixinAdapter将微信api返回的数据模型适配Spring Social的标准模型

public class WeixinAdapter implements ApiAdapter<Weixin> {

private String openId;

public WeixinAdapter() {
}

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

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

@Override
public void setConnectionValues(Weixin api, ConnectionValues values) {
WeixinUserInfo userInfo = api.getUserInfo(openId);
values.setProviderUserId(userInfo.getOpenid());
values.setDisplayName(userInfo.getNickname());
values.setImageUrl(userInfo.getHeadimgurl());
}

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

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

}
}

1.2.8 WeixinAuthConfig创建工厂和设置数据源

@Configuration
public class WeixinAuthConfig extends SocialAutoConfigurerAdapter {

@Autowired
private DataSource dataSource;

@Autowired
private ConnectionSignUp myConnectionSignUp;

@Override
protected ConnectionFactory<?> createConnectionFactory() {
return new WeixinConnectionFactory(DEFAULT_SOCIAL_WEIXIN_PROVIDER_ID, SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_ID,
SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_SECRET);
}

@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource,
connectionFactoryLocator, Encryptors.noOpText());
if (myConnectionSignUp != null) {
repository.setConnectionSignUp(myConnectionSignUp);
}
return repository;
}

/**
* /connect/weixin POST请求,绑定微信返回connect/weixinConnected视图
* /connect/weixin DELETE请求,解绑返回connect/weixinConnect视图
* @return
*/
@Bean({"connect/weixinConnect", "connect/weixinConnected"})
@ConditionalOnMissingBean(name = "weixinConnectedView")
public View weixinConnectedView() {
return new SocialConnectView();
}

}

1.2.9 社交登录配置类

由于社交登录都是通过SocialAuthenticationFilter过滤器拦截的,如果 上一章 已经配置过,则本章不需要配置。

效果如下:

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/weixin.gif

2. 代码下载

从我的 github 中下载,https://github.com/longfeizheng/logback

文章目录
  1. 1. 1. 前言
    1. 1.1. 1.1 准备工作
    2. 1.2. 1.2 目录结构
      1. 1.2.1. 1.2.1 定义返回用户信息接口
      2. 1.2.2. 1.2.2 处理微信返回的access_token类(添加openid)
      3. 1.2.3. 1.2.3 实现返回用户信息接口
      4. 1.2.4. 1.2.4 WeixinOAuth2Template处理微信返回的令牌信息
      5. 1.2.5. 1.2.5 WeixinServiceProvider连接服务提供商
      6. 1.2.6. 1.2.6 WeixinConnectionFactory连接服务提供商的工厂类
      7. 1.2.7. 1.2.7 WeixinAdapter将微信api返回的数据模型适配Spring Social的标准模型
      8. 1.2.8. 1.2.8 WeixinAuthConfig创建工厂和设置数据源
      9. 1.2.9. 1.2.9 社交登录配置类
  2. 2. 2. 代码下载