一:Spring Security社交集成原理
二:集成示例
public interface Weixin {
WeixinUserInfo getUserInfo(String openId);
}
@Data
@ToString
public class WeixinUserInfo {
/**
* 普通用户的标识,对当前开发者帐号唯一
*/
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;
}
public class WeixinImpl extends AbstractOAuth2ApiBinding implements Weixin {
public WeixinImpl(String accessToken) {
super(accessToken, TokenStrategy.ACCESS_TOKEN_PARAMETER);
}
/**
* 获取用户信息的url
*/
private static final String URL_GET_USER_INFO = "https://api.weixin.qq.com/sns/userinfo?openid=";
/**
* 默认注册的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 response = getRestTemplate().getForObject(url, String.class);
if (response.contains("errcode")) {
return null;
}
WeixinUserInfo weixinUserInfo = JSONObject.parseObject(response, WeixinUserInfo.class);
return weixinUserInfo;
}
}
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;
}
@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);
}
@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) {
String response = getRestTemplate().getForObject(accessTokenRequestUrl.toString(), String.class);
Map result = JSONObject.parseObject(response, Map.class);
Object errcode = result.get("errcode");
if (errcode != null) {
Object errmsg = result.get("errmsg");
throw new RuntimeException("获取access token失败, errcode:"+errcode+", errmsg:"+errmsg);
}
WeixinAccessGrant accessGrant = new WeixinAccessGrant(
result.get("access_token").toString(),
result.get("scope").toString(),
result.get("refresh_token").toString(),
Long.valueOf(result.get("expires_in").toString())
);
return accessGrant;
}
@Override
public String buildAuthorizeUrl(OAuth2Parameters parameters) {
return buildAuthenticateUrl(parameters);
}
/**
* 构建获取授权码的请求。也就是引导用户跳转到微信的地址。
*/
@Override
public String buildAuthenticateUrl(OAuth2Parameters parameters) {
String url = super.buildAuthenticateUrl(parameters);
url = url + "&appid="+clientId+"&scope=snsapi_login";
return url;
}
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
}
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) {
}
}
public class WeixinServiceProvider extends AbstractOAuth2ServiceProvider<Weixin> {
/**
* 微信获取授权码的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 WeixinServiceProvider(String appId, String appSecret) {
super(new WeixinOAuth2Template(appId, appSecret, URL_AUTHORIZE, URL_ACCESS_TOKEN));
}
@Override
public Weixin getApi(String accessToken) {
return new WeixinImpl(accessToken);
}
}
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 String getOpenId() {
return openId;
}
}
public class WeixinConnectionFactory extends OAuth2ConnectionFactory<Weixin> {
public WeixinConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new WeixinServiceProvider(appId, appSecret), new WeixinAdapter());
}
@Override
protected String extractProviderUserId(AccessGrant accessGrant) {
if (accessGrant instanceof WeixinAccessGrant) {
return ((WeixinAccessGrant)accessGrant).getOpenId();
}
return null;
}
@Override
public Connection<Weixin> createConnection(AccessGrant accessGrant) {
return new OAuth2Connection<Weixin>(getProviderId(), extractProviderUserId(accessGrant), accessGrant.getAccessToken(),
accessGrant.getRefreshToken(), accessGrant.getExpireTime(), getOAuth2ServiceProvider(), getApiAdapter());
}
public OAuth2ServiceProvider<Weixin> getOAuth2ServiceProvider() {
return (OAuth2ServiceProvider<Weixin>) getServiceProvider();
}
protected ApiAdapter<Weixin> getApiAdapter(String providerUserId) {
return new WeixinAdapter(providerUserId);
}
}
@Data
@ConfigurationProperties(prefix = "social.weixin")
public class WeixinProperties {
private String providerId = "weixin";
private String appId;
private String appSecret;
private String signupUrl;
}
application.yml
这里的filterProcessesUrl应该是QQ和微信公用的值,默认为/auth,这里配置的位置不太合适,最好不要配置到某个具体的社交下面。
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root123
social:
qq:
appId: xx
appSecret: xxx
filterProcessesUrl: /qqLogin
providerId: callback.do
signupUrl: /signup
weixin:
appId: xxx
appSecret: xxx
@Configuration
@ConditionalOnProperty(prefix = "social.weixin", name = "appId")
@EnableConfigurationProperties(WeixinProperties.class)
public class WeixinAutoConfiguration extends SocialConfigurerAdapter {
@Autowired
private WeixinProperties weixinProperties;
protected ConnectionFactory<?> createConnectionFactory() {
return new WeixinConnectionFactory(weixinProperties.getProviderId(), weixinProperties.getAppId(), weixinProperties.getAppSecret());
}
@Override
public void addConnectionFactories(ConnectionFactoryConfigurer configurer, Environment environment) {
configurer.addConnectionFactory(this.createConnectionFactory());
}
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
return null;
}
}
login.html
这里的"/qqLogin"就是application.yml中filterProcessesUrl配置的值,"/weixin"就是WeixinProperties中的providerId的默认值。
<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<title>登录</title>
</head>
<body>
<a href="/qqLogin/weixin">微信登录</a>
</body>
</html>
三:配置
具体配置请参考该文章 Spring Security(六):认证(Authentication)-社交登录(Social)-QQ 的最后一部分
GitHub源码地址:https://github.com/mengday/springboot-security-social