前言
在上一章Spring-Security源码分析三-Spring-Social社交登录过程中,我们已经实现了使用 SpringSocial
+ Security
的QQ社交登录。本章我们将实现微信的社交登录。(微信和QQ登录的大体流程相同,但存在一些细节上的差异,下面我们来简单实现一下)
准备工作
熟悉OAuth2.0协议标准,微信登录是基于OAuth2.0中的authorization_code模式的授权登录;
微信开放平台申请网站应用开发,获取 appid
和 appsecret
熟读网站应用微信登录开发指南
参考Spring-Security源码分析三-Spring-Social社交登录过程的准备工作
为了方便大家测试,博主在某宝租用了一个月的appid和appSecret
appid | |
appsecret | |
目录结构
参考
api
定义api绑定的公共接口
config
微信的一些配置信息
connect
与服务提供商建立连接所需的一些类。
定义返回用户信息接口
publicinterfaceWeixin{
WeixinUserInfo(String);
}
这里我们看到相对于QQ的 getUserInfo
微信多了一个参数 openId
。这是因为微信文档中在OAuth2.0的认证流程示意图第五步时,微信的 openid
同 access_token
一起返回。而 SpringSocial
获取 access_token
的类 AccessGrant.java
中没有 openid
。因此我们自己需要扩展一下 SpringSocial
获取令牌的类( AccessGrant.java
);
处理微信返回的access_token类(添加openid)
@Data
publicclassWeixinAccessGrantextendsAccessGrant{
privateString;
publicWeixinAccessGrant(){
super("");
}
publicWeixinAccessGrant(String,String,String,Long){
super(accessToken,,,);
}
}
实现返回用户信息接口
publicclassWeiXinImplextendsAbstractOAuth2ApiBindingimplementsWeixin{
/**
* 获取用户信息的url
*/
privatestaticfinalString="https://api.weixin.qq.com/sns/userinfo?openid=";
privateObjectMapper=newObjectMapper();
publicWeiXinImpl(String){
super(accessToken,TokenStrategy.ACCESS_TOKEN_PARAMETER);
}
/**
* 获取用户信息
*
* @param openId
* @return
*/
@Override
publicWeixinUserInfo(String){
String=+;
String=().getForObject(url,String.class);
if(StringUtils.contains(result,"errcode")){
returnnull;
}
WeixinUserInfo=null;
try{
=.readValue(result,WeixinUserInfo.class);
}catch(Exception){
.printStackTrace();
}
return;
}
/**
* 使用utf-8 替换默认的ISO-8859-1编码
* @return
*/
@Override
protectedList<HttpMessageConverter<?>>(){
List<HttpMessageConverter<?>>=super.getMessageConverters();
.remove(0);
.add(newStringHttpMessageConverter(Charset.forName("UTF-8")));
return;
}
}
与 QQ
获取用户信息相比, 微信
的实现类中少了一步通过 access_token
获取 openid
的请求。 openid
由自己定义的扩展类 WeixinAccessGrant
中获取;
WeixinOAuth2Template处理微信返回的令牌信息
@Slf4j
publicclassWeixinOAuth2TemplateextendsOAuth2Template{
privateString;
privateString;
privateString;
privatestaticfinalString="https://api.weixin.qq.com/sns/oauth2/refresh_token";
publicWeixinOAuth2Template(String,String,String,String){
super(clientId,,,);
(true);
this.clientId =;
this.clientSecret =;
this.accessTokenUrl =;
}
/* (non-Javadoc)
* @see org.springframework.social.oauth2.OAuth2Template#exchangeForAccess(java.lang.String, java.lang.String, org.springframework.util.MultiValueMap)
*/
@Override
publicAccessGrant(String,String,
MultiValueMap<String,String>){
StringBuilder=newStringBuilder(accessTokenUrl);
.append("?appid="+clientId);
.append("&secret="+clientSecret);
.append("&code="+authorizationCode);
.append("&grant_type=authorization_code");
.append("&redirect_uri="+redirectUri);
return(accessTokenRequestUrl);
}
publicAccessGrant(String,MultiValueMap<String,String>){
StringBuilder=newStringBuilder(REFRESH_TOKEN_URL);
.append("?appid="+clientId);
.append("&grant_type=refresh_token");
.append("&refresh_token="+refreshToken);
return(refreshTokenUrl);
}
@SuppressWarnings("unchecked")
privateAccessGrant(StringBuilder){
.info("获取access_token, 请求URL: "+accessTokenRequestUrl.toString());
String=().getForObject(accessTokenRequestUrl.toString(),String.class);
.info("获取access_token, 响应内容: "+response);
Map<String,Object>=null;
try{
=newObjectMapper().readValue(response,Map.class);
}catch(Exception){
.printStackTrace();
}
//返回错误码时直接返回空
if(StringUtils.isNotBlank(MapUtils.getString(result,"errcode"))){
String=MapUtils.getString(result,"errcode");
String=MapUtils.getString(result,"errmsg");
thrownewRuntimeException("获取access token失败, errcode:"+errcode+", errmsg:"+errmsg);
}
WeixinAccessGrant=newWeixinAccessGrant(
MapUtils.getString(result,"access_token"),
MapUtils.getString(result,"scope"),
MapUtils.getString(result,"refresh_token"),
MapUtils.getLong(result,"expires_in"));
.setOpenId(MapUtils.getString(result,"openid"));
return;
}
/**
* 构建获取授权码的请求。也就是引导用户跳转到微信的地址。
*/
publicString(OAuth2Parameters){
String=super.buildAuthenticateUrl(parameters);
=+"&appid="+clientId+"&scope=snsapi_login";
return;
}
publicString(OAuth2Parameters){
return(parameters);
}
/**
* 微信返回的contentType是html/text,添加相应的HttpMessageConverter来处理。
*/
protectedRestTemplate(){
RestTemplate=super.createRestTemplate();
.getMessageConverters().add(newStringHttpMessageConverter(Charset.forName("UTF-8")));
return;
}
}
与 QQ
处理令牌类相比多了三个全局变量并且复写了 exchangeForAccess
方法。这是因为 微信
在通过 code
获取 access_token
是传递的参数是 appid
和 secret
而不是标准的 client_id
和 client_secret
。
WeixinServiceProvider连接服务提供商
publicclassWeixinServiceProviderextendsAbstractOAuth2ServiceProvider<Weixin>{
/**
* 微信获取授权码的url
*/
privatestaticfinalString="https://open.weixin.qq.com/connect/qrconnect";
/**
* 微信获取accessToken的url(微信在获取accessToken时也已经返回openId)
*/
privatestaticfinalString="https://api.weixin.qq.com/sns/oauth2/access_token";
publicWeixinServiceProvider(String,String){
super(newWeixinOAuth2Template(appId,,,));
}
@Override
publicWeixin(String){
returnnewWeiXinImpl(accessToken);
}
}
WeixinConnectionFactory连接服务提供商的工厂类
publicclassWeixinConnectionFactoryextendsOAuth2ConnectionFactory<Weixin>{
/**
* @param appId
* @param appSecret
*/
publicWeixinConnectionFactory(String,String,String){
super(providerId,newWeixinServiceProvider(appId,),newWeixinAdapter());
}
/**
* 由于微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取
*/
@Override
protectedString(AccessGrant){
if(accessGrant instanceofWeixinAccessGrant){
return((WeixinAccessGrant)accessGrant).getOpenId();
}
returnnull;
}
/* (non-Javadoc)
* @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.oauth2.AccessGrant)
*/
publicConnection<Weixin>(AccessGrant){
returnnewOAuth2Connection<Weixin>(getProviderId(),(accessGrant),.getAccessToken(),
.getRefreshToken(),.getExpireTime(),(),(extractProviderUserId(accessGrant)));
}
/* (non-Javadoc)
* @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.connect.ConnectionData)
*/
publicConnection<Weixin>(ConnectionData){
returnnewOAuth2Connection<Weixin>(data,(),(data.getProviderUserId()));
}
privateApiAdapter<Weixin>(String){
returnnewWeixinAdapter(providerUserId);
}
privateOAuth2ServiceProvider<Weixin>(){
return(OAuth2ServiceProvider<Weixin>)();
}
}
WeixinAdapter将微信api返回的数据模型适配Spring Social的标准模型
publicclassWeixinAdapterimplementsApiAdapter<Weixin>{
privateString;
publicWeixinAdapter(){
}
publicWeixinAdapter(String){
this.openId =;
}
@Override
publicboolean(Weixin){
returntrue;
}
@Override
publicvoid(Weixin,ConnectionValues){
WeixinUserInfo=.getUserInfo(openId);
.setProviderUserId(userInfo.getOpenid());
.setDisplayName(userInfo.getNickname());
.setImageUrl(userInfo.getHeadimgurl());
}
@Override
publicUserProfile(Weixin){
returnnull;
}
@Override
publicvoid(Weixin,String){
}
}
WeixinAuthConfig创建工厂和设置数据源
@Configuration
publicclassWeixinAuthConfigextendsSocialAutoConfigurerAdapter{
@Autowired
privateDataSource;
@Autowired
privateConnectionSignUp;
@Override
protectedConnectionFactory<?>(){
returnnewWeixinConnectionFactory(DEFAULT_SOCIAL_WEIXIN_PROVIDER_ID,SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_ID,
SecurityConstants.DEFAULT_SOCIAL_WEIXIN_APP_SECRET);
}
@Override
publicUsersConnectionRepository(ConnectionFactoryLocator){
JdbcUsersConnectionRepository=newJdbcUsersConnectionRepository(dataSource,
,Encryptors.noOpText());
if(myConnectionSignUp !=null){
.setConnectionSignUp(myConnectionSignUp);
}
return;
}
/**
* /connect/weixin POST请求,绑定微信返回connect/weixinConnected视图
* /connect/weixin DELETE请求,解绑返回connect/weixinConnect视图
* @return
*/
@Bean({"connect/weixinConnect","connect/weixinConnected"})
@ConditionalOnMissingBean(name ="weixinConnectedView")
publicView(){
returnnewSocialConnectView();
}
}
社交登录配置类
由于社交登录都是通过 SocialAuthenticationFilter
过滤器拦截的,如果 上一章 已经配置过,则本章不需要配置。
效果如下:
代码下载
从我的 github 中下载,https://github.com/longfeizheng/logback