您的位置:首页 > 编程语言 > Java开发

Spring Security 源码分析(四):Spring Social实现微信社交登录

2018-01-23 13:44 686 查看

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


前言

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

准备工作

熟悉OAuth2.0协议标准,微信登录是基于OAuth2.0中的authorization_code模式的授权登录;

微信开放平台申请网站应用开发,获取
appid
appsecret


熟读网站应用微信登录开发指南

参考Spring-Security源码分析三-Spring-Social社交登录过程的准备工作

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

appid
wxfd6965ab1fc6adb2
appsecret
66bb4566de776ac699ec1dbed0cc3dd1

目录结构



参考

api
定义api绑定的公共接口

config
微信的一些配置信息

connect
与服务提供商建立连接所需的一些类。

定义返回用户信息接口

public interface Weixin {


WeixinUserInfo getUserInfo(String openId);


}


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

处理微信返回的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);


}


}


实现返回用户信息接口

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
中获取;

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
是传递的参数是
appid
secret
而不是标准的
client_id
client_secret


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


}


}


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


}


}


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) {


}


}


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


}


}


社交登录配置类

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

效果如下:



代码下载

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

推荐系列:

Spring Security源码分析(一):Spring Security认证过程

Spring Security源码分析(二):Spring Security授权过程
https://mp.weixin.qq.com/s?__biz=MzU0MDEwMjgwNA==&mid=2247484233&idx=1&sn=1e84ffd8c9169db56a0d48ccb31bc842&chksm=fb3f1ab2cc4893a4263799c466d73ee67971ce9deb22a91b8ae8e968621679de3bce83a2c558&mpshare=1&scene=24&srcid=0119R1KE5Q7t4Ym1RERJzexH#rd
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: