前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringSecurity-短信验证码接口开发

SpringSecurity-短信验证码接口开发

作者头像
好好学java
发布2020-03-20 21:04:06
4.9K0
发布2020-03-20 21:04:06
举报
文章被收录于专栏:好好学java的技术栈

前言

有时候我们需要有特殊登录形式,比如说短信验证码登录。他与验证码登录逻辑是不一样的,所以不能使用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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-03-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 好好学java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 内容
    • 1.开发短信验证码接口
      • 1.1 创建验证码实体
      • 1.2 ValidateCodeGenerator改造
      • 1.3 短信发送封装
      • 1.2 ValidateCodeController短信验证码生成
      • 1.3 前端页面
      • 1.4 添加短信验证码配置类
      • 1.5 添加短信验证码生成器
      • 1.6 生成验证码接口:controller
      • 1.7 模板方法模式重构ValidateCodeController中生成验证码
相关产品与服务
验证码
腾讯云新一代行为验证码(Captcha),基于十道安全栅栏, 为网页、App、小程序开发者打造立体、全面的人机验证。最大程度保护注册登录、活动秒杀、点赞发帖、数据保护等各大场景下业务安全的同时,提供更精细化的用户体验。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档