文章目录
- 在开发短信验证码接口
- 验证码实体SmsCode
- 手机验证码发送接口
- 手机验证码生成接口
- 不拦截短信验证码路径
- 登录页面
- 测试
- 重构
- 校验码处理接口 ValidateCodeProcessor,封装不同校验码的处理逻辑
- 抽象实现 AbstractValidateCodeProcessor
- 两个子类分别实现发送功能 ImageCodeProcessor SmsCodeProcessor
- ValidateCodeController 简化
- 短信登录开发
- SmsCodeAuthenticationToken
- SmsCodeAuthenticationProvider
- SmsCodeAuthenticationFilter
- SmsCodeAuthenticationSecurityConfig
- SmsCodeFilter
- BrowserSecurityConfig
- 测试
- 重构
在开发短信验证码接口
在之前图片验证码的基础上开发短信验证码
验证码实体SmsCode
-
短信验证码和图片验证码就差一个图片属性,直接把ImageCode拿来改成SmsCode并去掉 private BufferedImage image; 属性就行
-
因此可以使ImageCode继承SmsCode
-
最后把SmsCode名字改为ValidateCode比较好
-
验证码生成器ValidateCodeGenerator的返回值从ImageCode改为ValidateCode
手机验证码发送接口
每个人的短信验证码发送服务商都不同,应该让他们自己实现
SmsCodeSender
public interface SmsCodeSender {void send(String mobile,String code);}
SmsCodeSender默认实现DefaultSmsCodeSender
public class DefaultSmsCodeSender implements SmsCodeSender {@Overridepublic void send(String mobile, String code) {System.out.println("向手机发送短信验证码"+mobile+"---"+code);}
}
默认实现应该是让使用者覆盖掉的 同图片验证码生成器接口一样
配置如下
ValidateCodeBeanConfig
@Configuration
public class ValidateCodeBeanConfig {@Autowiredprivate SecurityProperties securityProperties;@Bean@ConditionalOnMissingBean(name = "imageCodeGenerator")public ValidateCodeGenerator imageCodeGenerator() { //方法的名字就是spring容器中bean的名字ImageCodeGenerator codeGenerator = new ImageCodeGenerator();codeGenerator.setSecurityProperties(securityProperties);return codeGenerator;}@Bean
// @ConditionalOnMissingBean(name = "smsCodeSender")@ConditionalOnMissingBean(SmsCodeSender.class)//当容器中找到了SmsCodeSender的实现就不会再用此实现beanpublic SmsCodeSender smsCodeSender() { //方法的名字就是spring容器中bean的名字return new DefaultSmsCodeSender();}}
手机验证码生成接口
类似于图像验证码生成接口
ValidateCodeController
@Autowiredprivate ValidateCodeGenerator smsCodeGenerator;//注入手机验证码创建接口@Autowiredprivate SmsCodeSender smsCodeSender; //注入手机验证码发送器@RequestMapping("/code/sms")private void createSms(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {//1根据请求中的随机数生成图片ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request));//2将随机数放到session中sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,smsCode);//3、这块应该由短信服务商将我们的短信发送出去,我们需要封装一个短信验证码发送的接口String mobile = ServletRequestUtils.getRequiredStringParameter(request,"mobile");smsCodeSender.send(mobile,smsCode.getCode());}
SmsCodeGenerator
手机验证码的长度和过期时间做成可配置的属性,具体方法和图片验证码的创建相似,这里就不介绍了
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {@Autowiredprivate SecurityProperties securityProperties;/** (non-Javadoc)* * @see* com.imooc.security.core.validate.code.ValidateCodeGenerator#generate(org.* springframework.web.context.request.ServletWebRequest)*/@Overridepublic 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;}}
不拦截短信验证码路径
BrowserSecurityConfig
···························.and().authorizeRequests() //对请求授权
// .antMatchers("/signIn.html","/code/image").permitAll() //加一个匹配器 对匹配的路径不进行身份认证.antMatchers(securityProperties.getBrowser().getLoginPage(),"/code/*").permitAll() //加一个匹配器 对匹配的路径不进行身份认证···························
登录页面
<h3>短信登录</h3>
<form action="/authentication/mobile" method="post"><table><tr><td>手机号:</td><td><input type="text" name="mobile" value="13012345678"></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>
测试
重构
现在我们的验证码生成器有两个实现的接口
@Autowired
private ValidateCodeGenerator imageCodeGenerator;@Autowired
private ValidateCodeGenerator smsCodeGenerator;
使用模板方法重构,重构后的结构如下
校验码处理接口 ValidateCodeProcessor,封装不同校验码的处理逻辑
/*** 校验码处理器,封装不同校验码的处理逻辑* * @author zhailiang**/
public interface ValidateCodeProcessor {/*** 验证码放入session时的前缀*/String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";/*** 创建校验码* * @param request* @throws Exception* ServletWebRequest spring提供的一个工具类 可以封装请求和响应*/void create(ServletWebRequest request) throws Exception;}
抽象实现 AbstractValidateCodeProcessor
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {/*** 操作session的工具类*/private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();/*** 收集系统中所有的 {@link ValidateCodeGenerator} 接口的实现。** 这个map的注入* spring启动的时候,会查找map的value接口ValidateCodeGenerator的所有实现bean,* 并把这个bean为value,bean的名称为key存入map中** 这种行为叫依赖搜索*/@Autowiredprivate Map<String, ValidateCodeGenerator> validateCodeGenerators;/** (non-Javadoc)* * @see* com.whale.security.core.validate.code.ValidateCodeProcessor#create(org.* springframework.web.context.request.ServletWebRequest)*/@Overridepublic void create(ServletWebRequest request) throws Exception {C validateCode = generate(request);//生成save(request, validateCode);//保存send(request, validateCode);//发送 这是一个抽象方法 需要子类去实现}/*** 生成校验码* * @param request* @return*/@SuppressWarnings("unchecked")private C generate(ServletWebRequest request) {String type = getProcessorType(request);String generatorName = type + "CodeGenerator";ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(generatorName);if (validateCodeGenerator == null) {throw new ValidateCodeException("验证码生成器" + generatorName + "不存在");}return (C) validateCodeGenerator.generate(request);}/*** 保存校验码** @param request* @param validateCode*/private void save(ServletWebRequest request, C validateCode) {sessionStrategy.setAttribute(request, getSessionKey(request), validateCode);}/*** 构建验证码放入session时的key** @param request* @return*/private String getSessionKey(ServletWebRequest request) {return SESSION_KEY_PREFIX + getProcessorType(request);}/*** 发送校验码,由子类实现* 它的抽象方法 send由具体的子类实现* * @param request* @param validateCode* @throws Exception*/protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;/*** 根据请求的url获取校验码的类型* @param request* @return*/private String getProcessorType(ServletWebRequest request){String type = StringUtils.substringAfter(request.getRequest().getRequestURI(), "/code/");return type;}
}
两个子类分别实现发送功能 ImageCodeProcessor SmsCodeProcessor
ImageCodeProcessor
@Component("imageCodeProcessor")
public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImageCode> {/*** 发送图形验证码,将其写到响应中*/@Overrideprotected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {ImageIO.write(imageCode.getImage(), "JPEG", request.getResponse().getOutputStream());}}
SmsCodeProcessor
@Component("smsCodeProcessor")
public class SmsCodeProcessor extends AbstractValidateCodeProcessor<ValidateCode> {/*** 短信验证码发送器*/@Autowiredprivate SmsCodeSender smsCodeSender;@Overrideprotected void send(ServletWebRequest request, ValidateCode validateCode) throws Exception {
// String mobile= ServletRequestUtils.getStringParameter((ServletRequest) request, "mobile");
// String mobile= ServletRequestUtils.getRequiredStringParameter((ServletRequest)request,"mobile");String mobile= ServletRequestUtils.getRequiredStringParameter(request.getRequest(),"mobile");smsCodeSender.send(mobile, validateCode.getCode());}}
ValidateCodeController 简化
@RestController
public class ValidateCodeController implements Serializable {public static final String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key@Autowiredprivate Map<String, ValidateCodeProcessor> validateCodeProcessors;/*** 创建验证码,根据验证码类型不同,调用不同的 {@link ValidateCodeProcessor}接口实现** @param request* @param response* @param type* @throws Exception*/@GetMapping("/code/{type}")public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type)throws Exception {validateCodeProcessors.get(type+"CodeProcessor").create(new ServletWebRequest(request,response));}}
短信登录开发
SmsCodeAuthenticationToken
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;// ~ Instance fields// ================================================================================================private final Object principal;// ~ Constructors// ===================================================================================================/*** This constructor can be safely used by any code that wishes to create a* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}* will return <code>false</code>.**/public SmsCodeAuthenticationToken(String mobile) {super(null);this.principal = mobile;setAuthenticated(false);}/*** This constructor should only be used by <code>AuthenticationManager</code> or* <code>AuthenticationProvider</code> implementations that are satisfied with* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)* authentication token.** @param principal* @param* @param authorities*/public SmsCodeAuthenticationToken(Object principal,Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true); // must use super, as we override}// ~ Methods// ========================================================================================================@Overridepublic Object getCredentials() {return null;}@Overridepublic Object getPrincipal() {return this.principal;}@Overridepublic void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {if (isAuthenticated) {throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");}super.setAuthenticated(false);}@Overridepublic void eraseCredentials() {super.eraseCredentials();}
}
SmsCodeAuthenticationProvider
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {private UserDetailsService userDetailsService;/** (non-Javadoc)* * @see org.springframework.security.authentication.AuthenticationProvider#* authenticate(org.springframework.security.core.Authentication)*/@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;String principal = (String) authenticationToken.getPrincipal();//token中的手机号UserDetails user = userDetailsService.loadUserByUsername(principal);//根据手机号拿到对应的UserDetailsif (user == null) {throw new InternalAuthenticationServiceException("无法获取用户信息");}SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());authenticationResult.setDetails(authenticationToken.getDetails());return authenticationResult;}/** (non-Javadoc)* * @see org.springframework.security.authentication.AuthenticationProvider#* supports(java.lang.Class)** AuthenticationManager 判断参数authentication是不是对应的token*/@Overridepublic boolean supports(Class<?> authentication) {return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);}public UserDetailsService getUserDetailsService() {return userDetailsService;}public void setUserDetailsService(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}}
SmsCodeAuthenticationFilter
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {// ~ Static fields/initializers// =====================================================================================private static String WHALE_FROM_MOBILE_KEY="mobile";private String mobileParameter = WHALE_FROM_MOBILE_KEY;private boolean postOnly = true;//当前处理器是否处理post请求// ~ Constructors// ===================================================================================================/*** 当前过滤器处理的请求是什么* 一个路径匹配器 手机表单登录的一个请求*/public SmsCodeAuthenticationFilter() {super(new AntPathRequestMatcher("/authentication/mobile", "POST"));}// ~ Methods// ========================================================================================================@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throws AuthenticationException {if (postOnly && !request.getMethod().equals("POST")) {//当前请求如果不是post请求则抛出异常throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String mobile = obtainMobile(request);//从请求中获取mobile参数if (mobile == null) {mobile = "";}mobile = mobile.trim();//实例化tokenSmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);// Allow subclasses to set the "details" propertysetDetails(request, authRequest);//使用AuthenticationManager 调用 tokenreturn this.getAuthenticationManager().authenticate(authRequest);}/*** 获取手机号*/protected String obtainMobile(HttpServletRequest request) {return request.getParameter(mobileParameter);}/*** Provided so that subclasses may configure what is put into the* authentication request's details property.** @param request* that an authentication request is being created for* @param authRequest* the authentication request object that should have its details* set*/protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {authRequest.setDetails(authenticationDetailsSource.buildDetails(request));}/*** Sets the parameter name which will be used to obtain the username from* the login request.** @param usernameParameter* the parameter name. Defaults to "username".*/public void setMobileParameter(String usernameParameter) {Assert.hasText(usernameParameter, "Username parameter must not be empty or null");this.mobileParameter = usernameParameter;}/*** Defines whether only HTTP POST requests will be allowed by this filter.* If set to true, and an authentication request is received which is not a* POST request, an exception will be raised immediately and authentication* will not be attempted. The <tt>unsuccessfulAuthentication()</tt> method* will be called as if handling a failed authentication.* <p>* Defaults to <tt>true</tt> but may be overridden by subclasses.*/public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}public final String getMobileParameter() {return mobileParameter;}}
SmsCodeAuthenticationSecurityConfig
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@Autowiredprivate AuthenticationSuccessHandler whaleAuthenticationSuccessHandler;@Autowiredprivate AuthenticationFailureHandler whaleAuthenticationFailureHandler;@Qualifier("myUserDetailsService")@Autowiredprivate UserDetailsService userDetailsService;@Overridepublic void configure(HttpSecurity http) throws Exception {SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(whaleAuthenticationSuccessHandler);smsCodeAuthenticationFilter.setAuthenticationFailureHandler(whaleAuthenticationFailureHandler);SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);http.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);}}
SmsCodeFilter
同ImageCodeFilter
校验短信验证码并登录
重构代码
BrowserSecurityConfig
同图片验证码过滤器一样
添加短信验证码过滤器
添加配置
测试
ok
重构
1两个验证码的过滤器合并为一个
2config配置整合
3重复字符串整合SecurityConstants
项目地址
https://github.com/whaleluo/securitydemo.git