SpringSecurityOAuth2集成短信验证码登录以及第三⽅登
录
前⾔
基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决⽅案来⽀持在Spring Cloud/Spring Boot环境下使⽤OAuth2.0,提供了开箱即⽤的组件。但是在开发过程中我们会发现由于Spring Security OAuth2的组件特别全⾯,这样就导致了扩展很不⽅便或者说是不太容易直指定扩展的⽅案,例如:
1. 图⽚验证码登录
2. 短信验证码登录
3. ⼩程序登录
4. 第三⽅系统登录
5. CAS单点登录
在⾯对这些场景的时候,预计很多对Spring Security OAuth2不熟悉的⼈恐怕会⽆从下⼿。基于上述的场景要求,如何优雅的集成短信验证码登录及第三⽅登录,怎么样才算是优雅集成呢?有以下要求:
1. 不侵⼊Spring Security OAuth2的原有代码
2. 对于不同的登录⽅式不扩展新的端点,使⽤/oauth/token可以适配所有的登录⽅式
3. 可以对所有登录⽅式进⾏兼容,抽象⼀套模型只要简单的开发就可以集成登录
基于上述的设计要求,接下来将会在⽂章种详细介绍如何开发⼀套集成登录认证组件开满⾜上述要求。
阅读本篇⽂章您需要了解OAuth2.0认证体系、SpringBoot、SpringSecurity以及Spring Cloud等相关知识
思路
我们来看下Spring Security OAuth2的认证流程:
这个流程当中,切⼊点不多,集成登录的思路如下:
1. 在进⼊流程之前先进⾏拦截,设置集成认证的类型,例如:短信验证码、图⽚验证码等信息。
2. 在拦截的通知进⾏预处理,预处理的场景有很多,⽐如验证短信验证码是否匹配、图⽚验证码是否匹配、是否是登录IP
⽩名单等处理
3. 在UserDetailService.loadUserByUsername⽅法中,根据之前设置的集成认证类型去获取⽤户信息,例如:通过⼿机号
码获取⽤户、通过⼩程序OPENID获取⽤户等等
接⼊这个流程之后,基本上就可以优雅集成第三⽅登录。
实现
介绍完思路之后,下⾯通过代码来展⽰如何实现:
第⼀步,定义拦截登录的请求
/**
* @author LIQIU
* @date 2018-3-30
**/
@Component
public class IntegrationAuthenticationFilter extends GenericFilterBean implements ApplicationContextAware {
private static final String AUTH_TYPE_PARM_NAME = "auth_type";
private static final String OAUTH_TOKEN_URL = "/oauth/token";
private Collection<IntegrationAuthenticator> authenticators;
private ApplicationContext applicationContext;
private RequestMatcher requestMatcher;
public IntegrationAuthenticationFilter(){
new AntPathRequestMatcher(OAUTH_TOKEN_URL, "GET"),
new AntPathRequestMatcher(OAUTH_TOKEN_URL, "POST")
);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if(requestMatcher.matches(request)){
//设置集成登录信息
IntegrationAuthentication integrationAuthentication = new IntegrationAuthentication();
integrationAuthentication.Parameter(AUTH_TYPE_PARM_NAME));
integrationAuthentication.ParameterMap());
IntegrationAuthenticationContext.set(integrationAuthentication);
try{
//预处理
this.prepare(integrationAuthentication);
filterChain.doFilter(request,response);
//后置处理
thisplete(integrationAuthentication);
}finally {
IntegrationAuthenticationContext.clear();
}
}else{
filterChain.doFilter(request,response);
}
}
/**
* 进⾏预处理
* @param integrationAuthentication
*/
private void prepare(IntegrationAuthentication integrationAuthentication) {
/
/延迟加载认证器
if(this.authenticators == null){
synchronized (this){
Map<String,IntegrationAuthenticator> integrationAuthenticatorMap = BeansOfType(IntegrationAuthenticator.class);
if(integrationAuthenticatorMap != null){
this.authenticators = integrationAuthenticatorMap.values();
}
}
}
if(this.authenticators == null){
this.authenticators = new ArrayList<>();
}
for (IntegrationAuthenticator authenticator: authenticators) {
if(authenticator.support(integrationAuthentication)){
authenticator.prepare(integrationAuthentication);
}
}
}
/**
* 后置处理
* @param integrationAuthentication
*/
private void complete(IntegrationAuthentication integrationAuthentication){
for (IntegrationAuthenticator authenticator: authenticators) {
if(authenticator.support(integrationAuthentication)){
authenticatorplete(integrationAuthentication);
}
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
在这个类种主要完成2部分⼯作:1、根据参数获取当前的是认证类型,2、根据不同的认证类型调⽤不同的IntegrationAuthenticator.prepar进⾏预处理
第⼆步,将放⼊到拦截链条中
/**
* @author LIQIU
* @date 2018-3-7
**/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private IntegrationUserDetailsService integrationUserDetailsService;
@Autowired
private WebResponseExceptionTranslator webResponseExceptionTranslator;
@Autowired
private IntegrationAuthenticationFilter integrationAuthenticationFilter;
@Autowired
private DatabaseCachableClientDetailsService redisClientDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// TODO persist clients details
clients.withClientDetails(redisClientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(new RedisTokenStore(redisConnectionFactory))
// .accessTokenConverter(jwtAccessTokenConverter())
.authenticationManager(authenticationManager)
.
exceptionTranslator(webResponseExceptionTranslator)
.reuseRefreshTokens(false)
.userDetailsService(integrationUserDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients()
.tokenKeyAccess("isAuthenticated()")
.checkTokenAccess("permitAll()")
.addTokenEndpointAuthenticationFilter(integrationAuthenticationFilter);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("cola-cloud");
return jwtAccessTokenConverter;
}
}
通过调⽤security. .addTokenEndpointAuthenticationFilter(integrationAuthenticationFilter);⽅法,将放⼊到认证链条中。
第三步,根据认证类型来处理⽤户信息
@Service
public class IntegrationUserDetailsService implements UserDetailsService {
@Autowired
private UpmClient upmClient;
private List<IntegrationAuthenticator> authenticators;
@Autowired(required = false)
public void setIntegrationAuthenticators(List<IntegrationAuthenticator> authenticators) {
this.authenticators = authenticators;
}
@Override
public User loadUserByUsername(String username) throws UsernameNotFoundException {
IntegrationAuthentication integrationAuthentication = ();
//判断是否是集成登录
if (integrationAuthentication == null) {
integrationAuthentication = new IntegrationAuthentication();
}
integrationAuthentication.setUsername(username);
UserVO userVO = this.authenticate(integrationAuthentication);
if(userVO == null){
throw new UsernameNotFoundException("⽤户名或密码错误");
}
User user = new User();
this.setAuthorize(user);
return user;
}
/**
* 设置授权信息
*
* @param user
*/
public void setAuthorize(User user) {
Authorize authorize = Id());
user.Roles());
user.Resources());
}
private UserVO authenticate(IntegrationAuthentication integrationAuthentication) {
if (this.authenticators != null) {
for (IntegrationAuthenticator authenticator : authenticators) {
if (authenticator.support(integrationAuthentication)) {
return authenticator.authenticate(integrationAuthentication);
}
}
}
return null;
}
}
这⾥实现了⼀个IntegrationUserDetailsService ,在loadUserByUsername⽅法中会调⽤authenticate⽅法,在authenticate⽅法中会当前上下⽂种的认证类型调⽤不同的IntegrationAuthenticator 来获取⽤户信息,接下来来看下默认的⽤户名密码是如何处理的:
@Component
@Primary
public class UsernamePasswordAuthenticator extends AbstractPreparableIntegrationAuthenticator {
@Autowired
private UcClient ucClient;
@Override
public UserVO authenticate(IntegrationAuthentication integrationAuthentication) {
return ucClient.Username());
}
@Override
public void prepare(IntegrationAuthentication integrationAuthentication) {
}
@Override
public boolean support(IntegrationAuthentication integrationAuthentication) {
return StringUtils.AuthType());
}
}
UsernamePasswordAuthenticator只会处理没有指定的认证类型即是默认的认证类型,这个类中主要是通过⽤户名获取密码。接下来来看下图⽚验证码登录如何处理的:
/**
* 集成验证码认证
* @author LIQIU
* @date 2018-3-31
**/
@Component
public class VerificationCodeIntegrationAuthenticator extends UsernamePasswordAuthenticator {
private final static String VERIFICATION_CODE_AUTH_TYPE = "vc";
@Autowired
private VccClient vccClient;
@Override
public void prepare(IntegrationAuthentication integrationAuthentication) {
String vcToken = AuthParameter("vc_token");
String vcCode = AuthParameter("vc_code");
//验证验证码
Result<Boolean> result = vccClient.validate(vcToken, vcCode, null);
if (!Data()) {
throw new OAuth2Exception("验证码错误");
}
}
@Override
public boolean support(IntegrationAuthentication integrationAuthentication) {
return VERIFICATION_CODE_AUTH_TYPE.AuthType());
}
}
VerificationCodeIntegrationAuthenticator继承UsernamePasswordAuthenticator,因为其只是需要在prepare⽅法中验证验证码是否正确,获取⽤户还是⽤过⽤户名密码的⽅式获取。但是需要认证类型为"vc"才会处理
接下来来看下短信验证码登录是如何处理的:
@Component
public class SmsIntegrationAuthenticator extends AbstractPreparableIntegrationAuthenticator imple
ments ApplicationEventPublisherAware {
@Autowired
web端登录private UcClient ucClient;
@Autowired
private VccClient vccClient;
@Autowired
private PasswordEncoder passwordEncoder;
private ApplicationEventPublisher applicationEventPublisher;
private final static String SMS_AUTH_TYPE = "sms";
@Override
public UserVO authenticate(IntegrationAuthentication integrationAuthentication) {
/
/获取密码,实际值是验证码
String password = AuthParameter("password");
//获取⽤户名,实际值是⼿机号
String username = Username();
//发布事件,可以监听事件进⾏⾃动注册⽤户
this.applicationEventPublisher.publishEvent(new SmsAuthenticateBeforeEvent(integrationAuthentication));
//通过⼿机号码查询⽤户
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论