Spring Security 實(shí)現(xiàn)短信驗(yàn)證碼登錄功能
之前文章都是基于用戶名密碼登錄,第六章圖形驗(yàn)證碼登錄其實(shí)還是用戶名密碼登錄,只不過多了一層圖形驗(yàn)證碼校驗(yàn)而已;Spring Security默認(rèn)提供的認(rèn)證流程就是用戶名密碼登錄,整個(gè)流程都已經(jīng)固定了,雖然提供了一些接口擴(kuò)展,但是有些時(shí)候我們就需要有自己特殊的身份認(rèn)證邏輯,比如用短信驗(yàn)證碼登錄,它和用戶名密碼登錄的邏輯是不一樣的,這時(shí)候就需要重新寫一套身份認(rèn)證邏輯。
開發(fā)短信驗(yàn)證碼接口
獲取驗(yàn)證碼
短信驗(yàn)證碼的發(fā)送獲取邏輯和圖片驗(yàn)證碼類似,這里直接貼出代碼。
@GetMapping("/code/sms")
public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 創(chuàng)建驗(yàn)證碼
ValidateCode smsCode = createCodeSmsCode(request);
// 將驗(yàn)證碼放到session中
sessionStrategy.setAttribute(new ServletWebRequest(request), SMS_CODE_SESSION_KEY, smsCode);
String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
// 發(fā)送驗(yàn)證碼
smsCodeSender.send(mobile, smsCode.getCode());
}
前端代碼
<tr> <td>手機(jī)號:</td> <td><input type="text" name="mobile" value="13012345678"></td> </tr> <tr> <td>短信驗(yàn)證碼:</td> <td> <input type="text" name="smsCode"> <a href="/code/sms?mobile=13012345678" rel="external nofollow" >發(fā)送驗(yàn)證碼</a> </td> </tr>
短信驗(yàn)證碼流程原理
短信驗(yàn)證碼登錄和用戶名密碼登錄對比

步驟流程
- 首先點(diǎn)擊登錄應(yīng)該會被
SmsAuthenticationFilter過濾器處理,這個(gè)過濾器拿到請求以后會在登錄請求中拿到手機(jī)號,然后封裝成自定義的一個(gè)SmsAuthenticationToken(未認(rèn)證)。 - 這個(gè)Token也會傳給AuthenticationManager,因?yàn)?code>AuthenticationManager整個(gè)系統(tǒng)只有一個(gè),它會檢索系統(tǒng)中所有的AuthenticationProvider,這時(shí)候我們要提供自己的
SmsAuthenticationProvider,用它來校驗(yàn)自己寫的SmsAuthenticationToken的手機(jī)號信息。 - 在校驗(yàn)的過程中同樣會調(diào)用
UserDetailsService,把手機(jī)號傳給它讓它去讀用戶信息,去判斷是否能登錄,登錄成功的話再把SmsAuthenticationToken標(biāo)記為已認(rèn)證。 - 到這里為止就是短信驗(yàn)證碼的認(rèn)證流程,上面的流程并沒有提到校驗(yàn)驗(yàn)證碼信息,其實(shí)它的驗(yàn)證流程和圖形驗(yàn)證碼驗(yàn)證流程也是類似,同樣是
在SmsAuthenticationFilter過濾器之前加一個(gè)過濾器來驗(yàn)證短信驗(yàn)證碼。
代碼實(shí)現(xiàn)
SmsCodeAuthenticationToken
- 作用:封裝認(rèn)證Token
- 實(shí)現(xiàn):可以繼承AbstractAuthenticationToken抽象類,該類實(shí)現(xiàn)了Authentication接口
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
/**
* 進(jìn)入SmsAuthenticationFilter時(shí),構(gòu)建一個(gè)未認(rèn)證的Token
*
* @param mobile
*/
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
/**
* 認(rèn)證成功以后構(gòu)建為已認(rèn)證的Token
*
* @param principal
* @param authorities
*/
public SmsCodeAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
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 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();
}
}
SmsCodeAuthenticationFilter
- 作用:處理短信登錄的請求,構(gòu)建Token,把請求信息設(shè)置到Token中。
- 實(shí)現(xiàn):該類可以模仿UsernamePasswordAuthenticationFilter類,繼承AbstractAuthenticationProcessingFilter抽象類
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParameter = "mobile";
private boolean postOnly = true;
/**
* 表示要處理的請求路徑
*/
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
@Override
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();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
// 把請求信息設(shè)到Token中
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
/**
* 獲取手機(jī)號
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.mobileParameter = usernameParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
}
SmsAuthenticationProvider
- 作用:提供認(rèn)證Token的校驗(yàn)邏輯,配置為能夠支持SmsCodeAuthenticationToken的校驗(yàn)
- 實(shí)現(xiàn):實(shí)現(xiàn)AuthenticationProvider接口,實(shí)現(xiàn)其兩個(gè)方法。
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
/**
* 進(jìn)行身份認(rèn)證的邏輯
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("無法獲取用戶信息");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
/**
* 表示支持校驗(yàn)的Token,這里是SmsCodeAuthenticationToken
*
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
ValidateCodeFilter
- 作用:校驗(yàn)短信驗(yàn)證碼
- 實(shí)現(xiàn):和圖形驗(yàn)證碼類似,繼承OncePerRequestFilter接口防止多次調(diào)用,主要就是驗(yàn)證碼驗(yàn)證邏輯,驗(yàn)證通過則繼續(xù)下一個(gè)過濾器。
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
/**
* 驗(yàn)證碼校驗(yàn)失敗處理器
*/
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
/**
* 系統(tǒng)配置信息
*/
@Autowired
private SecurityProperties securityProperties;
/**
* 系統(tǒng)中的校驗(yàn)碼處理器
*/
@Autowired
private ValidateCodeProcessorHolder validateCodeProcessorHolder;
/**
* 存放所有需要校驗(yàn)驗(yàn)證碼的url
*/
private Map<String, ValidateCodeType> urlMap = new HashMap<>();
/**
* 驗(yàn)證請求url與配置的url是否匹配的工具類
*/
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 初始化要攔截的url配置信息
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
urlMap.put("/authentication/mobile", ValidateCodeType.SMS);
addUrlToMap(securityProperties.getCode().getSms().getUrl(), ValidateCodeType.SMS);
}
/**
* 講系統(tǒng)中配置的需要校驗(yàn)驗(yàn)證碼的URL根據(jù)校驗(yàn)的類型放入map
*
* @param urlString
* @param type
*/
protected void addUrlToMap(String urlString, ValidateCodeType type) {
if (StringUtils.isNotBlank(urlString)) {
String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ",");
for (String url : urls) {
urlMap.put(url, type);
}
}
}
/**
* 驗(yàn)證短信驗(yàn)證碼
*
* @param request
* @param response
* @param chain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
ValidateCodeType type = getValidateCodeType(request);
if (type != null) {
logger.info("校驗(yàn)請求(" + request.getRequestURI() + ")中的驗(yàn)證碼,驗(yàn)證碼類型" + type);
try {
// 進(jìn)行驗(yàn)證碼的校驗(yàn)
validateCodeProcessorHolder.findValidateCodeProcessor(type)
.validate(new ServletWebRequest(request, response));
logger.info("驗(yàn)證碼校驗(yàn)通過");
} catch (ValidateCodeException exception) {
// 如果校驗(yàn)拋出異常,則交給我們之前文章定義的異常處理器進(jìn)行處理
authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
return;
}
}
// 繼續(xù)調(diào)用后邊的過濾器
chain.doFilter(request, response);
}
/**
* 獲取校驗(yàn)碼的類型,如果當(dāng)前請求不需要校驗(yàn),則返回null
*
* @param request
* @return
*/
private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
ValidateCodeType result = null;
if (!StringUtils.equalsIgnoreCase(request.getMethod(), "GET")) {
Set<String> urls = urlMap.keySet();
for (String url : urls) {
if (pathMatcher.match(url, request.getRequestURI())) {
result = urlMap.get(url);
}
}
}
return result;
}
}
添加配置
SmsCodeAuthenticationSecurityConfig
作用:配置SmsCodeAuthenticationFilter,后面需要把這些配置加到主配置類BrowserSecurityConfig
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
// 設(shè)置AuthenticationManager
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 設(shè)置登錄成功處理器
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(meicloudAuthenticationSuccessHandler);
// 設(shè)置登錄失敗處理器
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(meicloudAuthenticationFailureHandler);
String key = UUID.randomUUID().toString();
smsCodeAuthenticationFilter.setRememberMeServices(new PersistentTokenBasedRememberMeServices(key, userDetailsService, persistentTokenRepository));
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
// 將自己寫的Provider加到Provider集合里去
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
BrowserSecurityConfig
作用:主配置類;添加短信驗(yàn)證碼配置類、添加SmsCodeAuthenticationSecurityConfig配置
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private SecurityProperties securityProperties;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 驗(yàn)證碼校驗(yàn)過濾器
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
// 將驗(yàn)證碼校驗(yàn)過濾器加到 UsernamePasswordAuthenticationFilter 過濾器之前
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
// 當(dāng)用戶登錄認(rèn)證時(shí)默認(rèn)跳轉(zhuǎn)的頁面
.loginPage("/authentication/require")
// 以下這行 UsernamePasswordAuthenticationFilter 會知道要處理表單的 /authentication/form 請求,而不是默認(rèn)的 /login
.loginProcessingUrl("/authentication/form")
.successHandler(meicloudAuthenticationSuccessHandler)
.failureHandler(meicloudAuthenticationFailureHandler)
// 配置記住我功能
.and()
.rememberMe()
// 配置TokenRepository
.tokenRepository(persistentTokenRepository())
// 配置Token過期時(shí)間
.tokenValiditySeconds(3600)
// 最終拿到用戶名之后,使用UserDetailsService去做登錄
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
// 排除對 "/authentication/require" 和 "/meicloud-signIn.html" 的身份驗(yàn)證
.antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/*").permitAll()
// 表示所有請求都需要身份驗(yàn)證
.anyRequest()
.authenticated()
.and()
.csrf().disable()// 暫時(shí)把跨站請求偽造的功能關(guān)閉掉
// 相當(dāng)于把smsCodeAuthenticationSecurityConfig里的配置加到上面這些配置的后面
.apply(smsCodeAuthenticationSecurityConfig);
}
/**
* 記住我功能的Token存取器配置
*
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啟動(dòng)的時(shí)候自動(dòng)創(chuàng)建表,建表語句 JdbcTokenRepositoryImpl 已經(jīng)都寫好了
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
}
總結(jié)
到此這篇關(guān)于Spring Security 實(shí)現(xiàn)短信驗(yàn)證碼登錄功能的文章就介紹到這了,更多相關(guān)spring security 驗(yàn)證碼登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring Security 實(shí)現(xiàn)多種登錄方式(常規(guī)方式外的郵件、手機(jī)驗(yàn)證碼登錄)
- Spring Security登錄添加驗(yàn)證碼的實(shí)現(xiàn)過程
- SpringBoot + SpringSecurity 短信驗(yàn)證碼登錄功能實(shí)現(xiàn)
- Spring Security OAuth2集成短信驗(yàn)證碼登錄以及第三方登錄
- Spring Security Oauth2.0 實(shí)現(xiàn)短信驗(yàn)證碼登錄示例
- 使用Spring Security集成手機(jī)驗(yàn)證碼登錄功能實(shí)現(xiàn)
相關(guān)文章
快速校驗(yàn)實(shí)體類時(shí),@Valid,@Validated,@NotNull注解無效的解決
這篇文章主要介紹了快速校驗(yàn)實(shí)體類時(shí),@Valid,@Validated,@NotNull注解無效的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10
解決因jdk版本引起的TypeNotPresentExceptionProxy異常
這篇文章介紹了解決因jdk版本引起的TypeNotPresentExceptionProxy異常的方法,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12
Java Swing JSlider滑塊的實(shí)現(xiàn)示例
這篇文章主要介紹了Java Swing JSlider滑塊的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
關(guān)于springboot中的自定義配置項(xiàng)
這篇文章主要介紹了關(guān)于springboot中的自定義配置項(xiàng),在項(xiàng)目開發(fā)的過程中,經(jīng)常需要自定義系統(tǒng)業(yè)務(wù)方面的配置文件及配置項(xiàng),Spring Boot提供了@value注解、@ConfigurationProperties注解和Environment接口等3種方式自定義配置項(xiàng),需要的朋友可以參考下2023-07-07
Java 8函數(shù)式接口Function BiFunction DoubleFunction
這篇文章主要為大家介紹了Java 8函數(shù)式接口Function BiFunction DoubleFunction區(qū)別示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
SpringBoot使用PageHelper插件實(shí)現(xiàn)Mybatis分頁效果
這篇文章主要介紹了SpringBoot使用PageHelper插件實(shí)現(xiàn)Mybatis分頁效果,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-02-02

