源码分析
因为spring security social是基于SocialAuthenticationFilter实现的,所以咱们从SocialAuthenticationFilter入手开始分析
一说到Filter,必定有一个对应的配置类
SocialAuthenticationFilter也不例外,它对应的配置类就是SpringSocialConfigurer,为了让项目运行起来,咱们先把配置准备好:
SocialConfig
core项目com.spring.security.social路径
package com.spring.security.social;
import com.spring.security.properties.SecurityProperties;
import com.spring.security.social.qq.connet.QQConnectionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.UserIdSource;
import org.springframework.social.config.annotation.ConnectionFactoryConfigurer;
import org.springframework.social.config.annotation.EnableSocial;
import org.springframework.social.config.annotation.SocialConfigurerAdapter;
import org.springframework.social.connect.ConnectionFactoryLocator;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.security.AuthenticationNameUserIdSource;
import org.springframework.social.security.SpringSocialConfigurer;
import javax.sql.DataSource;
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private SecurityProperties securityProperties;
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository repository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
// 指定表前缀,后缀是固定的,在JdbcUsersConnectionRepository所在位置
repository.setTablePrefix("sys_");
return repository;
}
@Bean
public SpringSocialConfigurer hkSocialSecurityConfig(){
// 默认配置类,进行组件的组装
// 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
//自定义登录路径
String filterProcessesUrl = "/auth";
HkSpringSocialConfigurer configurer = new HkSpringSocialConfigurer(filterProcessesUrl);
return configurer;
}
// 添加关于ProviderId=qq的处理器。注意clientId和clientSecret需要自己去qq互联申请
@Override
public void addConnectionFactories(
ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
connectionFactoryConfigurer.addConnectionFactory(
new QQConnectionFactory("qq", "clientId", "clientSecret"));
}
/**
* 必须要创建一个UserIdSource,否则会报错
*
* @return
*/
@Override
public UserIdSource getUserIdSource() {
return new AuthenticationNameUserIdSource();
}
}
HkSpringSocialConfigurer
同上目录创建
package com.spring.security.social;
import lombok.Data;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;
/**
* 设置 登录 注册地址
*/
@Data
public class HkSpringSocialConfigurer extends SpringSocialConfigurer {
private String filterProcessesUrl;
public HkSpringSocialConfigurer(String filterProcessesUrl){
this.filterProcessesUrl = filterProcessesUrl;
}
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter filter = (SocialAuthenticationFilter) super.postProcess(object);
filter.setFilterProcessesUrl(filterProcessesUrl);
return (T) filter;
}
}
数据库中创建以下数据表:
create table sys_UserConnection (userId varchar(255) not null,
providerId varchar(255) not null,
providerUserId varchar(255),
rank int not null,
displayName varchar(255),
profileUrl varchar(512),
imageUrl varchar(512),
accessToken varchar(512) not null,
secret varchar(512),
refreshToken varchar(512),
expireTime bigint,
primary key (userId, providerId, providerUserId));
create unique index UserConnectionRank on UserConnection(userId, providerId, rank);
加入认证模块
@Autowired
private SpringSocialConfigurer hkSocialSecurityConfig;
.and()
//加入社交登录
.apply(hkSocialSecurityConfig)
以上两个配置,就完全可以让程序运行起来了。
访问/auth/qq,进入SocialAuthenticationFilter中
private static final String DEFAULT_FILTER_PROCESSES_URL = "/auth";
private String filterProcessesUrl = DEFAULT_FILTER_PROCESSES_URL;
// 拦截符合要求的请求
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
//假如请求的url是/auth/qq,那么ProviderId提取出来就是qq
String providerId = getRequestedProviderId(request);
if (providerId != null){
Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
// 检查是否存在处理ProviderId=qq的SocialAuthenticationService
return authProviders.contains(providerId);
}
return false;
}
//分析请求的url,从url中提取ProviderId。
//假如请求的url是/auth/qq,那么ProviderId提取出来就是qq
private String getRequestedProviderId(HttpServletRequest request) {
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';');
if (pathParamIndex > 0) {
// strip everything after the first semi-colon
uri = uri.substring(0, pathParamIndex);
}
// uri must start with context path
uri = uri.substring(request.getContextPath().length());
// remaining uri must start with filterProcessesUrl
if (!uri.startsWith(filterProcessesUrl)) {
return null;
}
uri = uri.substring(filterProcessesUrl.length());
// expect /filterprocessesurl/provider, not /filterprocessesurlproviderr
if (uri.startsWith("/")) {
return uri.substring(1);
} else {
return null;
}
}
public void addAuthenticationService(SocialAuthenticationService<?> authenticationService) {
addConnectionFactory(authenticationService.getConnectionFactory());
authenticationServices.put(authenticationService.getConnectionFactory().getProviderId(), authenticationService);
}
@Bean
public ConnectionFactoryLocator connectionFactoryLocator() {
if (securityEnabled) {
SecurityEnabledConnectionFactoryConfigurer cfConfig = new SecurityEnabledConnectionFactoryConfigurer();
for (SocialConfigurer socialConfigurer : socialConfigurers) {
socialConfigurer.addConnectionFactories(cfConfig, environment);
}
return cfConfig.getConnectionFactoryLocator();
} else {
DefaultConnectionFactoryConfigurer cfConfig = new DefaultConnectionFactoryConfigurer();
for (SocialConfigurer socialConfigurer : socialConfigurers) {
socialConfigurer.addConnectionFactories(cfConfig, environment);
}
return cfConfig.getConnectionFactoryLocator();
}
}
最终,通过对上面代码的分析,我们可以重写SocialConfigurerAdapter的addConnectionFactories方法,也就是咱们上面的SocialConfig类:
// 添加关于ProviderId=qq的处理器。注意clientId和clientSecret需要自己去qq互联申请
@Override
public void addConnectionFactories(
ConnectionFactoryConfigurer connectionFactoryConfigurer, Environment environment) {
connectionFactoryConfigurer.addConnectionFactory(
new QQConnectionFactory("qq", "clientId", "clientSecret"));
}
如果出现addConnectionFactories无法重写把spring-cloud-starter-oauth2换成最新版本
QQConnectionFactory
core项目com.spring.security.social.qq.connet路径创建QQConnectionFactory类继承OAuth2ConnectionFactory
package com.spring.security.social.qq.connet;
import com.spring.security.social.qq.api.QQ;
import org.springframework.social.connect.support.OAuth2ConnectionFactory;
public class QQConnectionFactory extends OAuth2ConnectionFactory<QQ> {
public QQConnectionFactory(String providerId, String appId, String appSecret) {
super(providerId, new QQServiceProvider(appId, appSecret), new QQAdapter());
}
}
OAuth2ConnectionFactory泛型是读取用户资料的接口,报错先不用管最后再做接口
继续跟进/auth/qq这个请求,当我们添加了一个针对qq的ConnectionFactory后,请求将向后执行至:
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (detectRejection(request)) {
if (logger.isDebugEnabled()) {
logger.debug("A rejection was detected. Failing authentication.");
}
throw new SocialAuthenticationException("Authentication failed because user rejected authorization.");
}
Authentication auth = null;
Set<String> authProviders = authServiceLocator.registeredAuthenticationProviderIds();
//获取/auth/qq中的ProviderId=qq
String authProviderId = getRequestedProviderId(request);
if (!authProviders.isEmpty() && authProviderId != null && authProviders.contains(authProviderId)) {
// 根据ProviderId=qq获取SocialAuthenticationService
SocialAuthenticationService<?> authService = authServiceLocator.getAuthenticationService(authProviderId);
// 通过SocialAuthenticationService获取Authentication
auth = attemptAuthService(authService, request, response);
if (auth == null) {
throw new AuthenticationServiceException("authentication failed");
}
}
return auth;
}
private Authentication attemptAuthService(final SocialAuthenticationService<?> authService, final HttpServletRequest request, HttpServletResponse response)
throws SocialAuthenticationRedirectException, AuthenticationException {
// 通过SocialAuthenticationService获取token
final SocialAuthenticationToken token = authService.getAuthToken(request, response);
if (token == null) return null;
Assert.notNull(token.getConnection());
// 获取SecurityContext中的Authentication,未登录的话就是null
Authentication auth = getAuthentication();
if (auth == null || !auth.isAuthenticated()) {
// 将获取到的第三方用户信息做一个更新,并返回Authentication
return doAuthentication(authService, request, token);
} else {
// 如果不是null,会检查数据库中的第三方用户信息表单,不存在就会添加进表单
addConnection(authService, request, token, auth);
return null;
}
}
跟着代码进final SocialAuthenticationToken token = authService.getAuthToken(request, response);也就是OAuth2AuthenticationService
public SocialAuthenticationToken getAuthToken(HttpServletRequest request, HttpServletResponse response) throws SocialAuthenticationRedirectException {
// 获取请求参数code
String code = request.getParameter("code");
// 如果没有拿到code,说明不是回调请求,根据oauth2.0协议,就要创建一个请求进入第三方登录页面
if (!StringUtils.hasText(code)) {
//准备请求参数
OAuth2Parameters params = new OAuth2Parameters();
params.setRedirectUri(buildReturnToUrl(request));
setScope(request, params);
params.add("state", generateState(connectionFactory, request));
addCustomParameters(params);
// 抛出异常,准备跳转至第三方登录页面
throw new SocialAuthenticationRedirectException(getConnectionFactory().getOAuthOperations().buildAuthenticateUrl(params));
} else if (StringUtils.hasText(code)) {
// 如果code不为空,那么意味着就是第三方回调请求,那么就需要截取code值去请求accessToken
try {
// 回调url,要保持和之前的回调url一致
String returnToUrl = buildReturnToUrl(request);
// 重点在于这里,这里是获取accessToken
AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
// 通过ApiAdapter设置ConnectionValues
Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
return new SocialAuthenticationToken(connection, null);
} catch (RestClientException e) {
logger.debug("failed to exchange for access", e);
return null;
}
} else {
return null;
}
}
AccessGrant accessGrant = getConnectionFactory().getOAuthOperations().exchangeForAccess(code, returnToUrl, null);
QQOAuth2Template
core项目com.spring.security.social.qq.connet路径创建QQOAuth2Template类
package com.spring.security.social.qq.connet;
import org.apache.commons.lang.StringUtils;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.social.oauth2.AccessGrant;
import org.springframework.social.oauth2.OAuth2Template;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.Charset;
/**
* QQ自定义Template 解析
*/
public class QQOAuth2Template extends OAuth2Template {
public QQOAuth2Template(String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl) {
super(clientId, clientSecret, authorizeUrl, accessTokenUrl);
// 设置成true才会将clientId和clientSecret放到请求参数中
setUseParametersForClientAuthentication(true);
}
/**
* 自定义发送请求并解析
*
* @param accessTokenUrl
* @param parameters
* @return
*/
@Override
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
String result = getRestTemplate().postForObject(accessTokenUrl, parameters, String.class);
System.out.println("响应数据:" + result);
return createAccessGrant(result);
}
/**
* 根据返回的结构创建AccessGrant 成功返回,即可在返回包中获取到Access Token。 如:
* access_token=FE04************************CCE2&expires_in=7776000&refresh_token=88E4************************BE14
*
* @param responseResult
* @return
*/
private AccessGrant createAccessGrant(String responseResult) {
String[] items = StringUtils.splitByWholeSeparatorPreserveAllTokens(responseResult, "&");
String access_token = StringUtils.substringAfterLast(items[0], "=");
String expires_in = StringUtils.substringAfterLast(items[1], "=");
String refresh_token = StringUtils.substringAfterLast(items[2], "=");
return new AccessGrant(
access_token, StringUtils.EMPTY, refresh_token, Long.valueOf(expires_in));
}
@Override
protected RestTemplate createRestTemplate() {
RestTemplate restTemplate = super.createRestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}
}
QQServiceProvider
core项目com.spring.security.social.qq.connet创建QQServiceProvider
package com.spring.security.social.qq.connet;
import com.spring.security.social.qq.api.QQ;
import com.spring.security.social.qq.api.QQImpl;
import org.springframework.social.oauth2.AbstractOAuth2ServiceProvider;
public class QQServiceProvider extends AbstractOAuth2ServiceProvider<QQ> {
private String appId;
//将用户导向到认证服务器的url 获取Authorization Code
private static final String URL_AUTHORIZE="https://graph.qq.com/oauth2.0/authorize";
//认证服务器返回授权码的url 通过Authorization Code获取Access Token
private static final String URL_ACCESSTOKEN="https://graph.qq.com/oauth2.0/token";
public QQServiceProvider(String appId,String appSecret) {
//String clientId, String clientSecret, String authorizeUrl, String accessTokenUrl
super(new QQOAuth2Template(appId,appSecret,URL_AUTHORIZE,URL_ACCESSTOKEN));
this.appId = appId;
}
@Override
public QQ getApi(String accessToken) {
return new QQImpl(accessToken, appId);
}
}
后续通过AcessGrant设置Connection
// 通过ApiAdapter设置ConnectionValues
Connection<S> connection = getConnectionFactory().createConnection(accessGrant);
也就是调用下面的setConnectionValues方法
QQAdapter
core项目com.spring.security.social.qq.connet创建QQAdapter
package com.spring.security.social.qq.connet;
import com.spring.security.social.qq.api.QQ;
import com.spring.security.social.qq.api.QQUserInfo;
import org.springframework.social.connect.ApiAdapter;
import org.springframework.social.connect.ConnectionValues;
import org.springframework.social.connect.UserProfile;
public class QQAdapter implements ApiAdapter<QQ> {
/**
* 测试QQ服务端是否可用
* @param qq
* @return
*/
@Override
public boolean test(QQ qq) {
return true;
}
@Override
public void setConnectionValues(QQ api, ConnectionValues values) {
QQUserInfo userInfo = api.getUserInfo();
//用户名称
values.setDisplayName(userInfo.getNickname());
//用户头像
values.setImageUrl(userInfo.getFigureurl_qq_1());
//个人主页
values.setProfileUrl(null);
//服务商用户ID openId
values.setProviderUserId(userInfo.getOpenId());
}
@Override
public UserProfile fetchUserProfile(QQ qq) {
return null;
}
@Override
public void updateStatus(QQ qq, String s) {
}
}
在拿到token后,回到SocialAuthenticationFilter.attemptAuthService方法中的doAuthentication操作
private Authentication doAuthentication(SocialAuthenticationService<?> authService, HttpServletRequest request, SocialAuthenticationToken token) {
try {
if (!authService.getConnectionCardinality().isAuthenticatePossible()) return null;
token.setDetails(authenticationDetailsSource.buildDetails(request));
// 根据数据库中是否存在当前第三方用户信息
// 不存在就抛异常并跳转进注册页面
Authentication success = getAuthenticationManager().authenticate(token);
Assert.isInstanceOf(SocialUserDetails.class, success.getPrincipal(), "unexpected principle type");
// 假如已经存在这个用户,就更新数据
updateConnections(authService, token, success);
return success;
} catch (BadCredentialsException e) {
// 如果是需要注册的用户,就跳注册页面
if (signupUrl != null) {
// 这里会把ConnectionData存储进session,可以通过ProviderSignInUtils获取
sessionStrategy.setAttribute(new ServletWebRequest(request), ProviderSignInAttempt.SESSION_ATTRIBUTE, new ProviderSignInAttempt(token.getConnection()));
throw new SocialAuthenticationRedirectException(buildSignupUrl(request));
}
throw e;
}
}
注意这里:
Authentication success = getAuthenticationManager().authenticate(token);
这段代码会先跳转至ProviderManager.authenticate,再进入SocialAuthenticationProvider.authenticate
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(SocialAuthenticationToken.class, authentication, "unsupported authentication type");
Assert.isTrue(!authentication.isAuthenticated(), "already authenticated");
SocialAuthenticationToken authToken = (SocialAuthenticationToken) authentication;
String providerId = authToken.getProviderId();
Connection<?> connection = authToken.getConnection();
// 查询数据库是否存在当前第三方数据,不存在就抛异常
// 通过JdbcUsersConnectionRepository查询
String userId = toUserId(connection);
if (userId == null) {
throw new BadCredentialsException("Unknown access token");
}
UserDetails userDetails = userDetailsService.loadUserByUserId(userId);
if (userDetails == null) {
throw new UsernameNotFoundException("Unknown connected account id");
}
return new SocialAuthenticationToken(connection, userDetails, authToken.getProviderAccountData(), getAuthorities(providerId, userDetails));
}
JdbcUsersConnectionRepository源码
public List<String> findUserIdsWithConnection(Connection<?> connection) {
ConnectionKey key = connection.getKey();
List<String> localUserIds = jdbcTemplate.queryForList("select userId from " + tablePrefix + "UserConnection where providerId = ? and providerUserId = ?", String.class, key.getProviderId(), key.getProviderUserId());
// 根据代码逻辑,发现假如设置了connectionSignUp,那么就会自动创建一个newUserId,就不会抛异常,也就不会进入注册页面
if (localUserIds.size() == 0 && connectionSignUp != null) {
String newUserId = connectionSignUp.execute(connection);
if (newUserId != null)
{
createConnectionRepository(newUserId).addConnection(connection);
return Arrays.asList(newUserId);
}
}
return localUserIds;
}
到这里,根据源码分析第三方登录基本告一段落,剩下的几个读取用户资料类分别是
QQ
core项目om.spring.security.social.qq.api路径创建QQ接口
package com.spring.security.social.qq.api;
/**
* 获取用户信息
*/
public interface QQ {
QQUserInfo getUserInfo();
}
QQImpl
同上面目录
package com.spring.security.social.qq.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang.StringUtils;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.TokenStrategy;
public class QQImpl extends AbstractOAuth2ApiBinding implements QQ {
//应用ID
private String appId;
//用户OpenID
private String openId;
//获取用户openId的URL
private static final String URL_GET_OPENID = "https://graph.qq.com/oauth2.0/me?access_token=%s";
//获取用户信息url
private static final String URL_GET_USERINFO = "https://graph.qq.com/user/get_user_info?oauth_consumer_key=%s&openid=%s";
private ObjectMapper objectMapper = new ObjectMapper();
/**
* 获取openid
* 响应: callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
*
* @param accessToken
* @param appId
*/
public QQImpl(String accessToken, String appId) {
super(accessToken, TokenStrategy.AUTHORIZATION_HEADER);
this.appId = appId;
// 之所以需要获取openId,是因为需要通过openId获取用户信息
this.openId = requestOpenId(accessToken);
}
/**
* 获取openId
*
* <p>响应: callback( {"client_id":"YOUR_APPID","openid":"YOUR_OPENID"} );
*
* @param accessToken
* @return
*/
private String requestOpenId(String accessToken) {
String url = String.format(URL_GET_OPENID, accessToken);
String result = getRestTemplate().getForObject(url, String.class);
System.out.println("获取openId:" + result);
String first = StringUtils.substringBeforeLast(result, "\"");
return StringUtils.substringAfterLast(first, "\"");
}
/**
* 获取用户信息
*
* <p>https://wiki.connect.qq.com/get_user_info
*
* @return
*/
@Override
public QQUserInfo getUserInfo() {
//发送请求
String url = String.format(URL_GET_USERINFO, appId, openId);
String result = getRestTemplate().getForObject(url, String.class);
System.out.println("获取用户资料:" + result);
try {
QQUserInfo qqUserInfo = objectMapper.readValue(result, QQUserInfo.class);
qqUserInfo.setOpenId(openId);
return qqUserInfo;
} catch (Exception e) {
throw new RuntimeException("获取用户信息失败");
}
}
}
QQUserInfo
同上面目录
参数参考:https://wiki.connect.qq.com/get_user_info
import lombok.Getter;
import lombok.Setter;
/** qq用户信息 */
@Getter
@Setter
public class QQUserInfo {
/** 返回码 */
private String ret;
/** 如果ret<0,会有相应的错误信息提示,返回数据全部用UTF-8编码。 */
private String msg;
/** */
private String openId;
/** 不知道什么东西,文档上没写,但是实际api返回里有。 */
private String is_lost;
/** 省(直辖市) */
private String province;
/** 市(直辖市区) */
private String city;
/** 出生年月 */
private String year;
/** 用户在QQ空间的昵称。 */
private String nickname;
/** 大小为30×30像素的QQ空间头像URL。 */
private String figureurl;
/** 大小为50×50像素的QQ空间头像URL。 */
private String figureurl_1;
/** 大小为100×100像素的QQ空间头像URL。 */
private String figureurl_2;
private String figureurl_type;
private String figureurl_qq;
/** 大小为40×40像素的QQ头像URL。 */
private String figureurl_qq_1;
/** 大小为100×100像素的QQ头像URL。需要注意,不是所有的用户都拥有QQ的100×100的头像,但40×40像素则是一定会有。 */
private String figureurl_qq_2;
/** 性别。 如果获取不到则默认返回”男” */
private String gender;
/** 标识用户是否为黄钻用户(0:不是;1:是)。 */
private String is_yellow_vip;
/** 标识用户是否为黄钻用户(0:不是;1:是) */
private String vip;
/** 黄钻等级 */
private String yellow_vip_level;
/** 黄钻等级 */
private String level;
/** 标识是否为年费黄钻用户(0:不是; 1:是) */
private String is_yellow_year_vip;
private String constellation;
}
来源:oschina
链接:https://my.oschina.net/u/1046143/blog/3190125