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的认证流程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SEWhEf88-1624506115358)(https://segmentfault.com/img/bV8sGr?w=1201&h=417)]
这个流程当中,切入点不多,集成登录的思路如下:
- 在进入流程之前先进行拦截,设置集成认证的类型,例如:短信验证码、图片验证码等信息。
- 在拦截的通知进行预处理,预处理的场景有很多,比如验证短信验证码是否匹配、图片验证码是否匹配、是否是登录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<IntegrationAuthenticator> 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"));}@Overridepublic 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<String,IntegrationAuthenticator> 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);}}}@Overridepublic 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 {@Autowiredprivate RedisConnectionFactory redisConnectionFactory;@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate IntegrationUserDetailsService integrationUserDetailsService;@Autowiredprivate WebResponseExceptionTranslator webResponseExceptionTranslator;@Autowiredprivate IntegrationAuthenticationFilter integrationAuthenticationFilter;@Autowiredprivate DatabaseCachableClientDetailsService redisClientDetailsService;@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {// TODO persist clients detailsclients.withClientDetails(redisClientDetailsService);}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
// .accessTokenConverter(jwtAccessTokenConverter()).authenticationManager(authenticationManager).exceptionTranslator(webResponseExceptionTranslator).reuseRefreshTokens(false).userDetailsService(integrationUserDetailsService);}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.allowFormAuthenticationForClients().tokenKeyAccess("isAuthenticated()").checkTokenAccess("permitAll()").addTokenEndpointAuthenticationFilter(integrationAuthenticationFilter);}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();jwtAccessTokenConverter.setSigningKey("cola-cloud");return jwtAccessTokenConverter;}
}
通过调用security. .addTokenEndpointAuthenticationFilter(integrationAuthenticationFilter);方法,将拦截器放入到认证链条中。
第三步,根据认证类型来处理用户信息
@Service
public class IntegrationUserDetailsService implements UserDetailsService {@Autowiredprivate UpmClient upmClient;private List<IntegrationAuthenticator> authenticators;@Autowired(required = false)public void setIntegrationAuthenticators(List<IntegrationAuthenticator> authenticators) {this.authenticators = authenticators;}@Overridepublic 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);}}}return null;}
}
```java这里实现了一个IntegrationUserDetailsService ,在loadUserByUsername方法中会调用authenticate方法,在authenticate方法中会当前上下文种的认证类型调用不同的IntegrationAuthenticator 来获取用户信息,接下来来看下默认的用户名密码是如何处理的:```java
@Component
@Primary
public class UsernamePasswordAuthenticator extends AbstractPreparableIntegrationAuthenticator {@Autowiredprivate UcClient ucClient;@Overridepublic UserVO authenticate(IntegrationAuthentication integrationAuthentication) {return ucClient.findUserByUsername(integrationAuthentication.getUsername());}@Overridepublic void prepare(IntegrationAuthentication integrationAuthentication) {}@Overridepublic 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";@Autowiredprivate VccClient vccClient;@Overridepublic void prepare(IntegrationAuthentication integrationAuthentication) {String vcToken = integrationAuthentication.getAuthParameter("vc_token");String vcCode = integrationAuthentication.getAuthParameter("vc_code");//验证验证码Result<Boolean> result = vccClient.validate(vcToken, vcCode, null);if (!result.getData()) {throw new OAuth2Exception("验证码错误");}}@Overridepublic boolean support(IntegrationAuthentication integrationAuthentication) {return VERIFICATION_CODE_AUTH_TYPE.equals(integrationAuthentication.getAuthType());}
}
VerificationCodeIntegrationAuthenticator继承UsernamePasswordAuthenticator,因为其只是需要在prepare方法中验证验证码是否正确,获取用户还是用过用户名密码的方式获取。但是需要认证类型为"vc"才会处理
接下来来看下短信验证码登录是如何处理的:
@Component
public class SmsIntegrationAuthenticator extends AbstractPreparableIntegrationAuthenticator implements ApplicationEventPublisherAware {@Autowiredprivate UcClient ucClient;@Autowiredprivate VccClient vccClient;@Autowiredprivate PasswordEncoder passwordEncoder;private ApplicationEventPublisher applicationEventPublisher;private final static String SMS_AUTH_TYPE = "sms";@Overridepublic 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;}@Overridepublic void prepare(IntegrationAuthentication integrationAuthentication) {String smsToken = integrationAuthentication.getAuthParameter("sms_token");String smsCode = integrationAuthentication.getAuthParameter("password");String username = integrationAuthentication.getAuthParameter("username");Result<Boolean> result = vccClient.validate(smsToken, smsCode, username);if (!result.getData()) {throw new OAuth2Exception("验证码错误或已过期");}}@Overridepublic boolean support(IntegrationAuthentication integrationAuthentication) {return SMS_AUTH_TYPE.equals(integrationAuthentication.getAuthType());}@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.applicationEventPublisher = applicationEventPublisher;}
}
SmsIntegrationAuthenticator会对登录的短信验证码进行预处理,判断其是否非法,如果是非法的则直接中断登录。如果通过预处理则在获取用户信息的时候通过手机号去获取用户信息,并将密码重置,以通过后续的密码校验。
总结
在这个解决方案中,主要是使用责任链和适配器的设计模式来解决集成登录的问题,提高了可扩展性,并对spring的源码无污染。如果还要继承其他的登录,只需要实现自定义的IntegrationAuthenticator就可以。
https://segmentfault.com/a/1190000014371789
spring security Oauth2验证码等多方式登录相关推荐
- java oauth sso 源码_基于Spring Security Oauth2的SSO单点登录+JWT权限控制实践
概 述 在前文<基于Spring Security和 JWT的权限系统设计>之中已经讨论过基于 Spring Security和 JWT的权限系统用法和实践,本文则进一步实践一下基于 Sp ...
- Spring Security OAuth2 实现多人登录互踢下线
点击上方蓝色字体,选择"标星公众号" 优质文章,第一时间送达 ▊ 老赵推荐(戳下方标题) 阿里大牛程序员的Java问题排查工具单 我已经不用 try catch 处理异常了!太烦人 ...
- 基于Spring Security + OAuth2 的SSO单点登录(服务端)
相关技术 spring security: 用于安全控制的权限框架 OAuth2: 用于第三方登录认证授权的协议 JWT:客户端和服务端通信的数据载体 传统登录 登录web系统后将用户信息保存在ses ...
- Spring Security OAuth2 微服务认证中心自定义授权模式扩展以及常见登录认证场景下的应用实战
本文源码地址 后端:https://gitee.com/youlaitech/youlai-mall/tree/v2.0.1 前端:https://gitee.com/youlaiorg/mall-a ...
- Spring Security Oauth2 单点登录案例实现和执行流程剖析
我已经试过了 教程很完美 Spring Security Oauth2 OAuth是一个关于授权的开放网络标准,在全世界得到的广泛的应用,目前是2.0的版本.OAuth2在"客户端" ...
- Spring Security OAuth2 优雅的集成短信验证码登录以及第三方登录
基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spring ...
- Spring Security OAuth2——自定义OAuth2第三方登录(Gitee)并与UsernamePassword登录关联解决方案
前文:Spring Security OAuth2--自定义OAuth2第三方登录(Gitee) Maven 主要 <!--Spring Security--><dependency ...
- 微信官方你真的懂OAuth2?Spring Security OAuth2整合企业微信扫码登录
❝ 企业微信扫码登录DEMO参见文末. 现在很多企业都接入了企业微信,作为私域社群工具,企业微信开放了很多API,可以打通很多自有的应用.既然是应用,那肯定需要做登录.正好企业微信提供了企业微信扫码授 ...
- Spring Security OAuth2 实现登录互踢
Spring Security OAuth2 实现登录互踢 工作中遇到的问题,通过网上查找资料,解决问题,记录一下,防止丢失. 1.重写DefaultTokenServices中的方法 自定义一个 ...
最新文章
- 中兴手机数据通道打不开_中兴手机有流量,但是数据开不了怎么办?
- luogu_3966【题解】单词 AC自动机
- mysql表分区数量限制_MySQL分区表的局限和限制详解
- labview与单片机正弦信号_LabVIEW小白入门——低通滤波器
- 解决KMPlayer 播放RMVB 一直是快进的问题
- 8——对象的作用域,生存期,……
- 论文解读——Improving Object Detection With One Line of Code
- mysql 常用管理命令
- socket.io html5 聊天,socket.io实现在线聊天页面
- 如何去掉word中的回车符??
- 数据湖产业生态联盟会员权益
- 数据库习题(填空题五)
- 联通智能城域网,到底有什么特别?
- GPS导航电文——第二子帧数据解析
- 软件工程大作业(完整详细)火车订票管理系统
- Linux中的libc和glibc
- 解决 mysql 自动增长的 id 不是从 1 开始 或 不连续
- php仿糗事百科,基于thinkphp框架开发仿糗事百科笑话系统
- R语言散点图分类、配色、添加趋势线
- bzoj 2844 albus就是要第一个出场