基于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(){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;}
}

这里实现了一个IntegrationUserDetailsService ,在loadUserByUsername方法中会调用authenticate方法,在authenticate方法中会当前上下文种的认证类型调用不同的IntegrationAuthenticator 来获取用户信息,接下来来看下默认的用户名密码是如何处理的:

@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 优雅的集成短信验证码登录以及第三方登录相关推荐

  1. Spring Security OAuth2 微服务认证中心自定义授权模式扩展以及常见登录认证场景下的应用实战

    本文源码地址 后端:https://gitee.com/youlaitech/youlai-mall/tree/v2.0.1 前端:https://gitee.com/youlaiorg/mall-a ...

  2. 集成短信验证码--Mob.com

    集成短信验证码–Mob.com ONE Goal,ONE Passion ! 现在大多数app 在注册时会使用手机号直接注册,这样到挺方便的.不过对开发者来说就要去集成一些短信验证的sdk了.用的比较 ...

  3. iOS开发——App集成短信验证码

    无论是在网页上还是在客户端,每当我们进行注册的时候,往往会进行短信或者邮箱验证,这是一种不错的安全机制.对于用户体验来说,如果是在PC的网页上,用邮箱或者短信验证都比较方便:如果是在手机上,那么使用短 ...

  4. python发短信验证码_python利用第三方模块,发送短信验证码

    对于初学者,如何利用第三方python开发包发送短信验证码,下面是具体的实现和记录过程! 环境:虚拟机上centos7平台,python3.7版本: 首先,申请账号的部分就省略了 1. 获得appid ...

  5. 防止刷单杜绝薅羊毛:语音验证码和短信验证码及最新一键登录(秒验点验)解决思路

    1.传统的网站和APP在早期开发时很少关注到刷单防范和羊毛党问题.甚至很多网站注册没有考虑到手机绑定.在以PC为主的互联网时代网站注册时很少使用到短信验证码,随着工信部强制要求手机号必须实名认证,同时 ...

  6. spring security oauth2.0 client集成第三方登录

       大家上网的时候可能会遇见这样的一个问题,就是我们去访问一个网站,但是又不想去注册这个网站的账号,账号太多了实在是记不来,于是我们可以用qq或者微信登录这个网站,简直不要太方便有没有.    这么 ...

  7. mob sdk vue 短信验证_如何快速集成短信验证码API[图文教程]

    基础知识 以SUBMAIL为例,在进行以下步骤前,请先完成您的账户配置并认证. 步骤一.前往->开发者 ->SDK开发包下载页面,下载对应语言的SDK开发包. SUBMAIL 提供多达9种 ...

  8. 手机短信验证码一次性 需要第三方平台

    用户登录到页面 - 输入手机号- 点击获取验证码按钮-后台生成6位随机数 - 通过第三方平台发送到手机 - 并把随机验证码放到session - 用户输入验证码 - 后台匹配手机号与验证码 - 120 ...

  9. Rxjava 优雅的实现短信验证码发送

    废话不多说直接上代码 private fun countDown() {btn_send_code_activity_login.isEnabled = falsedisPose = Flowable ...

最新文章

  1. linux crontab 定时任务 计划任务 不执行 原因
  2. Python slice() 函数
  3. java使用Crawler4j开发爬虫
  4. 剑指Offer - 面试题57. 和为s的两个数字(双指针)
  5. 【AI视野·今日Robot 机器人论文速览 第十五期】Fri, 25 Jun 2021
  6. Python中可变长度的参数args和**kwargs
  7. 机器学习实战系列(六):Adaboost提升法
  8. android toolbar 搜索框,SearchView配合RecyclerView实现分页搜索过滤关键字
  9. 【GitHub】中SSH key的配置
  10. 前馈神经网络练习:使用tensorflow进行葡萄酒种类识别
  11. 参考文献格式、论文尾注
  12. Flash地址空间的数据读取
  13. SM2加解密、签名验签
  14. Signatures-based、Anomaly-based、Specification-based三种入侵检测方法的简介
  15. SQL Server的安装和要求
  16. 第三章 概念模型设计(二)
  17. 大数据培训:Spark性能调优与参数配置
  18. ie firefox 点flash 卡死解决方案
  19. 使用Qwt绘制历史曲线
  20. [学习收藏]Linux系统硬盘优化

热门文章

  1. 互联网日报 | 2月25日 星期四 | 吉利与沃尔沃宣布达成最佳合并方案;货拉拉公布整改方案;中国成美国手游市场最大进口国...
  2. 《恐龙的兴衰》恐龙家族的演化历程
  3. 通用物体识别使用攻略
  4. 无线局域网WLAN扩展器工作模式
  5. SpringMVC06:Json交互处理
  6. 没有可用软件包 lspci yum install pciutils
  7. 算法分析与设计 二分查找
  8. python各种符号的使用_Python的各种符号(最新整理)
  9. Hive:with as用法、问题和优化
  10. Android毛玻璃(磨砂)效果(静态&动态背景图模糊 收集)