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")
    );
  }

  @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<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);
      }
    }
  }

  @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 = 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 {

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

总结

在这个解决方案中,主要是使用责任链和适配器的设计模式来解决集成登录的问题,提高了可扩展性,并对spring的源码无污染。如果还要继承其他的登录,只需要实现自定义的IntegrationAuthenticator就可以。

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

本地下载:cola-cloud_jb51.rar

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Spring Security OAuth2实现使用JWT的示例代码

    1.概括 在博客中,我们将讨论如何让Spring Security OAuth2实现使用JSON Web Tokens. 2.Maven 配置 首先,我们需要在我们的pom.xml中添加spring-security-jwt依赖项. <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> &l

  • 基于Spring Security的Oauth2授权实现方法

    前言 经过一段时间的学习Oauth2,在网上也借鉴学习了一些大牛的经验,推荐在学习的过程中多看几遍阮一峰的<理解OAuth 2.0>,经过对Oauth2的多种方式的实现,个人推荐Spring Security和Oauth2的实现是相对优雅的,理由如下: 1.相对于直接实现Oauth2,减少了很多代码量,也就减少的查找问题的成本. 2.通过调整配置文件,灵活配置Oauth相关配置. 3.通过结合路由组件(如zuul),更好的实现微服务权限控制扩展. Oauth2概述 oauth2根据使用场景不同

  • Spring Security OAuth2认证授权示例详解

    本文介绍了如何使用Spring Security OAuth2构建一个授权服务器来验证用户身份以提供access_token,并使用这个access_token来从资源服务器请求数据. 1.概述 OAuth2是一种授权方法,用于通过HTTP协议提供对受保护资源的访问.首先,OAuth2使第三方应用程序能够获得对HTTP服务的有限访问权限,然后通过资源所有者和HTTP服务之间的批准交互来让第三方应用程序代表资源所有者获取访问权限. 1.1 角色 OAuth定义了四个角色 资源所有者 - 应用程序的

  • 使用Spring Security OAuth2实现单点登录

    1.概述 在本教程中,我们将讨论如何使用Spring Security OAuth和Spring Boot实现SSO - 单点登录. 我们将使用三个单独的应用程序: •授权服务器 - 这是中央身份验证机制 •两个客户端应用程序:使用SSO的应用程序 非常简单地说,当用户试图访问客户端应用程序中的安全页面时,他们将被重定向到首先通过身份验证服务器进行身份验证. 我们将使用OAuth2中的授权代码授权类型来驱动身份验证委派. 2.客户端应用程序 让我们从客户端应用程序开始;当然,我们将使用Sprin

  • Spring Security OAuth2 token权限隔离实例解析

    这篇文章主要介绍了Spring Security OAuth2 token权限隔离实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 由于项目OAuth2采用了多种模式,授权码模式为第三方系统接入,密码模式用于用户登录,Client模式用于服务间调用, 所有不同的模式下的token需要用 @PreAuthorize("hasAuthority('client')") 进行隔离,遇到问题一直验证不通过. 通过调试发现资源服务从授权服

  • spring-boot集成spring-security的oauth2实现github登录网站的示例

    spring-security 里自带了oauth2,正好YIIU里也用到了spring-security做权限部分,那为何不直接集成上第三方登录呢? 然后我开始了折腾 注意:本篇只折腾了spring-security oauth2的客户端部分,spring-security还可以搭建标准的oauth2服务端 引入依赖 <dependency> <groupId>org.springframework.security.oauth</groupId> <artif

  • Spring Security Oauth2.0 实现短信验证码登录示例

    本文介绍了Spring Security Oauth2.0 实现短信验证码登录示例,分享给大家,具体如下: 定义手机号登录令牌 /** * @author lengleng * @date 2018/1/9 * 手机号登录令牌 */ public class MobileAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecur

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

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

  • 基于Redis实现短信验证码登录项目示例(附源码)

    目录 Redis短信登录流程描述 短信验证码的发送 短信验证码的验证 是否登录的验证 源码分析 模拟发送短信验证码 短信验证码的验证 校验是否登录 登录验证优化 Redis短信登录流程描述 短信验证码的发送 用户提交手机号,系统验证手机号是否有效,毕竟无效手机号会消耗你的短信验证次数还会导致系统的性能下降.如果手机号为无效的话就让用户重新提交手机号,如果有效就生成验证码并将该验证码作为value保存到redis中对应的key是手机号,之所以这么做的原因是保证key的唯一性,如果使用固定字符串作为

  • vue_drf实现短信验证码

    目录 一.需求 1,需求 二.sdk参数配置 1,目录结构 三.代码实现 1,后端代码 2,前端代码 一.需求 1,需求 我们在做网站开发时,登录页面很多情况下是可以用手机号接收短信验证码,然后实现登录的,那我们今天就来做一做这一功能. 伪代码: 进入登录页面,点击短信登录 输入手机号码,点击获取验证码,后端在redis里保存验证码 用户把手机收到的验证码输入,点击登录,会把手机号和验证码发往后端,然后进行验证 要想发送短信,让用户收到短信,我们的借助一个容联云的接口,注册一个账号. 使用时需要

  • Spring Security 实现短信验证码登录功能

    之前文章都是基于用户名密码登录,第六章图形验证码登录其实还是用户名密码登录,只不过多了一层图形验证码校验而已:Spring Security默认提供的认证流程就是用户名密码登录,整个流程都已经固定了,虽然提供了一些接口扩展,但是有些时候我们就需要有自己特殊的身份认证逻辑,比如用短信验证码登录,它和用户名密码登录的逻辑是不一样的,这时候就需要重新写一套身份认证逻辑. 开发短信验证码接口 获取验证码 短信验证码的发送获取逻辑和图片验证码类似,这里直接贴出代码. @GetMapping("/code/

  • Spring Security短信验证码实现详解

    目录 需求 实现步骤 获取短信验证码 短信验证码校验过滤器 短信验证码登录认证 配置类进行综合组装 需求 输入手机号码,点击获取按钮,服务端接受请求发送短信 用户输入验证码点击登录 手机号码必须属于系统的注册用户,并且唯一 手机号与验证码正确性及其关系必须经过校验 登录后用户具有手机号对应的用户的角色及权限 实现步骤 获取短信验证码 短信验证码校验过滤器 短信验证码登录认证过滤器 综合配置 获取短信验证码 在这一步我们需要写一个controller接收用户的获取验证码请求.注意:一定要为"/sm

  • Spring Security短信验证码实现详解

    目录 需求 实现步骤 获取短信验证码 短信验证码校验过滤器 短信验证码登录认证 配置类进行综合组装 需求 输入手机号码,点击获取按钮,服务端接受请求发送短信 用户输入验证码点击登录 手机号码必须属于系统的注册用户,并且唯一 手机号与验证码正确性及其关系必须经过校验 登录后用户具有手机号对应的用户的角色及权限 实现步骤 获取短信验证码 短信验证码校验过滤器 短信验证码登录认证过滤器 综合配置 获取短信验证码 在这一步我们需要写一个controller接收用户的获取验证码请求.注意:一定要为"/sm

  • SpringBoot + SpringSecurity 短信验证码登录功能实现

    实现原理 在之前的文章中,我们介绍了普通的帐号密码登录的方式: SpringBoot + Spring Security 基本使用及个性化登录配置. 但是现在还有一种常见的方式,就是直接通过手机短信验证码登录,这里就需要自己来做一些额外的工作了. 对SpringSecurity认证流程详解有一定了解的都知道,在帐号密码认证的过程中,涉及到了以下几个类:UsernamePasswordAuthenticationFilter(用于请求参数获取),UsernamePasswordAuthentica

  • 基于 antd pro 的短信验证码登录功能(流程分析)

    概要 最近使用 antd pro 开发项目时遇到个新的需求, 就是在登录界面通过短信验证码来登录, 不使用之前的用户名密码之类登录方式. 这种方式虽然增加了额外的短信费用, 但是对于安全性确实提高了不少. antd 中并没有自带能够倒计时的按钮, 但是 antd pro 的 ProForm components 中倒是提供了针对短信验证码相关的组件. 组件说明可参见: https://procomponents.ant.design/components/form 整体流程 通过短信验证码登录的

  • Redis实现短信验证码登录的示例代码

    目录 效果图 pom.xml applicatoin.yml Redis配置类 controller serviceImpl mapper 效果图 发送验证码 输入手机号.密码以及验证码完成登录操作 pom.xml 核心依赖 <dependencies> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version&g

随机推荐