短信登录也是一种常见的登录方式,但是短信登录的方式并没有集成到Spring Security中,所以往往还需要我们自己开发短信登录逻辑,将其集成到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也是一个接口,它有两个实现类,分别是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,代码如下:
CodePropertiespackage 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; }ImageCodePropertiespackage 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; }SmsCodePropertiespackage 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=62)编写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中得到验证,从而实现短信的验证功能。
---来自腾讯云社区的---itlemon
微信扫一扫打赏
支付宝扫一扫打赏