Spring Security:手机短信验证码功能实现


实现流程

前排提示:需要对spring security底层用户名密码登陆源码有所了解。不了解的可以看我上一篇博客:https://www.cnblogs.com/wwjj4811/p/14474866.html

类比用户名密码登陆流程:

image-20210303161208437

1.进入MobileValidateFilter,对用户输入的验证码进行校验比对

2.手机认证过滤器MobileAuthenticationFilter,校验手机号是否存在

3.自定义MobileAuthenticationToken提供给 MobileAuthenticationFilter

4.自定义MobileAuthenticationProvider提供给 ProviderManager 处理

5.创建针对手机号查询用户信息的MobileUserDetailsService,交给 MobileAuthenticationProvider

6.自定义MobileAuthenticationConfig配置类将上面组件连接起来,添加到容器中

7.将MobileAuthenticationConfig添加到spring security安全配置的过滤器链上

准备工作

自定义全局页面响应类

@Data
public class R {

    // 响应业务状态
    private Integer code;

    // 响应消息
    private String message;

    // 响应中的数据
    private Object data;

    public R() {
    }
    public R(Object data) {
        this.code = 200;
        this.message = "OK";
        this.data = data;
    }
    public R(String message, Object data) {
        this.code = 200;
        this.message = message;
        this.data = data;
    }

    public R(Integer code, String message, Object data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    public static R ok() {
        return new R(null);
    }
    public static R ok(String message) {
        return new R(message, null);
    }
    public static R ok(Object data) {
        return new R(data);
    }
    public static R ok(String message, Object data) {
        return new R(message, data);
    }

    public static R build(Integer code, String message) {
        return new R(code, message, null);
    }

    public static R build(Integer code, String message, Object data) {
        return new R(code, message, data);
    }

    public String toJsonString() {
        return JSON.toJSONString(this);
    }


    /**
     * JSON字符串转成 R 对象
     */
    public static R format(String json) {
        try {
            return JSON.parseObject(json, R.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

自定义认证成功处理器和失败处理器

@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Autowired
    SecurityProperties securityProperties;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        if(LoginResponseType.JSON.equals(securityProperties.getAuthentication().getLoginType())){
            R result = R.ok("认证成功");
            String string = result.toJsonString();
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(result.toJsonString());
        }else {
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
}
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    SecurityProperties securityProperties;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
       if(LoginResponseType.JSON.equals(securityProperties.getAuthentication().getLoginType())){
           R result = R.build(HttpStatus.UNAUTHORIZED.value(), exception.getMessage());
           String string = result.toJsonString();
           response.setContentType("application/json;charset=UTF-8");
           response.getWriter().write(result.toJsonString());
       }else {
           String referer = request.getHeader("Referer");
           String lastUrl = StringUtils.substringBefore(referer,"?");
           super.setDefaultFailureUrl(lastUrl+"?error");
           super.onAuthenticationFailure(request, response, exception);
       }
    }
}

前端代码thymeleaf(省略css)

<form th:action="@{/mobile/form}" action="index.html" method="post">
        <div class="input-group mb-3">
          <input id="mobile" name="mobile" type="text" class="form-control" placeholder="手机号码">
          <div class="input-group-append">
            <div class="input-group-text">
              <span class="fa fa-user"></span>
            </div>
          </div>
        </div>
        <div class="mb-3 row">
            <div class="col-7">
          <input type="text" name="code" class="form-control" placeholder="验证码">
          </div>
          <div class="col-5">
              <a id="sendCode" th:attr="code_url=@{/code/mobile?mobile=}" class="btn  btn-outline-primary btn-large" href="#"> 获取验证码 </a>
          </div>
        </div>
          <div class="col-4">
            <button type="submit" class="btn btn-primary btn-block">登录</button>
          </div>
        </div>

      </form>

配置类

@Data
@Component
@ConfigurationProperties(prefix = "boot.security")
public class SecurityProperties {

    private AuthenticationProperties authentication;

    @Data
    public static class AuthenticationProperties {

        private String[] permitPaths;

    }

}

配置文件:

boot:
  security:
    authentication:
     permitPaths:
       - /code/mobile
       - /mobile/page
       - /mobile/form

发送验证码模拟

public interface SmsSend {
    boolean sendSms(String mobile, String content);
}

@Slf4j
@Component
public class SmsCodeSender implements SmsSend{
    @Override
    public boolean sendSms(String mobile, String content) {
        String sendContent = String.format("短信验证码:%s", content);
        log.info("手机号:{},验证码:{}",mobile,content);
        return true;
    }
}

发送验证码接口

@Controller
public class MobileLoginController {

    public static final String SESSION_KEY = "SESSION_KEY_MOBILE_CODE";

    @Autowired
    SmsSend smsSend;

    /**
     * 前往手机短信登录页
     */
    @RequestMapping("/mobile/page")
    public String toMobilePage(){
        return "login-mobile";
    }

    @RequestMapping("/code/mobile")
    @ResponseBody
    public R smsCode(HttpServletRequest request){
        //这里用apache的随机数工具类生成模拟发送验证码
        //并将验证码存入session中,实际工作中可以存入到redis中,并加上电话号码作为标识
        String code = RandomStringUtils.randomNumeric(4);
        request.getSession().setAttribute(SESSION_KEY, code);
        smsSend.sendSms(request.getParameter("mobile"), code);
        return R.ok();
    }
}

实现MobileUserDetailsService

需要实现UserDetailsService接口。这里我就不查询数据库了,直接默认能查询到一个User。(实际工作根据需要去实现该方法)

/**
 * 通过手机号获取用户信息和权限
 */
@Component
public class MobileUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
        //模拟查到了用户
        return new User("wj", "",true, true, true, true,
                AuthorityUtils.commaSeparatedStringToAuthorityList(""));
    }
}

流程实现

MobileValidateFilter实现

@Component
public class MobileValidateFilter extends OncePerRequestFilter {

    @Autowired
    CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //1.判断是否是手机登录,且post请求
        if ("/mobile/form".equals(request.getRequestURI())
                && "post".equalsIgnoreCase(request.getMethod())) {
            try {
                validate(request);
            } catch (AuthenticationException e) {
                //处理异常
                customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        //放行请求
        filterChain.doFilter(request, response);
    }

    private void validate(HttpServletRequest request) {
        //从session中获取验证码
        String sessionCode = (String)request.getSession().getAttribute(MobileLoginController.SESSION_KEY);
        String code = request.getParameter("code");
        //判断验证码是否为空
        if(StringUtils.isBlank(code)){
            throw new ValidateCodeException("验证码不能为空");
        }
        //判断session中验证码和用户输入的是否相同
        if(!sessionCode.equalsIgnoreCase(code)){
            throw new ValidateCodeException("验证码输入错误");
        }
    }

}

MobileAuthenticationFilter实现

模仿UseranamePasswordAuthenticationFilter的实现,继承AbstractAuthenticationProcessingFilter抽象类,因为输入验证码,不需要用户输入密码,所以比UseranamePasswordAuthenticationFilter少一个字段

public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {


    private String mobileParameter = "mobile";
    private boolean postOnly = true;

    public MobileAuthenticationFilter() {
        super(new AntPathRequestMatcher("/mobile/form", "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());
        }

        String mobile = obtainMobile(request);

        if (mobile == null) {
            mobile = "";
        }
        mobile = mobile.trim();

        MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile);

        //sessionID,hostname
        setDetails(request, authRequest);

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

    /**
     * 获取用户输入的电话号码
     */
    @Nullable
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

    protected void setDetails(HttpServletRequest request,
                              MobileAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

}

MobileAuthenticationToken实现

模仿UseranamePasswordAuthenticationToken,继承AbstractAuthenticationToken

public class MobileAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;//认证前是手机号,认证后是用户信息

    public MobileAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    public MobileAuthenticationToken(Object principal,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    /**
     * 不需要密码,所以返回一个null
     */
    @Override
    public Object getCredentials() {
        return null;
    }

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

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException("");
        }

        super.setAuthenticated(false);
    }

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

MobileAuthenticationProvider实现

DaoAuthenticationProvider的继承树如下:

image-20210303164338804

我们需要实现AuthenticationProvider接口:

public class MobileAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

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

    /**
     * 认证处理:
     * 1.通过手机号码,查询用户信息(UserDetailsService实现)
     * 2.查询到用户信息,则认为认证通过,封装Authentication对象
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        MobileAuthenticationToken mobileAuthenticationToken
                = (MobileAuthenticationToken)authentication;
        String mobile = (String) mobileAuthenticationToken.getPrincipal();
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        if(Objects.isNull(userDetails)){
            throw new AuthenticationServiceException("手机号未注册");
        }
        //认证通过
        MobileAuthenticationToken token = new MobileAuthenticationToken(userDetails, userDetails.getAuthorities());
        token.setDetails(mobileAuthenticationToken.getDetails());
        return token;
    }

    /**
     * 通过这个方法,来选择对应的Provider,即选择MobileAuthenticationProvider
     */
    @Override
    public boolean supports(Class<?> authentication) {
        return (MobileAuthenticationToken.class.isAssignableFrom(authentication));
    }
}

MobileAuthenticationConfig配置类

@Component
public class MobileAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Autowired
    UserDetailsService mobileUserDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
        mobileAuthenticationFilter.setAuthenticationManager(
                http.getSharedObject(AuthenticationManager.class));
        //成功和失败的处理器
        mobileAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
        mobileAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);

        MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
        provider.setUserDetailsService(mobileUserDetailsService);

        //provider绑定到HttpSecurity上
        // MobileAuthenticationFilter放到UsernamePasswordAuthenticationFilter之后
        http.authenticationProvider(provider)
                .addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

spring security配置类

@Slf4j
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Autowired
    private MobileValidateFilter mobileValidateFilter;

    @Autowired
    private MobileAuthenticationConfig mobileAuthenticationConfig;

    /**
     * @Author wen.jie
     * @Description 资源权限配置
     * @Date 2021/3/1 13:55
     **/
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义验证表单
        http.addFilterBefore(mobileValidateFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginPage(securityProperties.getAuthentication().getLoginPage())
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler)
                .and()
                .authorizeRequests()//认证请求
               .antMatchers(securityProperties.getAuthentication().getPermitPaths()).permitAll()//放行指定请求
                .anyRequest().authenticated();//所有访问请求都需认证
        //将手机认证添加到过滤器链上
        http.apply(mobileAuthenticationConfig);
    }

}

启动项目,控制台会打印security的过滤器链

WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
MobileValidateFilter
UsernamePasswordAuthenticationFilter
MobileAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor

记住我功能

默认情况下,记住我功能使用的是NullRememberMeServices

修改很简单,MobileAuthenticationConfig中添加一行

@Component
public class MobileAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Autowired
    UserDetailsService mobileUserDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
        mobileAuthenticationFilter.setAuthenticationManager(
                http.getSharedObject(AuthenticationManager.class));

        //记住我功能
        mobileAuthenticationFilter.setRememberMeServices(http.getSharedObject(RememberMeServices.class));

        //成功和失败的处理器
        mobileAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
        mobileAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);

        //设置provider的UserDetailsService
        MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
        provider.setUserDetailsService(mobileUserDetailsService);

        //provider绑定到HttpSecurity上
        // MobileAuthenticationFilter放到UsernamePasswordAuthenticationFilter之后
        http.authenticationProvider(provider)
                .addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

同时修改spring security配置类:

@Slf4j
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired @Qualifier("customUserDetailsService")
    private UserDetailsService userDetailsService;

    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private MobileValidateFilter mobileValidateFilter;

    @Autowired
    private MobileAuthenticationConfig mobileAuthenticationConfig;

    @Bean
    public JdbcTokenRepositoryImpl jdbcTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //是否启动项目时自动创建表
        jdbcTokenRepository.setCreateTableOnStartup(false);
        return jdbcTokenRepository;
    }

    /**
     * @Author wen.jie
     * @Description 资源权限配置
     * @Date 2021/3/1 13:55
     **/
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //自定义验证表单
        http.addFilterBefore(mobileValidateFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler)
                .and()
                .authorizeRequests()//认证请求
                .antMatchers(securityProperties.getAuthentication().getPermitPaths()).permitAll()//放行指定请求
                .anyRequest().authenticated()//所有访问请求都需认证
                .and()
                .rememberMe()//记住我
                .tokenRepository(jdbcTokenRepository())//保存登陆信息
                .tokenValiditySeconds(60*60*24*7);//记住我功能有效时常
        //将手机认证添加到过滤器链上
        http.apply(mobileAuthenticationConfig);
    }

}

记住我功能的表结构可自行百度


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM