短信驗證碼登錄
public class ValidateCode {
private String code;
//有效期
private LocalDateTime expireTime;
public ValidateCode(String code, int expireTime) {
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(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;
}
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
}
/**
* 短信發送接口
*/
public interface SmsCodeSender {
void send(String mobile, String code);
}
public class DefaultSmsCodeSender implements SmsCodeSender {
@Override
public void send(String mobile, String code) {
System.out.println("向手機"+mobile+"發送短信驗證碼"+code);
}
}
@Component("smsValidateCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {
@Autowired
private SecurityProperties securityProperties;
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
@Override
public ValidateCode generate(HttpServletRequest request) {
String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSmsCode().getLength());
return new ValidateCode(code, securityProperties.getCode().getSmsCode().getExpireIn());
}
}
只有在用戶沒有實現smsCodeSender時才會使用默認實現
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "smsCodeSender")
public SmsCodeSender smsCodeSender(){
return new DefaultSmsCodeSender();
}
}
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY_SMS = "smscode";
@Autowired
@Qualifier("smsValidateCodeGenerator")
private ValidateCodeGenerator smsValidateCodeGenerator;
@Autowired
private SmsCodeSender smsCodeSender;
@GetMapping("/code/sms")
public void getsmsCaptcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
ValidateCode smsCode = smsValidateCodeGenerator.generate(request);
request.getSession().setAttribute(SESSION_KEY_SMS,smsCode);
smsCodeSender.send(ServletRequestUtils.getRequiredStringParameter(request,"mobile"),smsCode.getCode());
}
}
現在已經有了兩種驗證方式,接下來我們進行代碼重構
用到一個session的操作工具SessionStrategy,需要引入依賴
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-web</artifactId>
<version>1.1.6.RELEASE</version>
</dependency>
/**
* 校驗碼處理器,封裝不同校驗碼的處理邏輯
*/
public interface ValidateCodeProcessor {
//驗證碼放入session時的前綴
String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";
//創建校驗碼 ServletWebRequest 已經包含request response
void create(ServletWebRequest request) throws Exception;
}
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 收集系統中所有的 {@link ValidateCodeGenerator} 接口的實現。
*/
@Autowired
private Map<String, ValidateCodeGenerator> validateCodeGenerators;
@Override
public void create(ServletWebRequest request) throws Exception {
C validateCode = generate(request);
save(request,validateCode);
send(request,validateCode);
}
/**
* 保存驗證碼到session
* @param request
* @param validateCode
*/
private void save(ServletWebRequest request, C validateCode){
sessionStrategy.setAttribute(request,getSessionKey(request),validateCode);
}
private String getSessionKey(ServletWebRequest request){
return SESSION_KEY_PREFIX + getProcessorType(request).toUpperCase();
}
/**
* 發送驗證碼有子類實現
* @param request
* @param validateCode
* @throws Exception
*/
protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;
//生成驗證碼
@SuppressWarnings("unchecked")
private C generate(ServletWebRequest request) {
String type = getProcessorType(request);
ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(type + "CodeGenerator");
return (C) validateCodeGenerator.generate(request);
}
//根據請求的url獲取校驗碼的類型
private String getProcessorType(ServletWebRequest request){
return StringUtils.substringAfter(request.getRequest().getRequestURI(),"/code/");
}
}
public class ValidateCode {
private String code;
//有效期
private LocalDateTime expireTime;
public ValidateCode(String code, int expireTime) {
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(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;
}
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
}
public class ImageCode extends ValidateCode {
private BufferedImage image;
public ImageCode(String code, int expireTime,BufferedImage image) {
super(code,expireTime);
this.image = image;
}
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
}
@Component("imageCodeProcessor")
public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImageCode> {
@Override
protected void send(ServletWebRequest request, ImageCode validateCode) throws Exception {
ServletOutputStream out = request.getResponse().getOutputStream();
ImageIO.write(validateCode.getImage(),"jpg",out);
}
}
短信的
@Component("smsCodeProcessor")
public class SmsCodeProcessor extends AbstractValidateCodeProcessor<ValidateCode> {
@Autowired
private SmsCodeSender smsCodeSender;
@Override
protected void send(ServletWebRequest request, ValidateCode validateCode) throws Exception {
String mobile = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), "mobile");
smsCodeSender.send(mobile, validateCode.getCode());
}
}
默認bean配置
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")
public ValidateCodeGenerator imageCodeGenerator(){
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
@Bean
@ConditionalOnMissingBean(name = "smsCodeSender")
public SmsCodeSender smsCodeSender(){
return new DefaultSmsCodeSender();
}
}
修改controller
@RestController
public class ValidateCodeController {
@Autowired
private Map<String, ValidateCodeProcessor> validateCodeProcessors;
@GetMapping("/code/{type}")
public void getCaptcha(@PathVariable("type") String type, HttpServletRequest request, HttpServletResponse response) throws Exception {
validateCodeProcessors.get(type+"CodeProcessor").create(new ServletWebRequest(request,response));
}
}
/**
* 參考 {@link UsernamePasswordAuthenticationToken}
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
public SmsAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
public SmsAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public 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);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
public class SmsAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken smsAuthenticationToken = (SmsAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) smsAuthenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("無法獲取用戶信息");
}
SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(smsAuthenticationToken.getDetails());
return authenticationResult;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
/**
* 參考{@link UsernamePasswordAuthenticationFilter}
*/
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
public SmsAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST")); //這個地址就是登錄的地址
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
//獲取手機號
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request,
SmsAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
}
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
public void configure(HttpSecurity builder) throws Exception {
SmsAuthenticationFilter filter = new SmsAuthenticationFilter();
filter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
SmsAuthenticationProvider provider = new SmsAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
builder.authenticationProvider(provider)
.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
}
}
public class SmsValidateCodeFilter extends OncePerRequestFilter {
public SmsValidateCodeFilter(AuthenticationFailureHandler flyAuthenticationFailureHandler) {
this.flyAuthenticationFailureHandler = flyAuthenticationFailureHandler;
}
private AuthenticationFailureHandler flyAuthenticationFailureHandler;
public AuthenticationFailureHandler getFlyAuthenticationFailureHandler() {
return flyAuthenticationFailureHandler;
}
public void setFlyAuthenticationFailureHandler(AuthenticationFailureHandler flyAuthenticationFailureHandler) {
this.flyAuthenticationFailureHandler = flyAuthenticationFailureHandler;
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
if ("/authentication/mobile".equals(httpServletRequest.getRequestURI())&&"post".equalsIgnoreCase(httpServletRequest.getMethod())){
try {
validate(httpServletRequest);
}catch (VerificationCodeException e){
flyAuthenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
return;
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
private void validate(HttpServletRequest request) {
HttpSession session = request.getSession();
ValidateCode codeInSession = (ValidateCode) session.getAttribute("SESSION_KEY_FOR_CODE_SMS");
String smsCode = request.getParameter("smsCode");
if (StringUtils.isEmpty(smsCode)){
throw new VerificationCodeException("驗證碼的值不能為空");
}
if (codeInSession==null){
throw new VerificationCodeException("驗證碼不存在");
}
if (codeInSession.isExpried()){
session.removeAttribute("SESSION_KEY_FOR_CODE_SMS");
throw new VerificationCodeException("驗證碼已過期");
}
if (!smsCode.equals(codeInSession.getCode())){
throw new VerificationCodeException("驗證碼不匹配");
}
session.removeAttribute("SESSION_KEY_FOR_CODE_SMS");
}
}
修改WebSecurityConfig加入ValidateCodeFilter與smsCodeAuthenticationSecurityConfig
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler flyAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler flyAuthenticationFailureHandler;
@Autowired
private SecurityProperties securityProperties;
@Autowired
private PasswordEncoder passwordEncoder;
@Bean
public PasswordEncoder setPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private DataSource dataSource;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
// tokenRepository.setCreateTableOnStartup(true);
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter codeFilter = new ValidateCodeFilter(flyAuthenticationFailureHandler);
SmsValidateCodeFilter smsValidateCodeFilter = new SmsValidateCodeFilter(flyAuthenticationFailureHandler);
http
.addFilterBefore(codeFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(smsValidateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/request")
.loginProcessingUrl("/authentication/form")
.successHandler(flyAuthenticationSuccessHandler)
.failureHandler(flyAuthenticationFailureHandler)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMe())
.userDetailsService(userDetails())
.and()
.authorizeRequests()
.antMatchers("/authentication/request",
securityProperties.getBrowser().getLoginPage(),
"/code/*")
.permitAll()
.anyRequest().authenticated()
.and().csrf().disable()
.apply(smsCodeAuthenticationSecurityConfig);
}
@Bean
public UserDetailsService userDetails(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("user").password(passwordEncoder.encode("123")).roles("USER").build());
manager.createUser(User.withUsername("13312345678").password(passwordEncoder.encode("123")).roles("USER").build());
return manager;
}
}