Spring Security OAuth2集成短信验证码登录以及第三方登录

网友投稿 702 2023-02-08


Spring Security OAuth2集成短信验证码登录以及第三方登录

前言

基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spring Boot环境下使用OAuth2.0,提供了开箱即用的组件。但是在开发过程中我们会发现由于Spring Security OAuth2的组件特别全面,这样就导致了扩展很不方便或者说是不太容易直指定扩展的方案,例如:

图片验证码登录

短信验证码登录

微信小程序登录

第三方系统登录

CAS单点登录

在面对这些场景的时候,预计很多对Spring Security OAuth2不熟悉的人恐怕会无从下手。基于上述的场景要求,如何优雅的集成短信验证码登录及第三方登录,怎么样才算是优雅集成呢?有以下要求:

不侵入Spring Security OAuth2的原有代码

对于不同的登录方式不扩展新的端点,使用/oauth/token可以适配所有的登录方式

可以对所有登录方式进行兼容,抽象一套模型只要简单的开发就可以集成登录

基于上述的设计要求,接下来将会在文章种详细介绍如何开发一套集成登录认证组件开满足上述要求。

阅读本篇文章您需要了解OAuth2.0认证体系、SpringBoot、SpringSecurity以及Spring Cloud等相关知识

思路

我们来看下Spring Security OAuth2的认证流程:

这个流程当中,切入点不多,集成登录的思路如下:

在进入流程之前先进行拦截,设置集成认证的类型,例如:短信验证码、图片验证码等信息。

在拦截的通知进行预处理,预处理的场景有很多,比如验证短信验证码是否匹配、图片验证码是否匹配、是否是登录IP白名单等处理

在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 authenticators;

private ApplicationContext applicationContext;

private RequestMatcher requestMatcher;

public IntegrationAuthenticationFilter(){

this.requestMatcher = new OrRequestMatcher(

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.setAuthType(request.getParameter(AUTH_TYPE_PARM_NAME));

integrationAuthentication.setAuthParameters(request.getParameterMap());

IntegrationAuthenticationContext.set(integrationAuthentication);

try{

//预处理

this.prepare(integrationAuthentication);

filterChain.doFilter(request,response);

//后置处理

this.complete(integrationAuthentication);

}finally {

IntegrationAuthenticationContext.clear();

}

}else{

filterChain.doFilter(request,response);

}

}

/**

* 进行预处理

* @param integrationAuthentication

*/

private void prepare(IntegrationAuthentication integrationAuthentication) {

//延迟加载认证器

if(this.authenticators == null){

synchronized (this){

Map integrationAuthenticatorMap = applicationContext.getBeansOfType(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)){

authenticator.complete(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 authenticators;

@Autowired(required = false)

public void setIntegrationAuthenticators(List authenticators) {

this.authenticators = authenticators;

}

@Override

public User loadUserByUsername(String username) throws UsernameNotFoundException {

IntegrationAuthentication integrationAuthentication = IntegrationAuthenticationContext.get();

//判断是否是集成登录

if (integrationAuthentication == null) {

integrationAuthentication = new IntegrationAuthentication();

}

integrationAuthentication.setUsername(username);

UserVO userVO = this.authenticate(integrationAuthentication);

if(userVO == null){

throw new UsernameNotFoundException("用户名或密码错误");

}

User user = new User();

BeanUtils.copyProperties(userVO, user);

this.setAuthorize(user);

return user;

}

/**

* 设置授权信息

*

* @param user

*/

public void setAuthorize(User user) {

Authorize authorize = this.upmClient.getAuthorize(user.getId());

user.setRoles(authorize.getRoles());

user.setResources(authorize.getResources());

}

private UserVO authenticate(IntegrationAuthentication integrationAuthentication) {

if (this.authenticators != null) {

for (IntegrationAuthenticator authenticator : authenticators) {

if (authenticator.support(integrationAuthentication)) {

return authenticator.authenticate(integrationAuthentication);

kuvmMukYb }

}

}

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.findUserByUsername(integrationAuthentication.getUsername());

}

@Override

public void prepare(IntegrationAuthentication integrationAuthentication) {

}

@Override

public boolean support(IntegrationAuthentication integrationAuthentication) {

return StringUtils.isEmpty(integrationAuthentication.getAuthType());

}

}

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 = integrationAuthentication.getAuthParameter("vc_token");

String vcCode = integrationAuthentication.getAuthParameter("vc_code");

//验证验证码

Result result = vccClient.validate(vcToken, vcCode, null);

if (!result.getData()) {

throw new OAuth2Exception("验证码错误");

}

}

@Override

public boolean support(IntegrationAuthentication integrationAuthentication) {

return VERIFICATION_CODE_AUTH_TYPE.equals(integrationAuthentication.getAuthType());

}

}

VerificationCodeIntegrationAuthenticator继承UsernamePasswordAuthenticator,因为其只是需要在prepare方法中验证验证码是否正确,获取用户还是用过用户名密码的方式获取。但是需要认证类型为"vc"才会处理

接下来来看下短信验证码登录是如何处理的:

@Component

public class SmsIntegrationAuthenticator extends AbstractPreparableIntegrationAuthenticator implements ApplicationEventPublisherAware {

@Autowired

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 = integrationAuthentication.getAuthParameter("password");

//获取用户名,实际值是手机号

String username = integrationAuthentication.getUsername();

//发布事件,可以监听事件进行自动注册用户

this.applicationEventPublisher.publishEvent(new SmsAuthenticateBeforeEvent(integrationAuthentication));

//通过手机号码查询用户

UserVO userVo = this.ucClient.findUserByPhoneNumber(username);

if (userVo != null) {

//将密码设置为验证码

userVo.setPassword(passwordEncoder.encode(password));

//发布事件,可以监听事件进行消息通知

this.applicationEventPublisher.publishEvent(new SmsAuthenticateSuccessEvent(integrationAuthentication));

}

return userVo;

}

@Override

public void prepare(IntegrationAuthentication integrationAuthentication) {

String smsToken = integrationAuthentication.getAuthParameter("sms_token");

String smsCode = integrationAuthentication.getAuthParameter("password");

String username = integrationAuthentication.getAuthParameter("username");

Result result = vccClient.validate(smsToken, smsCode, username);

if (!result.getData()) {

throw new OAuth2Exception("验证码错误或已过期");

}

}

@Override

public boolean support(IntegrationAuthentication integrationAuthentication) {

return SMS_AUTH_TYPE.equals(integrationAuthentication.getAuthType());

}

@Override

public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {

this.applicationEventPublisher = applicationEventPublisher;

}

}

SmsIntegrationAuthenticator会对登录的短信验证码进行预处理,判断其是否非法,如果是非法的则直接中断登录。如果通过预处理则在获取用户信息的时候通过手机号去获取用户信息,并将密码重置,以通过后续的密码校验。

总结

在这个解决方案中,主要是使用责任链和适配器的设计模式来解决集成登录的问题,提高了可扩展性,并对spr

ing的源码无污染。如果还要继承其他的登录,只需要实现自定义的IntegrationAuthenticator就可以。

项目地址:https://gitee.com/leecho/cola-cloud

本地下载:cola-cloud_jb51.rar


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:React diff算法的实现示例
下一篇:查看已连接共享文件夹账号(如何查看访问共享文件登录用户)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~