Spring Security技术栈开发企业级认证与授权(十一)开发短信验证码登录

短信登录也是一种常见的登录方式,但是短信登录的方式并没有集成到Spring Security中,所以往往还需要我们自己开发短信登录逻辑,将其集成到Spring Security中,使用Spring Security来进行校验。本文将介绍开发短信登录的方法,并将其加入到Spring Security的验证逻辑中。

一、短信登录逻辑设计以及图片验证码代码重构

在前面一篇博客《Spring Security技术栈开发企业级认证与授权(九)开发图形验证码接口》中介绍了如何开发图形验证码接口,并将验证逻辑加入到Spring Security中,这里将介绍如何开发短信验证,两者之间有许多非常类似的代码,所以在设计短信登录代码的时候,将它们进一步整合、抽象与重构。
图形验证码和短信验证码重构后的结构图如下所示:
这里写图片描述

  • ValidateCodeController是这个验证码接口体系的入口,它主要抽象出可以同时接收两种验证码的请求方式,使用请求类型type来进行区分。

  • ValidateCodeProcessor是一个接口,专门用来生成验证码,并将验证码存入到session中,最后将验证码发送出去,发送的方式有两种,图片验证码是写回到response中,短信验证码调用第三方短信服务平台的API进行发送,比如阿里巴巴的短信服务。

  • AbstractValidateCodeProcessor是一个抽象类,它实现了ValidateCodeProcessor接口,并提供了抽象方法send方法,因为图片的发送方法和短信的发送方法具体实现不同,所以得使用具体的方法进行发送。这里面的create方法完成了验证码的生成、保存与发送功能。

  • ValidateCodeGenerator也是一个接口,它有两个实现类,分别是ImageCodeGeneratorSmsCodeGenerator,它们具体是完成了代码的生成逻辑。

  • ImageCodeProcessor和SmsCodeProcessor是专门用来重写send方法的一个处理器,展示了两种验证码的不同发送方式。

1)将短信验证码和图形验证码的相同属性进行抽取

短信验证码和图形验证后包含属性有codeexpireTime,短信验证码只有这两个属性,而图形验证码还多一个BufferedImage实例对象属性,所以将共同属性进行抽取,抽取为ValidateCode类,代码如下:

package com.lemon.security.core.validate.code;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * @author lemon
 * @date 2018/4/17 下午8:13
 */
@Data
@AllArgsConstructor
public class ValidateCode {

    private String code;

    private LocalDateTime expireTime;

    public boolean isExpired() {
        return LocalDateTime.now().isAfter(expireTime);
    }
}

抽取后的图片验证码实体类为:

package com.lemon.security.core.validate.code.image;

import com.lemon.security.core.validate.code.ValidateCode;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

/**
 * 图片验证码实体类
 *
 * @author lemon
 * @date 2018/4/6 下午4:34
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class ImageCode extends ValidateCode {

    private BufferedImage image;

    public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
        super(code, expireTime);
        this.image = image;
    }

    public ImageCode(BufferedImage image, String code, int expireIn) {
        super(code, LocalDateTime.now().plusSeconds(expireIn));
        this.image = image;
    }
}

图片验证码实体类继承了ValidateCode类,那么在写一个短信验证码实体类:

扫描二维码关注公众号,回复: 2496229 查看本文章
package com.lemon.security.core.validate.code.sms;

import com.lemon.security.core.validate.code.ValidateCode;

import java.time.LocalDateTime;

/**
 * 短信验证码实体类
 *
 * @author lemon
 * @date 2018/4/17 下午8:18
 */
public class SmsCode extends ValidateCode {

    public SmsCode(String code, LocalDateTime expireTime) {
        super(code, expireTime);
    }

    public SmsCode(String code, int expireIn) {
        super(code, LocalDateTime.now().plusSeconds(expireIn));
    }
}

短信验证码只需要继承ValidateCode即可,没有其他多余的属性增加。
对于配置的代码,也是可以进一步进行重构,短信验证码和图片验证码在配置上有几个重复的属性,比如:验证码长度length,验证码过期时间expireIn,以及需要添加短信验证的url地址。ImageCodePropertiesSmsCodeProperties共同抽取出CodeProperties,代码如下:

  • CodeProperties
package com.lemon.security.core.properties;

import lombok.Data;

/**
 * @author lemon
 * @date 2018/4/17 下午9:11
 */
@Data
public class CodeProperties {

    /**
     * 验证码长度
     */
    private int length = 6;
    /**
     * 验证码过期时间
     */
    private int expireIn = 60;

    /**
     * 需要验证码的url字符串,用英文逗号隔开
     */
    private String url;
}
  • ImageCodeProperties
package com.lemon.security.core.properties;

import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 图形验证码的默认配置
 *
 * @author lemon
 * @date 2018/4/6 下午9:42
 */
@EqualsAndHashCode(callSuper = true)
@Data
public class ImageCodeProperties extends CodeProperties {

    public ImageCodeProperties() {
        setLength(4);
    }

    /**
     * 验证码宽度
     */
    private int width = 67;
    /**
     * 验证码高度
     */
    private int height = 23;
}
  • SmsCodeProperties
package com.lemon.security.core.properties;

/**
 * @author lemon
 * @date 2018/4/17 下午9:13
 */
public class SmsCodeProperties extends CodeProperties {

}

为了实现配置信息可以由用户自定义配置,还需要将其加入到读取配置文件的配置类中,创建一个ValidateCodeProperties类,将图片验证码和短信验证码实例对象作为属性配置进去,代码如下:

package com.lemon.security.core.properties;

import lombok.Data;

/**
 * 封装多个配置的类
 *
 * @author lemon
 * @date 2018/4/6 下午9:45
 */
@Data
public class ValidateCodeProperties {

    private ImageCodeProperties image = new ImageCodeProperties();
    private SmsCodeProperties sms = new SmsCodeProperties();

}

再将ValidateCodeProperties封装到整个安全配置类SecurityProperties中,具体的代码如下:

package com.lemon.security.core.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author lemon
 * @date 2018/4/5 下午3:08
 */
@Data
@ConfigurationProperties(prefix = "com.lemon.security")
public class SecurityProperties {

    private BrowserProperties browser = new BrowserProperties();

    private ValidateCodeProperties code = new ValidateCodeProperties();
}

这个时候就可以读取到用户自定义的配置文件application.properties或者application.yml中的配置。关于验证码的配置方式的application.properties文件内容形式如下,application.yml类似:

com.lemon.security.code.image.length=4
com.lemon.security.code.sms.length=6

2)编写ValidateCodeProcessor接口
ValidateCodeProcessor接口主要是完成了验证码的生成、保存与发送的一整套流程,接口的主要设计如下所示:

package com.lemon.security.core.validate.code;

import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletRequest;

/**
 * 验证码生成接口
 *
 * @author lemon
 * @date 2018/4/17 下午9:46
 */
public interface ValidateCodeProcessor {

    String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";
    String CODE_PROCESSOR = "CodeProcessor";

    /**
     * 生成验证码
     *
     * @param request 封装了 {@link HttpServletRequest} 实例对象的请求
     * @throws Exception 异常
     */
    void create(ServletWebRequest request) throws Exception;
}

由于图片验证码和短信验证码的生成和保存、发送等流程是固定的,只是在生成两种验证码的时候分别调用各自的生成方法,保存到session中是完全一致的,最后的发送各有不同,图片验证码是写到response中,而短信验证码是调用第三方短信发送平台的SDK来实现发送功能。所以这里写一个抽象类来实现ValidateCodeProcessor接口。

package com.lemon.security.core.validate.code.impl;

import com.lemon.security.core.validate.code.ValidateCodeGenerator;
import com.lemon.security.core.validate.code.ValidateCodeProcessor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;

import java.util.Map;

/**
 * @author lemon
 * @date 2018/4/17 下午9:56
 */
@Component
public abstract class AbstractValidateCodeProcessor<C> implements ValidateCodeProcessor {

    private static final String SEPARATOR = "/code/";

    /**
     * 操作session的工具集
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /**
     * 这是Spring的一个特性,就是在项目启动的时候会自动收集系统中 {@link ValidateCodeGenerator} 接口的实现类对象
     */
    @Autowired
    private Map<String, ValidateCodeGenerator> validateCodeGeneratorMap;

    @Override
    public void create(ServletWebRequest request) throws Exception {
        C validateCode = generate(request);
        save(request, validateCode);
        send(request, validateCode);
    }

    /**
     * 生成验证码
     *
     * @param request ServletWebRequest实例对象
     * @return 验证码实例对象
     */
    @SuppressWarnings("unchecked")
    private C generate(ServletWebRequest request) {
        String type = getProcessorType(request);
        ValidateCodeGenerator validateCodeGenerator = validateCodeGeneratorMap.get(type.concat(ValidateCodeGenerator.CODE_GENERATOR));
        return (C) validateCodeGenerator.generate(request);
    }

    /**
     * 保存验证码到session中
     *
     * @param request      ServletWebRequest实例对象
     * @param validateCode 验证码
     */
    private void save(ServletWebRequest request, C validateCode) {
        sessionStrategy.setAttribute(request, SESSION_KEY_PREFIX.concat(getProcessorType(request).toUpperCase()), validateCode);
    }

    /**
     * 发送验证码
     *
     * @param request      ServletWebRequest实例对象
     * @param validateCode 验证码
     * @throws Exception 异常
     */
    protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;

    /**
     * 获取请求URL中具体请求的验证码类型
     *
     * @param request ServletWebRequest实例对象
     * @return 验证码类型
     */
    private String getProcessorType(ServletWebRequest request) {
        // 获取URI分割后的第二个片段
        return StringUtils.substringAfter(request.getRequest().getRequestURI(), SEPARATOR);
    }
}

对上面的代码进行解释:

  • 首先将验证码生成接口ValidateCodeGenerator的实现类对象注入到Map集合中,这个是Spring的一个特性。

  • 抽象类中实现了ValidateCodeProcessor接口的create方法,从代码中可以看出,它主要是完成了验证码的创建、保存和发送的功能。

  • generate方法根据传入的不同泛型而生成了特定的验证码,而泛型的传入是通过AbstractValidateCodeProcessor的子类来实现的。

  • save方法是将生成的验证码实例对象存入到session中,两种验证码的存储方式一致,所以代码也是通用的。

  • send方法一个抽象方法,分别由ImageCodeProcessorSmsCodeProcessor来具体实现,也是根据泛型来判断具体调用哪一个具体的实现类的send方法。

3)编写验证码的生成接口

package com.lemon.security.core.validate.code;

import org.springframework.web.context.request.ServletWebRequest;

/**
 * @author lemon
 * @date 2018/4/7 上午11:06
 */
public interface ValidateCodeGenerator {

    String CODE_GENERATOR = "CodeGenerator";

    /**
     * 生成图片验证码
     *
     * @param request 请求
     * @return ImageCode实例对象
     */
    ValidateCode generate(ServletWebRequest request);
}

它有两个具体的实现,分别是ImageCodeGeneratorSmsCodeGenerator,具体代码如下:

package com.lemon.security.core.validate.code.image;

import com.lemon.security.core.properties.SecurityProperties;
import com.lemon.security.core.validate.code.ValidateCodeGenerator;
import lombok.Data;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

/**
 * 图片验证码生成器
 *
 * @author lemon
 * @date 2018/4/7 上午11:09
 */
@Data
public class ImageCodeGenerator implements ValidateCodeGenerator {

    private static final String IMAGE_WIDTH_NAME = "width";
    private static final String IMAGE_HEIGHT_NAME = "height";
    private static final Integer MAX_COLOR_VALUE = 255;

    private SecurityProperties securityProperties;

    @Override
    public ImageCode generate(ServletWebRequest request) {
        int width = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_WIDTH_NAME, securityProperties.getCode().getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_HEIGHT_NAME, securityProperties.getCode().getImage().getHeight());
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();

        Random random = new Random();

        // 生成画布
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        // 生成数字验证码
        StringBuilder sRand = new StringBuilder();
        for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand.append(rand);
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }

        g.dispose();

        return new ImageCode(image, sRand.toString(), securityProperties.getCode().getImage().getExpireIn());
    }

    /**
     * 生成随机背景条纹
     *
     * @param fc 前景色
     * @param bc 背景色
     * @return RGB颜色
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > MAX_COLOR_VALUE) {
            fc = MAX_COLOR_VALUE;
        }
        if (bc > MAX_COLOR_VALUE) {
            bc = MAX_COLOR_VALUE;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}
package com.lemon.security.core.validate.code.sms;

import com.lemon.security.core.properties.SecurityProperties;
import com.lemon.security.core.validate.code.ValidateCodeGenerator;
import lombok.Data;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;

/**
 * 短信验证码生成器
 *
 * @author lemon
 * @date 2018/4/7 上午11:09
 */
@Data
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {

    private final SecurityProperties securityProperties;

    @Autowired
    public SmsCodeGenerator(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    @Override
    public SmsCode generate(ServletWebRequest request) {
        String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
        return new SmsCode(code, securityProperties.getCode().getSms().getExpireIn());
    }
}

两个实现类完成了具体的验证码生成逻辑,根据传入的泛型然后进行强转之后便可调用各自的生成逻辑方法。

4)编写验证码的发送逻辑类
不同的验证码的发送逻辑是不一样的,图片验证码是写回response中,而短信验证码是将验证码发送到指定手机号的手机上。
图片验证码的发送逻辑类的代码如下:

package com.lemon.security.core.validate.code.image;

import com.lemon.security.core.validate.code.impl.AbstractValidateCodeProcessor;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;

/**
 * @author lemon
 * @date 2018/4/17 下午11:37
 */
@Component("imageCodeProcessor")
public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImageCode> {

    private static final String FORMAT_NAME = "JPEG";

    /**
     * 发送图形验证码,将其写到相应中
     *
     * @param request   ServletWebRequest实例对象
     * @param imageCode 验证码
     * @throws Exception 异常
     */
    @Override
    protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
        ImageIO.write(imageCode.getImage(), FORMAT_NAME, request.getResponse().getOutputStream());
    }
}

短信验证码的发送逻辑类的代码如下:

package com.lemon.security.core.validate.code.sms;

import com.lemon.security.core.validate.code.impl.AbstractValidateCodeProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;

/**
 * @author lemon
 * @date 2018/4/17 下午11:41
 */
@Component("smsCodeProcessor")
public class SmsCodeProcessor extends AbstractValidateCodeProcessor<SmsCode> {

    private static final String SMS_CODE_PARAM_NAME = "mobile";

    private final SmsCodeSender smsCodeSender;

    @Autowired
    public SmsCodeProcessor(SmsCodeSender smsCodeSender) {
        this.smsCodeSender = smsCodeSender;
    }

    @Override
    protected void send(ServletWebRequest request, SmsCode smsCode) throws Exception {
        String mobile = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), SMS_CODE_PARAM_NAME);
        smsCodeSender.send(mobile, smsCode.getCode());
    }
}

注意到上面的短信发送调用了SmsCodeSender的实现类,因此和图片的发送有所区别。而在设计中,SmsCodeSender有一个默认的实现,也就是自带的短信发送方式,但是在实际的开发过程中,往往需要开发者覆盖自带的发送逻辑,而是采用自定义的发送逻辑,所以需要默认的短信发送方式是可以被覆盖的。SmsCodeSender接口代码如下:

package com.lemon.security.core.validate.code.sms;

/**
 * 短信验证发送接口
 *
 * @author lemon
 * @date 2018/4/17 下午8:25
 */
public interface SmsCodeSender {

    /**
     * 短信验证码发送接口
     *
     * @param mobile 手机号
     * @param code   验证码
     */
    void send(String mobile, String code);
}

它的默认实现类代码啊如下:

package com.lemon.security.core.validate.code.sms;

/**
 * 默认的短信发送逻辑
 *
 * @author lemon
 * @date 2018/4/17 下午8:26
 */
public class DefaultSmsCodeSender implements SmsCodeSender {

    @Override
    public void send(String mobile, String code) {
        // 这里仅仅写个打印,具体逻辑一般都是调用第三方接口发送短信
        System.out.println("向手机号为:" + mobile + "的用户发送验证码:" + code);
    }
}

注意到上面的代码并没有使用@Component注解来标注为一个SpringBean,这么做不是说它不由Spring管理,而是需要配置的可以被覆盖的形式,所以在ValidateCodeBeanConfig类中加上配置其为Spring Bean的代码,为了体现代码的完整性,这里贴出ValidateCodeBeanConfig类中的所有代码。

package com.lemon.security.core.validate.code;

import com.lemon.security.core.properties.SecurityProperties;
import com.lemon.security.core.validate.code.image.ImageCodeGenerator;
import com.lemon.security.core.validate.code.sms.DefaultSmsCodeSender;
import com.lemon.security.core.validate.code.sms.SmsCodeSender;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author lemon
 * @date 2018/4/7 上午11:22
 */
@Configuration
public class ValidateCodeBeanConfig {

    private final SecurityProperties securityProperties;

    @Autowired
    public ValidateCodeBeanConfig(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    @Bean
    @ConditionalOnMissingBean(name = "imageCodeGenerator")
    public ValidateCodeGenerator imageCodeGenerator() {
        ImageCodeGenerator imageCodeGenerator = new ImageCodeGenerator();
        imageCodeGenerator.setSecurityProperties(securityProperties);
        return imageCodeGenerator;
    }

    @Bean
    @ConditionalOnMissingBean(SmsCodeSender.class)
    public SmsCodeSender smsCodeSender() {
        return new DefaultSmsCodeSender();
    }
}

在最后一个Bean的配置中,使用了@ConditionalOnMissingBean注解,这里是告诉Spring,如果上下文环境中没有SmsCodeSender接口的实现类对象,那么就执行下面的方法进行默认的Bean创建。所以对于用户自定义方式,只需要写一个类实现SmsCodeSender接口,并将其标注为SpringBean即可,就可以覆盖自带的短信发送逻辑。如果一开始使用@Component注解来进行标注了,那就无法获得这样自定义的效果。

至此,我们已经完成了对文章开始处的逻辑分析的所有代码,接下来将代码整合到Spring Security中,让其能在Spring Security中得到验证,从而实现短信的验证功能。

二、将短信验证码登录方式集成到Spring Security中

猜你喜欢

转载自blog.csdn.net/lammonpeter/article/details/79982924