Spring Security 自定义短信验证码登录

短信验证码登录的思路,需要通过验证码过滤器,过滤验证码是否正确。次过程和图形验证码校验逻辑完全一样。 之后,需要通过Spring Security 认真的一套逻辑,来去数据库查询用户信息,进行 认证信息 Authentication的封装。

此处案例的Provider认证校验类,只是从数据库查询信息,然后进行封装。实际开发中可能需求不同,按需求进行更改。

发送验证码功能

1、定义验证码实体类
@Data
public class ValidateCode {

    /**
     *  验证码
     */
    private String code;

    /**
     *  过期时间
     */
    private LocalDateTime expireTime;

    public ValidateCode(String code, int expireTime) {
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
    }

    public ValidateCode(String code, LocalDateTime expireTime) {
        this.code = code;
        this.expireTime = expireTime;
    }

    /**
     *  判断验证码是否过期
     * @return
     */
    public boolean isExpire() {
        return LocalDateTime.now().isAfter(expireTime);
    }
}
2、定义生成验证码的接口
/**
 * @Author L.jg
 * @Title       抽象接口,让客户端可配置接口
 * @Date 2021/5/24 11:42
 */
public interface ValidateCodeGenerate {

    ValidateCode generate(HttpServletRequest request);

}
3、 实现生成短信验证码
public class SmsCodeGenerate implements ValidateCodeGenerate {

    private SmsProperties smsProperties;

    public SmsCodeGenerate(SmsProperties smsProperties) {
        this.smsProperties = smsProperties;
    }

    @Override
    public ValidateCode generate(HttpServletRequest request) {
        String smsCode = RandomUtil.randomNumbers(smsProperties.getLength());
        return new ValidateCode(smsCode, smsProperties.getExpireTime());
    }
}
4、将生成短信验证码的类加入Bean容器
@Configuration
public class VlidateCodeConfig {

    @Autowired
    private SecurityProperties securityProperties;

    @Bean
    @ConditionalOnMissingBean(name = "smsCodeGenerate")
    public ValidateCodeGenerate smsCodeGenerate() {
        SmsCodeGenerate smsCodeGenerate = new SmsCodeGenerate(securityProperties.getValidateCode().getSms());
        return smsCodeGenerate;
    }
}
5、发送验证码接口
public interface SmsCodeSender {

    /**
     * 发送验证码
     *
     * @param mobile 手机号
     * @param code  验证码
     */
    void send(String mobile, String code);
}
6、模拟发送验证码实现类
@Component
public class DefaultSmsCodeSender implements SmsCodeSender {

    @Override
    public void send(String mobile, String code) {
        System.out.println("手机号:" + mobile + "短信验证码:" + code);
    }
}
7、 发送验证码接口
    @GetMapping("/sms/code")
    public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
        // 生成imageCode
//        ImageCode imageCode = createImageCode(request);
        ValidateCode smsCode = smsCodeGenerate.generate(request);
        // 将imageCode 保存在session中
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, smsCode);
        String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
        smsCodeSender.send(mobile, smsCode.getCode());
    }

手机验证码登录

以Spring Security的form表单为例,是先通过UsernamePasswordAuthenticationFilter 来获取用户的登录信息。
然后将登录信息封装为UsernamePasswordAuthenticationToken
将封装的 Authentication信息交给 AuthenticationManager管理。
根据Authentication的类型,调用对应的Provider来处理认证逻辑。

这里参考 UsernamepasswordAuthenticationFilterUsernamePasswordAuthenticationTokenDaoAuthenticationProvider 实现自己的各个类。

1、自定义 SmsCodeAuthenticationToken
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

  
    // 手机号
    private final Object principal;

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

    public SmsCodeAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

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

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

    public void eraseCredentials() {
        super.eraseCredentials();
    }
}
2、自定义 SmsCodeAuthenticationFilter
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private String mobileParameter = "mobile";

    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String mobile = this.obtainMobile(request);
            if (mobile == null) {
                mobile = "";
            }
            mobile = mobile.trim();
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }


    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(this.mobileParameter);
    }


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

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

}
3、自定义 SmsCodeAuthenticationProvider
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username =  authentication.getPrincipal().toString();
        UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getPrincipal().toString());
        if (userDetails == null) {
            throw new InternalAuthenticationServiceException("无法通过手机号获取用户信息");
        }
        SmsCodeAuthenticationToken smsCodeAuthenticationToken = new SmsCodeAuthenticationToken( userDetails.getAuthorities(),userDetails);
        smsCodeAuthenticationToken.setDetails(authentication.getDetails());
        return smsCodeAuthenticationToken;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}
4、SmsCodeAuthenticationSecurityConfig 配置类

将自定义的实现逻辑,配置到 Security 里

/**
 * @Author L.jg
 * @Title   app和浏览器都需要使用,短信验证配置
 * @Date 2021/5/24 17:35
 */
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity builder) throws Exception {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler);
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        builder.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
5、 应用配置
/**
     *  短信自定义登录config
     */
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    /**
     *  为减少代码重复开发,多个应用使用同一个认证中心,每个应用需要自己指定登录页面。
     *  这里需要将 loginpage 指向一个controlelr地址。
     *  如果是html页面,就跳转到指定的登录页。
     *  如果不是html页面,就提示401 没有认证信息。
     *  如果有应用有指定的就使用自己的。如果没指定就使用本认证模块默认的登录页。
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(loginFailureHandler);

        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.afterPropertiesSet();
        // 配置过滤器的位置
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
        http.apply(smsCodeAuthenticationSecurityConfig);
        http.formLogin()
                .loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/form")
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)
//                .successForwardUrl("/index")
                .defaultSuccessUrl("/index")
                .and()
                .authorizeRequests()
                .antMatchers("/sms/code","/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable();

    }

这里缺少了,登录验证码校验功能,可以参考 图像验证码校验功能,只要在 添加一个过滤器,自定义校验即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值