短信驗證碼登入的思路,需要通過驗證碼過濾器,過濾驗證碼是否正确。次過程和圖形驗證碼校驗邏輯完全一樣。 之後,需要通過Spring Security 認真的一套邏輯,來去資料庫查詢使用者資訊,進行 認證資訊
Authentication
的封裝。
此處案例的
Provider
認證校驗類,隻是從資料庫查詢資訊,然後進行封裝。實際開發中可能需求不同,按需求進行更改。
發送驗證碼功能
1、定義驗證碼實體類
@Data
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 ValidateCode(String code, LocalDateTime expireTime) {
this.code = code;
this.expireTime = expireTime;
}
/**
* 判斷驗證碼是否過期
* @return
*/
public boolean isExpire() {
return LocalDateTime.now().isAfter(expireTime);
}
}
2、定義生成驗證碼的接口
/**
* @Author L.jg
* @Title 抽象接口,讓用戶端可配置接口
* @Date 2021/5/24 11:42
*/
public interface ValidateCodeGenerate {
ValidateCode generate(HttpServletRequest request);
}
3、 實作生成短信驗證碼
public class SmsCodeGenerate implements ValidateCodeGenerate {
private SmsProperties smsProperties;
public SmsCodeGenerate(SmsProperties smsProperties) {
this.smsProperties = smsProperties;
}
@Override
public ValidateCode generate(HttpServletRequest request) {
String smsCode = RandomUtil.randomNumbers(smsProperties.getLength());
return new ValidateCode(smsCode, smsProperties.getExpireTime());
}
}
4、将生成短信驗證碼的類加入Bean容器
@Configuration
public class VlidateCodeConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "smsCodeGenerate")
public ValidateCodeGenerate smsCodeGenerate() {
SmsCodeGenerate smsCodeGenerate = new SmsCodeGenerate(securityProperties.getValidateCode().getSms());
return smsCodeGenerate;
}
}
5、發送驗證碼接口
public interface SmsCodeSender {
/**
* 發送驗證碼
*
* @param mobile 手機号
* @param code 驗證碼
*/
void send(String mobile, String code);
}
6、模拟發送驗證碼實作類
@Component
public class DefaultSmsCodeSender implements SmsCodeSender {
@Override
public void send(String mobile, String code) {
System.out.println("手機号:" + mobile + "短信驗證碼:" + code);
}
}
7、 發送驗證碼接口
@GetMapping("/sms/code")
public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
// 生成imageCode
// ImageCode imageCode = createImageCode(request);
ValidateCode smsCode = smsCodeGenerate.generate(request);
// 将imageCode 儲存在session中
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, smsCode);
String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
smsCodeSender.send(mobile, smsCode.getCode());
}
手機驗證碼登入
以Spring Security的form表單為例,是先通過
UsernamePasswordAuthenticationFilter
來擷取使用者的登入資訊。
然後将登入資訊封裝為
UsernamePasswordAuthenticationToken
。
将封裝的 Authentication資訊交給
AuthenticationManager
管理。
根據
Authentication
的類型,調用對應的Provider來處理認證邏輯。
這裡參考
UsernamepasswordAuthenticationFilter
、
UsernamePasswordAuthenticationToken
、
DaoAuthenticationProvider
實作自己的各個類。
1、自定義 SmsCodeAuthenticationToken
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// 手機号
private final Object principal;
public SmsCodeAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object principal) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean authenticated) {
if (authenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
}
}
2、自定義 SmsCodeAuthenticationFilter
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParameter = "mobile";
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String mobile = this.obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
3、自定義 SmsCodeAuthenticationProvider
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getPrincipal().toString();
UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getPrincipal().toString());
if (userDetails == null) {
throw new InternalAuthenticationServiceException("無法通過手機号擷取使用者資訊");
}
SmsCodeAuthenticationToken smsCodeAuthenticationToken = new SmsCodeAuthenticationToken( userDetails.getAuthorities(),userDetails);
smsCodeAuthenticationToken.setDetails(authentication.getDetails());
return smsCodeAuthenticationToken;
}
@Override
public boolean supports(Class<?> aClass) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
4、SmsCodeAuthenticationSecurityConfig 配置類
将自定義的實作邏輯,配置到 Security 裡
/**
* @Author L.jg
* @Title app和浏覽器都需要使用,短信驗證配置
* @Date 2021/5/24 17:35
*/
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity builder) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(loginFailureHandler);
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(loginSuccessHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
builder.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
5、 應用配置
/**
* 短信自定義登入config
*/
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
/**
* 為減少代碼重複開發,多個應用使用同一個認證中心,每個應用需要自己指定登入頁面。
* 這裡需要将 loginpage 指向一個controlelr位址。
* 如果是html頁面,就跳轉到指定的登入頁。
* 如果不是html頁面,就提示401 沒有認證資訊。
* 如果有應用有指定的就使用自己的。如果沒指定就使用本認證子產品預設的登入頁。
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(loginFailureHandler);
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
// 配置過濾器的位置
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
http.apply(smsCodeAuthenticationSecurityConfig);
http.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
// .successForwardUrl("/index")
.defaultSuccessUrl("/index")
.and()
.authorizeRequests()
.antMatchers("/sms/code","/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
這裡缺少了,登入驗證碼校驗功能,可以參考 圖像驗證碼校驗功能,隻要在 添加一個過濾器,自定義校驗即可。