前言
有时候我们需要有特殊登录形式,比如说短信验证码登录。他与验证码登录逻辑是不一样的,所以不能使用Spring Security默认提供的那套逻辑;需要自个去写一个自定义身份认证逻辑。实现步骤如下:
ValidateCodeController 我们之前已经写了图形验证码了,现在我们在此基础之上重构代码
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
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;
}
}
因为ImageCode继承ValidateCode,所以我们这个接口返回父类,继承、面向接口编程。
public interface ValidateCodeGenerator {
/**
* 生成验证码
* @param request
* @return
*/
ValidateCode generate(ServletWebRequest request);
}
1.定义短信发送接口
public interface SmsCodeSender {
/**
* 给某个手机发送短信验证码
* @param mobile
* @param code
*/
void send(String mobile,String code);
}
2.定义短信接口默认实现类 模拟定义默认接口发送实现类
public class DefaultSmsCodeSender implements SmsCodeSender {
@Override
public void send(String mobile, String code) {
System.out.println("向手机:"+mobile+" 发送短信验证码:"+code);
}
}
3.ValidateCodeBeanConfig里面注入
@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();
}
}
@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());
}
}
<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>
我们抽取短信验证码如下属性SmsCodeProperties:
public class SmsCodeProperties {
private int length = 6;//长度
private int expireIn = 60;//过期时间
private String url;//要处理的url
//getter setter
}
并且图片验证码和其有很大重复部分,我们用继承关系替代。但是图片验证码默认是4位,而短信验证码是6位,如何处理呢?我们在父类默认:length = 6 但是在图片验证码构造器中:setLength(4);ImageCodeProperties:
public class ImageCodeProperties extends SmsCodeProperties{
private int width = 67;
private int height = 23;
public ImageCodeProperties(){
setLength(4);
}
}
ValidateCodeProperties配置:
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
private SmsCodeProperties sms = new SmsCodeProperties();
//getter setter
}
主要可配置的是长度和过期时间 我们在ValidateCodeProperties加一个配置
短信验证码生成器,我们使用@Component("smsCodeGenerator")注解注入到Spring
图片验证码生成器, @Bean @ConditionalOnMissingBean(name="imageCodeGenerator")注解注入到Spring
@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;
}
}
@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页面前端;一个是发送到客户手机号上面)
像这种主干逻辑相同,其中个别步骤不一样的,我们一般会使用“模板方法模式”将其抽象。
image.png
声明一个ValidateCodeProcessor接口,这个接口有一个抽象的实现: AbstractValidateCodeProcessor(之前短信/图片验证码的流程逻辑会写到这里面)
具体的发送是不一样的:一种是请求返回,一种是调用短信运营商返回。这些不同的地方,会让其子类去实现。
注意: 1.ValidateCodeProcessor里面封装了处理整个验证码的生成流程的:包括:a.生成验证码 b.存放session c.发送出去
2.具体的生成逻辑在:ValidateCodeGenerator:他只是封装了:ValidateCodeProcessor接口的一部分。这也是我们设计思想中分层去封装。当业务发生变化时候,根据业务发生变化的力度去实现业务逻辑
/**
*使用依赖查找模式改造
* 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。
*找到之后,把Spring中的bean的名字作为key *然后ValidateCodeGenerator作为value放到map里面去;
*目前ValidateCodeGenerator的实现有两个:图形验证码实现和短信验证码实现
*/
@Autowired
private Map<String, ValidateCodeGenerator> validateCodeGenerators;
@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));
}
}
WebSecurityConfig之前授权是针对:"/code/image"现在变成:"/code/*"
@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