微信登陆和QQ登陆大致流程一致,只是有些api不一样,主要是QQ的getUserInfo微信多了一个参数openId。这是因为文档中在OAuth2.0的认证流程示意图第五步时,微信的openid 同access_token一起返回。而Spring Social获取access_token的类AccessGrant.java中没有openid
微信暂时没有申请测试账户,先上代码后续网站备案下来再自行测试
这里也分三个模块进行开发 api connect config
api
package com.rui.tiger.auth.core.social.wechat.api;
/**
* 微信用户api接口
* @author CaiRui
* @Date 2019-01-12 12:08
*/
public interface WechatApi {
/**
* 获取微信用户信息
* @param openId
* @return
*/
WechatUserInfo getUserInfo(String openId);
}
package com.rui.tiger.auth.core.social.wechat.api;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;
import java.nio.charset.Charset;
import java.util.List;
/**
* @author CaiRui
* @Date 2019-01-12 12:24
*/
@Slf4j
public class WechatApiImpl extends AbstractOAuth2ApiBinding implements WechatApi {
/**
* 获取用户信息的url
* https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
* access_token 父类会帮我们拼接
*/
private static final String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";
/**
* @param accessToken
*/
public WechatApiImpl(String accessToken) {
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
}
/**
* 默认注册的StringHttpMessageConverter字符集为ISO-8859-1,而微信返回的是UTF-8的,所以覆盖了原来的方法。
*/
protected List<HttpMessageConverter<?>> getMessageConverters() {
List<HttpMessageConverter<?>> messageConverters = super.getMessageConverters();
messageConverters.remove(0);
messageConverters.add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return messageConverters;
}
@Override
public WechatUserInfo getUserInfo(String openId) {
String url = URL_GET_USER_INFO + openId;
String response = getRestTemplate().getForObject(url, String.class);
if(StringUtils.contains(response, "errcode")) {
log.info("微信用户信息获取失败:"+response);
return null;
}
WechatUserInfo profile = null;
try {
profile = JSON.parseObject(response, WechatUserInfo.class);
} catch (Exception e) {
log.info("微信用户信息json转换异常",e);
}
return profile;
}
}
微信社交用户信息封装
package com.rui.tiger.auth.core.social.wechat.api;
import lombok.Data;
/**
* 微信用户信息
* https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316518&token=&lang=zh_CN}
* @author CaiRui
* @Date 2019-01-12 12:13
*/
@Data
public class WechatUserInfo {
/**
* 普通用户的标识,对当前开发者帐号唯一
*/
private String openid;
/**
* 普通用户昵称
*/
private String nickname;
/**
* 语言
*/
private String language;
/**
* 普通用户性别,1为男性,2为女性
*/
private String sex;
/**
* 普通用户个人资料填写的省份
*/
private String province;
/**
* 普通用户个人资料填写的城市
*/
private String city;
/**
* 国家,如中国为CN
*/
private String country;
/**
* 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
*/
private String headimgurl;
/**
* 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
*/
private String[] privilege;
/**
* 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。
*/
private String unionid;
}
connect
微信的返回比标准的多了个openId
package com.rui.tiger.auth.core.social.wechat.connect;
import org.springframework.social.oauth2.AccessGrant;
/**
* 微信的access_token信息。与标准OAuth2协议不同,微信在获取access_token时会同时返回openId,并没有单独的通过accessToke换取openId的服务
* 所以在这里继承了标准AccessGrant,添加了openId字段,作为对微信access_token信息的封装。
* @author CaiRui
* @Date 2019-01-12 13:08
*/
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);
}
/**
* @return the openId
*/
public String getOpenId() {
return openId;
}
/**
* @param openId the openId to set
*/
public void setOpenId(String openId) {
this.openId = openId;
}
}
api适配器
package com.rui.tiger.auth.core.social.wechat.connect;
import com.rui.tiger.auth.core.social.wechat.api.WechatApi;
import com.rui.tiger.auth.core.social.wechat.api.WechatUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;
/**
* 微信 api适配器,将微信 api的数据模型转为spring social的标准模型。
* @author CaiRui
* @Date 2019-01-12 13:34
*/
public class WechatApiAdapter implements ApiAdapter<WechatApi> {
private String openId;
public WechatApiAdapter() {}
public WechatApiAdapter(String openId){
this.openId = openId;
}
/**
* @param api
* @return
*/
@Override
public boolean test(WechatApi api) {
return true;
}
/**
* @param api
* @param values
*/
@Override
public void setConnectionValues(WechatApi api, ConnectionValues values) {
WechatUserInfo profile = api.getUserInfo(openId);
values.setProviderUserId(profile.getOpenid());
values.setDisplayName(profile.getNickname());
values.setImageUrl(profile.getHeadimgurl());
}
/**
* @param api
* @return
*/
@Override
public UserProfile fetchUserProfile(WechatApi api) {
return null;
}
/**
* @param api
* @param message
*/
@Override
public void updateStatus(WechatApi api, String message) {
//do nothing
}
}
package com.rui.tiger.auth.core.social.wechat.connect;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Parameters;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
import java.util.Map;
/**
* 完成微信的OAuth2认证流程的模板类。国内厂商实现的OAuth2每个都不同,
* spring默认提供的OAuth2Template适应不了,只能针对每个厂商自己微调。
* @author CaiRui
* @Date 2019-01-12 13:23
*/
@Slf4j
public class WechatOAuth2Template extends OAuth2Template {
private String clientId;
private String clientSecret;
private String accessTokenUrl;
/**
* 获取token
* https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
*/
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;
}
/* (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 = JSON.parseObject(response, Map.class);
} catch (Exception e) {
log.error("微信获取token解析json异常",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);
}
WechatAccessGrant accessToken = new WechatAccessGrant(
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;
}
/**
* 构建获取授权码的请求。也就是引导用户跳转到微信的地址。
* https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list&t=resource/res_list&verify=1&id=open1419316505&token=&lang=zh_CN
*/
public String buildAuthenticateUrl(OAuth2Parameters parameters) {
String url = super.buildAuthenticateUrl(parameters);
url=url.replace("client_id","appid");
//url = url + "&appid="+clientId+"&scope=snsapi_login";
url = url + "&scope=snsapi_login";
log.info("微信获取授权码地址url:"+url);
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;
}
}
package com.rui.tiger.auth.core.social.wechat.connect;
import com.rui.tiger.auth.core.social.wechat.api.WechatApi;
import com.rui.tiger.auth.core.social.wechat.api.WechatApiImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
/**
* 微信的OAuth2流程处理器的提供器,供spring social的connect体系调用
* @author CaiRui
* @Date 2019-01-12 13:40
*/
public class WechatServiceProvider extends AbstractOAuth2ServiceProvider<WechatApi> {
/**
* 微信获取授权码的url
*
* https://open.weixin.qq.com/connect/qrconnect?
* appid=APPID&
* redirect_uri=REDIRECT_URI&
* response_type=code&
* scope=SCOPE&
* state=STATE#wechat_redirect
*/
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";
/**
* @param appId
* @param appSecret
*/
public WechatServiceProvider(String appId, String appSecret) {
super(new WechatOAuth2Template(appId, appSecret,URL_AUTHORIZE,URL_ACCESS_TOKEN));
}
/* (non-Javadoc)
* @see org.springframework.social.oauth2.AbstractOAuth2ServiceProvider#getApi(java.lang.String)
*/
@Override
public WechatApi getApi(String accessToken) {
return new WechatApiImpl(accessToken);
}
}
package com.rui.tiger.auth.core.social.wechat.connect;
import com.rui.tiger.auth.core.social.wechat.api.WechatApi;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.Connection;
import org.springframework.social.connect.ConnectionData;
import org.springframework.social.connect.support.OAuth2Connection;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2ServiceProvider;
/**
* @author CaiRui
* @Date 2019-01-12 13:32
*/
public class WechatConnectionFactory extends OAuth2ConnectionFactory<WechatApi> {
/**
* @param appId
* @param appSecret
*/
public WechatConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new WechatServiceProvider(appId, appSecret), new WechatApiAdapter());
}
/**
* 由于微信的openId是和accessToken一起返回的,所以在这里直接根据accessToken设置providerUserId即可,不用像QQ那样通过QQAdapter来获取
*/
@Override
protected String extractProviderUserId(AccessGrant accessGrant) {
if(accessGrant instanceof WechatAccessGrant) {
return ((WechatAccessGrant)accessGrant).getOpenId();
}
return null;
}
/* (non-Javadoc)
* @see org.springframework.social.connect.support.OAuth2ConnectionFactory#createConnection(org.springframework.social.oauth2.AccessGrant)
*/
public Connection<WechatApi> createConnection(AccessGrant accessGrant) {
return new OAuth2Connection<WechatApi>(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<WechatApi> createConnection(ConnectionData data) {
return new OAuth2Connection<WechatApi>(data, getOAuth2ServiceProvider(), getApiAdapter(data.getProviderUserId()));
}
private ApiAdapter<WechatApi> getApiAdapter(String providerUserId) {
return new WechatApiAdapter(providerUserId);
}
private OAuth2ServiceProvider<WechatApi> getOAuth2ServiceProvider() {
return (OAuth2ServiceProvider<WechatApi>) getServiceProvider();
}
}
config
package com.rui.tiger.auth.core.social.wechat.config;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import com.rui.tiger.auth.core.properties.WechatProperties;
import com.rui.tiger.auth.core.social.wechat.connect.WechatConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactory;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
/**
* 微信登陆配置
* @author CaiRui extends SocialConfigurerAdapter
* @Date 2019-01-12 13:57
*/
//@Configuration
//@ConditionalOnProperty(prefix = "tiger.auth.social.wechat", name = "app-id")
public class WechatAutoConfiguration extends SocialConfigurerAdapter {
/* @Autowired
private SecurityProperties securityProperties;
@Override
public void addConnectionFactories(ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
connectionFactoryConfigurer.addConnectionFactory(createConnectionFactory());
}
private ConnectionFactory<?> createConnectionFactory() {
WechatProperties weixinConfig = securityProperties.getSocial().getWechat();
return new WechatConnectionFactory(weixinConfig.getProviderId(), weixinConfig.getAppId(),
weixinConfig.getAppSecret());
}
// 后补:做到处理注册逻辑的时候发现的一个bug:登录完成后,数据库没有数据,但是再次登录却不用注册了
// 就怀疑是否是在内存中存储了。结果果然发现这里父类的内存ConnectionRepository覆盖了SocialConfig中配置的jdbcConnectionRepository
// 这里需要返回null,否则会返回内存的 ConnectionRepository
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
return null;
}*/
}
配置文件调整
package com.rui.tiger.auth.core.properties;
/**
* 微信配置文件
* @author CaiRui
* @Date 2019-01-12 14:00
*/
public class WechatProperties {
/**
* 第三方id,用来决定发起第三方登录的url,默认是 weixin。
*/
private String providerId = "wechat";
private String appId;//应用id
private String appSecret;//应用密匙
public String getProviderId() {
return providerId;
}
public void setProviderId(String providerId) {
this.providerId = providerId;
}
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getAppSecret() {
return appSecret;
}
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
}
放到社交配置中
添加微信相关配置
social:
filterProcessesUrl: /auth
qq:
app-id: ***
app-secret: ***
wechat:
app-id: ***
app-secret: ***
前台界面添加微信登陆 tiger-login.html
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>图形验证码:</td>
<td>
<input type="text" name="imageCode">
<img src="/captcha/image">
</td>
</tr>
<tr>
<td colspan="2"><input type="checkbox" name="remember-me" value="true"/>记住我</td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
<h3>短信登录</h3>
<form action="/authentication/mobile" method="post">
<table>
<tr>
<td>手机号:</td>
<td><input type="text" name="mobile" value="15026929536"></td>
</tr>
<tr>
<td>短信验证码:</td>
<td>
<input type="text" name="smsCode">
<a href="/captcha/sms?mobile=13012345678">发送验证码</a>
</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
<!--<h3>社交登录</h3>
<!–不支持get请求 /auth/qq 默认是这个请求 /login/qq –>
<form action="/auth/qq" method="post">
<button type="submit">QQ登录</button>
<a href="/qqLogin/weixin">微信登录</a>
</form>-->
<h3>社交登录</h3>
<a href="/auth/qq">QQ登录</a>
<a href="/auth/wechat">微信登录</a>
</body>
</html>
ok 微信登陆的代码都写完了 ,等待后续测试