重构社交登录
app里面的第三方登录不像浏览器中一样,一般是通过调用sdk(服务提供商),一般有2中模式。
浏览器社交登录流程
简化模式
从流程图分析,我们要提供一个用openId登录后台,这个流程和短信登录很相似,只是openId是从我们的社交用户表(UserConnection)中获取。直接上代码
OpenIdAuthenticationToken
package com.rui.tiger.auth.app.social.openid;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* openId登录
* @author CaiRui
* @date 2019-04-23 08:20
*/
public class OpenIdAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final Object principal;//openId
private String providerId;//供应商ID
// ~ Constructors
// ===================================================================================================
public OpenIdAuthenticationToken(Object principal, String providerId) {
super(null);
this.principal = principal;
this.providerId = providerId;
super.setAuthenticated(true); // must use super, as we override
}
public OpenIdAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// ========================================================================================================
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public Object getCredentials() {
return null;
}
public String getProviderId() {
return providerId;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
过滤器 OpenIdAuthenticationFilter
package com.rui.tiger.auth.app.social.openid;
import com.rui.tiger.auth.core.properties.QQProperties;
import com.rui.tiger.auth.core.properties.SecurityConstants;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 逻辑通短信登录验证
* @author CaiRui
* @date 2019-04-23 08:23
*/
public class OpenIdAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String openIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_OPENID;
// 服务提供商id,qq还是微信
/**
* @see QQProperties#providerId
*/
private String providerIdParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_PROVIDERID;
private boolean postOnly = true;
public OpenIdAuthenticationFilter() {
super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_OPENID, "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String openId = obtainOpenId(request);
String providerId = obtainProviderId(request);
if (openId == null) {
openId = "";
}
if (providerId == null) {
providerId = "";
}
openId = openId.trim();
OpenIdAuthenticationToken authRequest = new OpenIdAuthenticationToken(openId, providerId);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected String obtainOpenId(HttpServletRequest request) {
return request.getParameter(openIdParameter);
}
private String obtainProviderId(HttpServletRequest request) {
return request.getParameter(providerIdParameter);
}
protected void setDetails(HttpServletRequest request,
OpenIdAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setOpenIdParameter(String openIdParameter) {
Assert.hasText(openIdParameter, "Username parameter must not be empty or null");
this.openIdParameter = openIdParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getOpenIdParameter() {
return openIdParameter;
}
}
常量字典新增 SecurityConstants
供应商 OpenIdAuthenticationProvider
package com.rui.tiger.auth.app.social.openid;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.security.SocialUserDetailsService;
import java.util.HashSet;
import java.util.Set;
/**
* 验证OpenIdAuthenticationToken
* 查询社交数据库表UserConnection
* @author CaiRui
* @date 2019-04-23 08:30
*/
public class OpenIdAuthenticationProvider implements AuthenticationProvider {
private SocialUserDetailsService userDetailsService;
private UsersConnectionRepository usersConnectionRepository;
/*
* (non-Javadoc)
*
* @see org.springframework.security.authentication.AuthenticationProvider#
* authenticate(org.springframework.security.core.Authentication)
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;
Set<String> providerUserIds = new HashSet<>();
providerUserIds.add((String) authenticationToken.getPrincipal());//openId
//UserConnection 这个表里查询信息
Set<String> userIds = usersConnectionRepository.findUserIdsConnectedTo(authenticationToken.getProviderId(), providerUserIds);
if(CollectionUtils.isEmpty(userIds) || userIds.size() != 1) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
String userId = userIds.iterator().next();
UserDetails user = userDetailsService.loadUserByUserId(userId);
if (user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
/*
* (non-Javadoc)
*
* @see org.springframework.security.authentication.AuthenticationProvider#
* supports(java.lang.Class)
*/
@Override
public boolean supports(Class<?> authentication) {
return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
}
public SocialUserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(SocialUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public UsersConnectionRepository getUsersConnectionRepository() {
return usersConnectionRepository;
}
public void setUsersConnectionRepository(UsersConnectionRepository usersConnectionRepository) {
this.usersConnectionRepository = usersConnectionRepository;
}
}
配置类
package com.rui.tiger.auth.app.social.openid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.security.SocialUserDetailsService;
import org.springframework.stereotype.Component;
/**
* openId 权限配置类
* @author CaiRui
* @date 2019-04-23 08:39
*/
@Component
public class OpenIdAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private SocialUserDetailsService userDetailsService;
@Autowired
private UsersConnectionRepository usersConnectionRepository;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
OpenIdAuthenticationProvider provider = new OpenIdAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setUsersConnectionRepository(usersConnectionRepository);
OpenIdAuthenticationFilter filter = new OpenIdAuthenticationFilter();
filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
//密码登录后置过滤
http.
authenticationProvider(provider)
.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
}
}
资源服务器配置openid登录 com.rui.tiger.auth.app.TigerResourceServerConfig#configure
ok 下面我们来测试下
社交登录库中查看社交用户细信息
postman发送测试,同样请求头中添加app相关信息
授权码模式
此模式下,只需要app端把拿到的授权码转发给服务器,再由服务器拿授权码去换令牌,获取成功后不是跳转到首页而是我们自定义的成功处理器,然后走oath2的 token生成逻辑返回我们系统自定义的token;
成功处理器接口及实现
package com.rui.tiger.auth.core.social;
import org.springframework.social.security.SocialAuthenticationFilter;
/**
* @author CaiRui
* @date 2019-04-24 16:01
*/
public interface SocialAuthenticationFilterPostProcessor {
void process(SocialAuthenticationFilter socialAuthenticationFilter);
}
实现
package com.rui.tiger.auth.app.social.impl;
import com.rui.tiger.auth.core.social.SocialAuthenticationFilterPostProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* @author CaiRui
* @date 2019-04-24 16:11
*/
@Component
public class AppSocialAuthenticationFilterPostProcessor implements SocialAuthenticationFilterPostProcessor {
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
@Override
public void process(SocialAuthenticationFilter socialAuthenticationFilter) {
// 这里设置的其实就是之前 重构用户名密码登录里面实现的 tigerAuthenticationSuccessHandler
//这样就可以返回token
socialAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
}
}
社交配置类进行修改
package com.rui.tiger.auth.core.social;
import org.springframework.social.security.SocialAuthenticationFilter;
import org.springframework.social.security.SpringSocialConfigurer;
/**
* 自定义SpringSocialConfigurer 用于覆盖默认的社交登陆拦截请求
*
* @author CaiRui
* @Date 2019/1/5 16:15
*/
public class TigerSpringSocialConfigurer extends SpringSocialConfigurer {
private String filterProcessesUrl;//覆盖默认的/auth 拦截路径
private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;
public TigerSpringSocialConfigurer(String filterProcessesUrl) {
this.filterProcessesUrl = filterProcessesUrl;
}
@Override
protected <T> T postProcess(T object) {
SocialAuthenticationFilter socialAuthenticationFilter = (SocialAuthenticationFilter) super.postProcess(object);
socialAuthenticationFilter.setFilterProcessesUrl(filterProcessesUrl);
if (socialAuthenticationFilterPostProcessor!=null){
socialAuthenticationFilterPostProcessor.process(socialAuthenticationFilter);
}
return (T) socialAuthenticationFilter;
}
public SocialAuthenticationFilterPostProcessor getSocialAuthenticationFilterPostProcessor() {
return socialAuthenticationFilterPostProcessor;
}
public void setSocialAuthenticationFilterPostProcessor(SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor) {
this.socialAuthenticationFilterPostProcessor = socialAuthenticationFilterPostProcessor;
}
}
注入
package com.rui.tiger.auth.core.social;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.social.UserIdSource;
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.ConnectionRepository;
import org.springframework.social.connect.ConnectionSignUp;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.social.connect.jdbc.JdbcUsersConnectionRepository;
import org.springframework.social.connect.web.ConnectController;
import org.springframework.social.connect.web.ProviderSignInUtils;
import org.springframework.social.security.AuthenticationNameUserIdSource;
import org.springframework.social.security.SpringSocialConfigurer;
import javax.sql.DataSource;
/**
* 社交配置类
*
* @author CaiRui
* @Date 2019/1/5 11:46
*/
@Configuration
@EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
@Autowired
private DataSource dataSource;//数据源
@Autowired
private SecurityProperties securityProperties;
//第三方登录直接注册用户 可以不实现 跳到注册界面
@Autowired(required = false)
private ConnectionSignUp connectionSignUp;
//浏览器项目不用实现这个接口
@Autowired(required = false)
private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;
/**
* 默认配置类 包括了过滤器SocialAuthenticationFilter 添加到security过滤链中
*
* @return
*/
@Bean
public SpringSocialConfigurer tigerSpringSocialConfigurer() {
TigerSpringSocialConfigurer tigerSpringSocialConfigurer = new TigerSpringSocialConfigurer(
securityProperties.getSocial().getFilterProcessesUrl());
//配置自己的注册界面
tigerSpringSocialConfigurer.signupUrl(securityProperties.getBrowser().getSignupUrl());
tigerSpringSocialConfigurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
return tigerSpringSocialConfigurer;
}
/**
* 业务系统用户和服务提供商用户对应关系,保存在表UserConnection
* JdbcUsersConnectionRepository.sql 中有建表语句
* userId 业务系统Id
* providerId 服务提供商的Id
* providerUserId 同openId
* Encryptors 加密策略 这里不加密
*
* @param connectionFactoryLocator
* @return
*/
@Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
JdbcUsersConnectionRepository jdbcUsersConnectionRepository = new JdbcUsersConnectionRepository(dataSource, connectionFactoryLocator, Encryptors.noOpText());
//设定表UserConnection的前缀 表名不可以改变
//jdbcUsersConnectionRepository.setTablePrefix("tiger_");
if(connectionSignUp!=null){
jdbcUsersConnectionRepository.setConnectionSignUp(connectionSignUp);
}
return jdbcUsersConnectionRepository;
}
/**
* 从认证中获取用户信息
*
* @return
*/
@Override
public UserIdSource getUserIdSource() {
return new AuthenticationNameUserIdSource();
}
/**
* social和注册互动工具类
* @return
*/
@Bean
public ProviderSignInUtils providerSignInUtils(ConnectionFactoryLocator connectionFactoryLocator){
return new ProviderSignInUtils(connectionFactoryLocator,getUsersConnectionRepository(connectionFactoryLocator));
}
//https://docs.spring.io/spring-social/docs/1.1.x-SNAPSHOT/reference/htmlsingle/#creating-connections-with-connectcontroller
//社交账号绑定和解绑处理帮助类
@Bean
public ConnectController connectController(
ConnectionFactoryLocator connectionFactoryLocator,
ConnectionRepository connectionRepository) {
return new ConnectController(connectionFactoryLocator, connectionRepository);
}
}
ok 开启我们的测试验证
测试步骤:
1. demo项目引用浏览器项目,做qq授权登录,拿到授权码(断点调试关闭服务器还是执行,这里我们先注释这段请求token的代码)
org.springframework.social.security.provider.OAuth2AuthenticationService#getAuthToken
com.rui.tiger.auth.core.social.qq.connect.QQOAuth2Template#postForAccessGrant
授权码:D9463227AB66B2D7517BA2616537324E
state:d2436704-33d3-4933-a2b8-67da5253f3a1
2.关闭服务器,再把demo切换到app项目,注意要把上面注释的请求token的代码放开
3.postman带上授权码和client信息发送请求token信息
ok 成功返回我们自己的token信息
下章我们重构社交注册逻辑