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

作者:袖梨 2022-11-14

实现原理

对SpringSecurity认证流程详解有一定了解的都知道,在帐号密码认证的过程中,涉及到了以下几个类:UsernamePasswordAuthenticationFilter(用于请求参数获取),UsernamePasswordAuthenticationToken(表示用户登录信息),ProviderManager(进行认证校验),

因为是通过的短信验证码登录,所以我们需要对请求的参数,认证过程,用户登录Token信息进行一定的重写。
当然验证码的过程我们应该放在最前面,如果图形验证码的实现一样。这样的做法的好处是:将验证码认证该过程解耦出来,让其他接口也可以使用到。

基本实现

验证码校验

短信验证码的功能实现,其实和图形验证码的原理是一样的。只不过一个是返回给前端一个图片,一个是给用户发送短消息,这里只需要去调用一下短信服务商的接口就好了。更多的原理可以参考 SpringBoot + SpringSecurity 实现图形验证码功能

AuthenticationToken

在使用帐号密码登录的时候,UsernamePasswordAuthenticationToken里面包含了用户的帐号,密码,以及其他的是否可用等状态信息。我们是通过手机短信来做登录,所以就没有密码了,这里我们就直接将UsernamePasswordAuthenticationToken的代码copy过来,把密码相关的信息去掉就可以了

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

  private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

  private final Object principal;

  public SmsCodeAuthenticationToken(String mobile) {
    super(null);
    this.principal = mobile;
    setAuthenticated(false);
  }

  public SmsCodeAuthenticationToken(Object principal,
                   Collection authorities) {
    super(authorities);
    this.principal = principal;
    super.setAuthenticated(true); // must use super, as we override
  }

  public Object getCredentials() {
    return null;
  }

  public Object getPrincipal() {
    return this.principal;
  }

  public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
    if (isAuthenticated) {
      throw new IllegalArgumentException(
          "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
    }
    super.setAuthenticated(false);
  }

  @Override
  public void eraseCredentials() {
    super.eraseCredentials();
  }
}

AuthenticationFilter

在帐户密码登录的流程中,默认使用的是UsernamePasswordAuthenticationFilter,它的作用是从请求中获取帐户、密码,请求方式校验,生成AuthenticationToken。这里我们的参数是有一定改变的,所以还是老方法,copy过来进行简单的修改

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  // 请求参数key
  private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE;
  // 是否只支持POST
  private boolean postOnly = true;

  public SmsCodeAuthenticationFilter() {
    // 请求接口的url
    super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, "POST"));
  }

  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
      throws AuthenticationException {
    if (postOnly && !request.getMethod().equals("POST")) {
      throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    // 根据请求参数名,获取请求value
    String mobile = obtainMobile(request);
    if (mobile == null) {
      mobile = "";
    }
    mobile = mobile.trim();

    // 生成对应的AuthenticationToken
    SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

    setDetails(request, authRequest);

    return this.getAuthenticationManager().authenticate(authRequest);
  }

  /**
   * 获取手机号
   */
  protected String obtainMobile(HttpServletRequest request) {
    return request.getParameter(mobileParameter);
  }
  // 省略不相关代码
}

Provider

在帐号密码登录的过程中,密码的正确性以及帐号是否可用是通过DaoAuthenticationProvider来校验的。我们也应该自己实现一个Provier

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

  private UserDetailsService userDetailsService;

  /**
   * 身份逻辑验证
   * @param authentication
   * @return
   * @throws AuthenticationException
   */
  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

    UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

    if (user == null) {
      throw new InternalAuthenticationServiceException("无法获取用户信息");
    }

    SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());

    authenticationResult.setDetails(authenticationToken.getDetails());

    return authenticationResult;
  }

  @Override
  public boolean supports(Class authentication) {
    return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
  }

  public UserDetailsService getUserDetailsService() {
    return userDetailsService;
  }

  public void setUserDetailsService(UserDetailsService userDetailsService) {
    this.userDetailsService = userDetailsService;
  }
}

配置

主要的认证流程就是通过以上四个过程实现的, 这里我们再降它们配置一下就可以了

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter {

  @Autowired
  private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

  @Autowired
  private AuthenticationFailureHandler myAuthenticationFailureHandler;

  @Autowired
  private UserDetailsService userDetailsService;

  @Override
  public void configure(HttpSecurity http) throws Exception {

    SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
    smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
    smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
    smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);

    SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
    smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

    http.authenticationProvider(smsCodeAuthenticationProvider)
        .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

  }
}
 
// BrowerSecurityConfig.java
@Override
protected void configure(HttpSecurity http) throws Exception {
  http.apply(smsCodeAuthenticationSecurityConfig);
}

代码下载

Spring-Security

相关文章

精彩推荐