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

基于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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/27161.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

谷歌Google Authenticator实现双因素认证

参考&#xff1a; https://www.cnblogs.com/hanyifeng/p/kevin4real.html 介绍&#xff1a;什么是双因素认证 双因素身份认证就是通过你所知道再加上你所能拥有的这二个要素组合到一起才能发挥作用的身份认证系统。双因素认证是一种采用时间同步技术的系统&#xff0c;采用了…

Google-Authenticator双因子认证

简介&#xff1a;Google-authenticator是基于时间的一次性密码算法&#xff08;TOTP&#xff09;是一种根据预共享的密钥与当前时间计算一次性密码的算法。它已被互联网工程任务组接纳为RFC 6238标准[1]&#xff0c;成为主动开放认证&#xff08;OATH&#xff09;的基石&#x…

通过谷歌身份验证器实现双保险认证(1)

目录 一、谷歌身份验证器 二、下载谷歌验证器 1.linux端下载 2.手机端下载 三、linux端安装 1.安装环境 2.解压 3.编译安装​编辑 4.使用 一、谷歌身份验证器 谷歌身份验证器&#xff08;google authenticator&#xff09;是谷歌推出的基于时间的一次性密码&#xff08…

微信消息推送配置token验证

留一篇帖子来避坑 在接口配置修改中&#xff0c;微信官方需要验证你申请时填写的url能够正常响应&#xff0c;会给你填写的url接口发送get请求&#xff0c;接口需要正确返回才能都配置成功。我按照指引查看文档&#xff1a; 这狗官方只给php示例也就算了&#xff0c;这php的代码…

四、登录验证功能

登录验证. 用户访问任何业务资源,都需要进行登录验证. *只有登录成功的用户才能访问业务资源 *没有登录成功的用户访问业务资源,跳转到登录页面 分析 1.过滤器 ①implements Filter{ init doFilter destroy } ②配置过滤器web.xml 2.拦截器&#xff1a; ①提供拦截…

用双因子认证2FA替换Google authenticator谷歌令牌,助力准上市公司实现等保安全审计

21世纪初&#xff0c;某人力资源科技公司试水HR SaaS赛道&#xff0c;以大客户为目标客群&#xff0c;持续深耕&#xff0c;稳扎稳打&#xff0c;如今已是一家专门为中大型企业提供一体化HR SaaS及人才管理产品/解决方案的头部企业。其产品覆盖了从员工招募、入职、管理到离职的…

5 客户端认证 OAuth2ClientAuthenticationFilter

客户端认证 https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method 当访问 OAuth2 相关接口时&#xff08;/oauth2/token、/oauth2/introspect、…

outlook突然变得很慢很卡

​outlook之前用的好好的&#xff0c;突然有一天变得很卡&#xff0c;特别是在上传附件后。 可能原因&#xff1a;没有勾选使用缓存Exchange模式&#xff0c;导致没有生成本地的同步ost文件&#xff0c;而是直接从服务器存取数据。 对应解决办法&#xff1a;到账户设置里&#…

学术海报Poster-- 模板分享

学术海报Poster-- 模板分享 0、引言1、下载地址百度网盘链接地址CSDN托管 2、模板样例 ⚠申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xff0c;请标注链接地址。 全文共计3077字&#xff0c;阅读大概需要3分钟 &#x1f308;更多学习内容…

世界杯球队分析

文章目录 1. 本文思路2.数据介绍2.1 results.csv数据集2.2 shootouts.csv数据集 3. 数据分析3.1 多维度分析3.2 数据分析案例3.2.1 导入模块3.2.2 导入数据1.遍历目录2.读取数据 3.2.3 查看描述数据1.查看数据2. 获取所有世界杯比赛的数据3. 查看包含FIFA的类型4. 获取世界杯数…

NBA比赛数据分析与预测

我的任务利用13到16年的NBA比赛统计数据&#xff0c;去预测17年的每场NBA比赛。数据是从http://www.basketball-reference.com/这个网站获得的。前期参考了https://www.shiyanlou.com/courses/782/labs/2647/document这里的分析与实现方法。这个实验楼里实现用了LogisticRegres…

使用python爬取足球比赛数据,关于足球预测策略模型,这是我见过唯一三年都盈利的

市场上有很多基于程序自动化的足球预测模型&#xff0c;我本人也不断摸索&#xff0c;自学python&#xff0c;最终实现了程序预测的自动化&#xff0c;并且验证了很多策略&#xff0c;几乎所有的模型都是阶段性表现很不错&#xff0c;但是长期总体下来&#xff0c;都达不到预期…

大数据+NCAA=?球迷情绪预测体育赛事结果

点击上方蓝字关注我们 每年三月&#xff0c;有一项赛事将吸引全世界篮球迷的眼光&#xff0c;那就是全国大学篮球联赛&#xff08;NCAA&#xff09;的“疯狂三月”。 “疯狂三月”指的是NCAA男子甲组&#xff08;Division 1&#xff09;篮球锦标赛&#xff0c;赛程从3月中旬持…

生成模型的2022年——人工智能AIGC顶级论文回顾

2022年是生成模型奇幻发展的一年&#xff0c;Stable Diffusion&#x1f3a8;创造了超现实主义的艺术&#xff0c; ChatGPT &#x1f4ac;回答了生命意义的问题&#xff0c;Make a Video&#x1f3a5;从文本生成了栩栩如生的马儿&#xff0c;DreamFusion✨生成了不可思议的三维模…

大语言模型

前言&#xff1a; Open AI推出Chat GPT后&#xff0c;风靡全球。AI的强大表现让人折服&#xff0c;带来的商业效应已经逐渐扩散开来。随着人工智能越来越强&#xff0c;未来人们的生活受到影响也会越来越广泛。 继 OpenAI 推出 ChatGPT 后&#xff0c;微软迅速上线基于 GPT 模型…

OpenAI 利用基于“一致性”的图像生成器超越扩散模型

图像生成领域发展迅速。 尽管 Midjourney 和 Stable Diffusion 等流行工具使用的扩散模型可能看起来是我们所拥有的最好的&#xff0c;但下一个东西总是会出现——OpenAI 可能会用“一致性模型”来解决它&#xff0c;它已经可以完成简单的任务和 数量级比 DALL-E 之类的快。 该…

扩散模型和Transformer梦幻联动!替换U-Net,一举拿下新SOTA!

点击下方卡片&#xff0c;关注“CVer”公众号 AI/CV重磅干货&#xff0c;第一时间送达 点击进入—>扩散模型微信技术交流群 转载自&#xff1a;量子位 “U-Net已死&#xff0c;Transformer成为扩散模型新SOTA了&#xff01;” 就在ChatGPT占尽AI圈风头时&#xff0c;纽约大学…

扩散模型diffusion model用于图像恢复任务详细原理 (去雨,去雾等皆可),附实现代码

文章目录 1. 去噪扩散概率模型2. 前向扩散3. 反向采样3. 图像条件扩散模型4. 可以考虑改进的点5. 实现代码 话不多说&#xff0c;先上代码&#xff1a; 扩散模型diffusion model用于图像恢复完整可运行代码&#xff0c;附详细实验操作流程 令外一篇简化超分扩散模型SR3来实现图…

0基础学习diffusion_model扩散模型【易理解的公式推导】Denoising Diffusion Probabilistic Models

0基础学习diffusion_model扩散模型【易理解的公式推导】 一、概述二、扩散过程(已知X0求Xt)三、逆扩散过程(已知Xt求Xt-1)1。算法流程图四、结论五、损失函数六、心得体会(优缺点分析)一、概述 DDPM论文链接: Jonathan Ho_Denoising Diffusion Probabilistic Models(NeurIPS…

07.04.2023 日语笔记

貰『もら』える&#xff08;可以……吗&#xff1f;&#xff09; コーヒーをテイクアウトでもらえますか。&#xff08;可以打包咖啡吗&#xff1f;&#xff09; マグカップで、飲み物をもらえますか。&#xff08;可以自带杯子吗&#xff1f;&#xff09; マグカップ&#x…