短信登录
也是一种常见的登录方式,但是短信登录
的方式并没有集成到Spring Security
中,所以往往还需要我们自己开发短信登录
逻辑,将其集成到Spring Security
中,使用Spring Security
来进行校验。本文将介绍开发短信登录
的方法,并将其加入到Spring Security
的验证逻辑中。
一、短信登录逻辑设计以及图片验证码代码重构
在前面一篇博客《Spring Security技术栈开发企业级认证与授权(九)开发图形验证码接口》中介绍了如何开发图形验证码接口,并将验证逻辑加入到Spring Security
中,这里将介绍如何开发短信验证,两者之间有许多非常类似的代码,所以在设计短信登录代码的时候,将它们进一步整合、抽象与重构。
图形验证码和短信验证码重构后的结构图如下所示:
ValidateCodeController
是这个验证码接口体系的入口,它主要抽象出可以同时接收两种验证码的请求方式,使用请求类型type
来进行区分。ValidateCodeProcessor
是一个接口,专门用来生成验证码,并将验证码存入到session
中,最后将验证码发送出去,发送的方式有两种,图片验证码是写回到response
中,短信验证码调用第三方短信服务平台的API
进行发送,比如阿里巴巴的短信服务。AbstractValidateCodeProcessor
是一个抽象类,它实现了ValidateCodeProcessor
接口,并提供了抽象方法send
方法,因为图片的发送方法和短信的发送方法具体实现不同,所以得使用具体的方法进行发送。这里面的create
方法完成了验证码的生成、保存与发送功能。ValidateCodeGenerator
也是一个接口,它有两个实现类,分别是ImageCodeGenerator
和SmsCodeGenerator
,它们具体是完成了代码的生成逻辑。ImageCodeProcessor和SmsCodeProcessor
是专门用来重写send
方法的一个处理器,展示了两种验证码的不同发送方式。
1)将短信验证码和图形验证码的相同属性进行抽取
短信验证码和图形验证后包含属性有code
和expireTime
,短信验证码只有这两个属性,而图形验证码还多一个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
类,那么在写一个短信验证码实体类:
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地址。ImageCodeProperties
和SmsCodeProperties
共同抽取出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
方法一个抽象方法,分别由ImageCodeProcessor
和SmsCodeProcessor
来具体实现,也是根据泛型来判断具体调用哪一个具体的实现类的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);
}
它有两个具体的实现,分别是ImageCodeGenerator
和SmsCodeGenerator
,具体代码如下:
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
注解来标注为一个Spring
的Bean
,这么做不是说它不由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
接口,并将其标注为Spring
的Bean
即可,就可以覆盖自带的短信发送逻辑。如果一开始使用@Component
注解来进行标注了,那就无法获得这样自定义的效果。
至此,我们已经完成了对文章开始处的逻辑分析的所有代码,接下来将代码整合到Spring Security
中,让其能在Spring Security
中得到验证,从而实现短信的验证功能。