SpringSecurity-短信验证码接口开发

2020-03-20 21:04:06 浏览数 (1)

前言

有时候我们需要有特殊登录形式,比如说短信验证码登录。他与验证码登录逻辑是不一样的,所以不能使用Spring Security默认提供的那套逻辑;需要自个去写一个自定义身份认证逻辑。实现步骤如下:

  1. 开发短信验证码接口
  2. 校验短信验证码并登录
  3. 重构代码

内容

1.开发短信验证码接口

ValidateCodeController 我们之前已经写了图形验证码了,现在我们在此基础之上重构代码

1.1 创建验证码实体
代码语言:javascript复制
public class ValidateCode {
    private String code;
    /**
     * 过期时间
     */
    private LocalDateTime expireTime;
    public ValidateCode(String code, int expireIn){
       this.code=code;
        /**
         * 过期时间传递的参数应该是一个秒数:根据这个秒数去计算过期时间
         */
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }
    public boolean isExpried() {
        return LocalDateTime.now().isAfter(expireTime);
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public LocalDateTime getExpireTime() {
        return expireTime;
    }
    public void setExpireTime(LocalDateTime expireTime) {
        this.expireTime = expireTime;
    }
}

图片验证码继承ValidateCode

代码语言:javascript复制
public class ImageCode extends ValidateCode{
    private BufferedImage image;

    public ImageCode(BufferedImage image,String code,int expireIn){
        super(code,expireIn);
        this.image=image;
    }
    public BufferedImage getImage() {
        return image;
    }
    public void setImage(BufferedImage image) {
        this.image = image;
    }
}
1.2 ValidateCodeGenerator改造

因为ImageCode继承ValidateCode,所以我们这个接口返回父类,继承、面向接口编程。

代码语言:javascript复制
public interface ValidateCodeGenerator {
    /**
     * 生成验证码
     * @param request
     * @return
     */
    ValidateCode generate(ServletWebRequest request);
}
1.3 短信发送封装

1.定义短信发送接口

代码语言:javascript复制
public interface SmsCodeSender {
    /**
     * 给某个手机发送短信验证码
     * @param mobile
     * @param code
     */
    void send(String mobile,String code);
}

2.定义短信接口默认实现类 模拟定义默认接口发送实现类

代码语言:javascript复制
public class DefaultSmsCodeSender implements SmsCodeSender {
    @Override
    public void send(String mobile, String code) {
        System.out.println("向手机:" mobile " 发送短信验证码:" code);
    }
}

3.ValidateCodeBeanConfig里面注入

代码语言:javascript复制
@Configuration
public class ValidateCodeBeanConfig {
    @Autowired
    private SecurityProperties securityProperties;

    /*
    * 这个配置与我们在ImageCodeGenerator上面加一个注解是类似的,但是这样配置灵活,
    * 可以添加注解:@ConditionalOnMissingBean 作用是:在初始化这个bean的时候,
    * 先到spring容器去查找imageCodeGenerator,如果有一个imageCodeGenerator时候,
    * 就不会再用下面代码去创建
    **/
    @Bean
    @ConditionalOnMissingBean(name="imageCodeGenerator")
    public ValidateCodeGenerator imageCodeGenerator(){//方法的名字就是放到Spring容器里bean的名字
        ImageCodeGenerator imageCodeGenerator = new ImageCodeGenerator();
        imageCodeGenerator.setSecurityProperties(securityProperties);
        return imageCodeGenerator;
    }

    @Bean
    @ConditionalOnMissingBean(SmsCodeSender.class)
    public SmsCodeSender smsCodeSender(){//方法的名字就是放到Spring容器里bean的名字
        return new DefaultSmsCodeSender();
    }
}
1.2 ValidateCodeController短信验证码生成
代码语言:javascript复制
@RestController
public class ValidateCodeController {
    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private ValidateCodeGenerator imageCodeGenerator;

    @Autowired
    private ValidateCodeGenerator smsCodeGenerator;

    @Autowired
    private SmsCodeSender smsCodeSender;

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        /**
         * 1.根据随机数生成图片
         * 2.将随机数存到session中
         * 3.将生成图片写到接口的响应中
         */
        ImageCode imageCode = (ImageCode) imageCodeGenerator.generate(new ServletWebRequest(request));
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
        ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
    }

    @GetMapping("/code/sms")
    public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
        /**
         * 1.根据随机数生成图片
         * 2.将随机数存到session中
         * 3.调用短信服务:将短信发送到指定平台
         */
        ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,smsCode);
        //3.调用短信服务:将短信发送到指定平台,我们封装成如下接口:
        String mobile = ServletRequestUtils.getRequiredStringParameter(request,"mobile");
         smsCodeSender.send(mobile,smsCode.getCode());
    }
}
1.3 前端页面
代码语言:javascript复制
<h3>短信登录</h3>
<form action="/authentication/mobile" method="post">
    <table>
        <tr>
            <td>手机号:</td>
            <td><input type="text" name="mobile" value="13226595347"></td>
        </tr>
        <tr>
            <td>短信验证码</td>
            <td>
                <input type="text" name="smsCode">
                <a href="/code/sms?mobile=13012345678">发送验证码</a>
            </td>
        </tr>
        <tr>
            <td colspan="2"><button type="submit">登录</button></td>
        </tr>
    </table>
</form>
1.4 添加短信验证码配置类

我们抽取短信验证码如下属性SmsCodeProperties:

代码语言:javascript复制
public class SmsCodeProperties {
    private int length = 6;//长度
    private int expireIn = 60;//过期时间
    private String url;//要处理的url

    //getter setter
}

并且图片验证码和其有很大重复部分,我们用继承关系替代。但是图片验证码默认是4位,而短信验证码是6位,如何处理呢?我们在父类默认:length = 6 但是在图片验证码构造器中:setLength(4);ImageCodeProperties:

代码语言:javascript复制
public class ImageCodeProperties extends SmsCodeProperties{
    private int width = 67;
    private int height = 23;
    public ImageCodeProperties(){
        setLength(4);
    }
}

ValidateCodeProperties配置:

代码语言:javascript复制
public class ValidateCodeProperties {
    private ImageCodeProperties image = new ImageCodeProperties();
    private SmsCodeProperties sms = new SmsCodeProperties();

    //getter setter
}

主要可配置的是长度和过期时间 我们在ValidateCodeProperties加一个配置

1.5 添加短信验证码生成器

短信验证码生成器,我们使用@Component("smsCodeGenerator")注解注入到Spring

图片验证码生成器, @Bean @ConditionalOnMissingBean(name="imageCodeGenerator")注解注入到Spring

代码语言:javascript复制
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {
    private SecurityProperties securityProperties;

    @Override
    public ValidateCode generate(ServletWebRequest request) {
        String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
        return new ValidateCode(code,securityProperties.getCode().getSms().getExpireIn());
    }
    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }
    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }
}
1.6 生成验证码接口:controller
代码语言:javascript复制
@RestController
public class ValidateCodeController {
    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private ValidateCodeGenerator imageCodeGenerator;

    @Autowired
    private ValidateCodeGenerator smsCodeGenerator;

    @Autowired
    private SmsCodeSender smsCodeSender;

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        /**
         * 1.根据随机数生成图片
         * 2.将随机数存到session中
         * 3.将生成图片写到接口的响应中
         */
        ImageCode imageCode = (ImageCode) imageCodeGenerator.generate(new ServletWebRequest(request));
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
        ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
    }

    @GetMapping("/code/sms")
    public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
        /**
         * 1.根据随机数生成短信验证码
         * 2.将随机数存到session中
         * 3.调用短信服务:将短信发送到指定平台
         */
        ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,smsCode);
        //3.调用短信服务:将短信发送到指定平台,我们封装成如下接口:
        String mobile = ServletRequestUtils.getRequiredStringParameter(request,"mobile");
         smsCodeSender.send(mobile,smsCode.getCode());
    }
}

我们观察到:生成图形验证码和生成短信验证码的逻辑是差不多的,都是3步: 1.生成验证码 2.保存验证码到session中 3.将验证码发送出去(一个是发送到response页面前端;一个是发送到客户手机号上面)

像这种主干逻辑相同,其中个别步骤不一样的,我们一般会使用“模板方法模式”将其抽象。

1.7 模板方法模式重构ValidateCodeController中生成验证码
1.7.1 重构完的代码逻辑如下:

image.png

声明一个ValidateCodeProcessor接口,这个接口有一个抽象的实现: AbstractValidateCodeProcessor(之前短信/图片验证码的流程逻辑会写到这里面)

具体的发送是不一样的:一种是请求返回,一种是调用短信运营商返回。这些不同的地方,会让其子类去实现。

注意: 1.ValidateCodeProcessor里面封装了处理整个验证码的生成流程的:包括:a.生成验证码 b.存放session c.发送出去

2.具体的生成逻辑在:ValidateCodeGenerator:他只是封装了:ValidateCodeProcessor接口的一部分。这也是我们设计思想中分层去封装。当业务发生变化时候,根据业务发生变化的力度去实现业务逻辑

1.7.2 AbstractValidateCodeProcessor下generate逻辑
代码语言:javascript复制
/**
 *使用依赖查找模式改造
 * 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。
 *找到之后,把Spring中的bean的名字作为key *然后ValidateCodeGenerator作为value放到map里面去;
 *目前ValidateCodeGenerator的实现有两个:图形验证码实现和短信验证码实现
 */
@Autowired
private Map<String, ValidateCodeGenerator> validateCodeGenerators;
1.7.3 ValidateCodeController
代码语言:javascript复制
@RestController
public class ValidateCodeController {
    @Autowired
    private Map<String, ValidateCodeProcessor> validateCodeProcessors;

    //将以上2个服务变成一个服务
    @GetMapping("/code/{type}")
    public void createCode(@PathVariable String type,HttpServletRequest request, HttpServletResponse response) throws Exception {
        validateCodeProcessors.get(type "CodeProcessor").create(new ServletWebRequest(request,response));
    }
}
1.7.4 WebSecurityConfig

WebSecurityConfig之前授权是针对:"/code/image"现在变成:"/code/*"

代码语言:javascript复制
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private DataSource dataSource;

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

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        //
        //因为是Jdbc操作,所以我们需要注入数据源:org.springframework.jdbc.core.support.JdbcDaoSupport
        //tokenRepository继承org.springframework.jdbc.core.support.JdbcDaoSupport
        tokenRepository.setDataSource(dataSource);
        System.out.println("PersistentTokenRepository--dataSource:>dataSource");
        //tokenRepository.setCreateTableOnStartup(true);//系统启动的时候创建:CREATE_TABLE_SQL表
        return tokenRepository;

    }
    /**
     * 定义web安全配置类:覆盖config方法
     * 1.参数为HttpSecurity
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 定义了任何请求都需要表单认证
         */
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        validateCodeFilter.setSecurityProperties(securityProperties);//传递securityProperties
        validateCodeFilter.afterPropertiesSet();

        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//自定义的额过滤器加到UsernamePasswordAuthenticationFilter前面去
               .formLogin()//表单登录---指定了身份认证方式
                      // .loginPage("/login.html")
                       .loginPage("/authentication/require")
                       .loginProcessingUrl("/authentication/form")//配置UsernamePasswordAuthenticationFilter需要拦截的请求
                       .successHandler(myAuthenticationSuccessHandler)//表单登录成功之后用自带的处理器
                       .failureHandler(myAuthenticationFailureHandler)//表单登录失败之后用自带的处理器
                   // http.httpBasic()//http的basic登录
                      .and()
                .rememberMe()
                      .tokenRepository(persistentTokenRepository())//配置remeberMe的token操作
                      .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())//配置token失效秒数
                      .userDetailsService(userDetailsService)//配置操作数据库用户的service
                      .and()
          .authorizeRequests()//对请求进行授权
                      .antMatchers("/authentication/require",
                              securityProperties.getBrowser().getLoginPage(),
                              "/code/*").permitAll()//对匹配login.html的请求允许访问
                      .anyRequest()//任何请求
                      .authenticated()
                       .and()
           .csrf()
                     .disable();//都需要认证
    }
}

我们重启服务试一下:

image.png

0 人点赞