Spring?Security自定義認(rèn)證邏輯實(shí)例詳解
前言
這篇文章的內(nèi)容基于對(duì)Spring Security 認(rèn)證流程的理解,如果你不了解,可以讀一下這篇文章:Spring Security 認(rèn)證流程 。
分析問(wèn)題
以下是 Spring Security 內(nèi)置的用戶(hù)名/密碼認(rèn)證的流程圖,我們可以從這里入手:

根據(jù)上圖,我們可以照貓畫(huà)虎,自定義一個(gè)認(rèn)證流程,比如手機(jī)短信碼認(rèn)證。在圖中,我已經(jīng)把流程中涉及到的主要環(huán)節(jié)標(biāo)記了不同的顏色,其中藍(lán)色塊的部分,是用戶(hù)名/密碼認(rèn)證對(duì)應(yīng)的部分,綠色塊標(biāo)記的部分,則是與具體認(rèn)證方式無(wú)關(guān)的邏輯。
因此,我們可以按照藍(lán)色部分的類(lèi),開(kāi)發(fā)我們自定義的邏輯,主要包括以下內(nèi)容:
- 一個(gè)自定義的
Authentication實(shí)現(xiàn)類(lèi),與UsernamePasswordAuthenticationToken類(lèi)似,用來(lái)保存認(rèn)證信息。 - 一個(gè)自定義的過(guò)濾器,與
UsernamePasswordAuthenticationFilter類(lèi)似,針對(duì)特定的請(qǐng)求,封裝認(rèn)證信息,調(diào)用認(rèn)證邏輯。 - 一個(gè)
AuthenticationProvider的實(shí)現(xiàn)類(lèi),提供認(rèn)證邏輯,與DaoAuthenticationProvider類(lèi)似。
接下來(lái),以手機(jī)驗(yàn)證碼認(rèn)證為例,一一完成。
自定義 Authentication
先給代碼,后面進(jìn)行說(shuō)明:
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
private Object credentials;
public SmsCodeAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}和 UsernamePasswordAuthenticationToken 一樣,繼承 AbstractAuthenticationToken 抽象類(lèi),需要實(shí)現(xiàn) getPrincipal 和 getCredentials 兩個(gè)方法。在用戶(hù)名/密碼認(rèn)證中,principal 表示用戶(hù)名,credentials 表示密碼,在此,我們可以讓它們指代手機(jī)號(hào)和驗(yàn)證碼,因此,我們?cè)黾舆@兩個(gè)屬性,然后實(shí)現(xiàn)方法。
除此之外,我們需要寫(xiě)兩個(gè)構(gòu)造方法,分別用來(lái)創(chuàng)建未認(rèn)證的和已經(jīng)成功認(rèn)證的認(rèn)證信息。
自定義 Filter
這一部分,可以參考 UsernamePasswordAuthenticationFilter 來(lái)寫(xiě)。還是線(xiàn)上代碼:
public class SmsCodeAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
public static final String FORM_MOBILE_KEY = "mobile";
public static final String FORM_SMS_CODE_KEY = "smsCode";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login",
"POST");
private boolean postOnly = true;
protected SmsCodeAuthenticationProcessingFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
mobile = (mobile != null) ? mobile : "";
mobile = mobile.trim();
String smsCode = obtainSmsCode(request);
smsCode = (smsCode != null) ? smsCode : "";
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, smsCode);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private String obtainMobile(HttpServletRequest request) {
return request.getParameter(FORM_MOBILE_KEY);
}
private String obtainSmsCode(HttpServletRequest request) {
return request.getParameter(FORM_SMS_CODE_KEY);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}這部分比較簡(jiǎn)單,關(guān)鍵點(diǎn)如下:
- 首先,默認(rèn)的構(gòu)造方法中制定了過(guò)濾器匹配那些請(qǐng)求,這里匹配的是
/sms/login的 POST 請(qǐng)求。 - 在
attemptAuthentication方法中,首先從request中獲取表單輸入的手機(jī)號(hào)和驗(yàn)證碼,創(chuàng)建未經(jīng)認(rèn)證的 Token 信息。 - 將 Token 信息交給
this.getAuthenticationManager().authenticate(authRequest)方法。
自定義 Provider
這里是完成認(rèn)證的主要邏輯,這里的代碼只有最基本的校驗(yàn)邏輯,沒(méi)有寫(xiě)比較嚴(yán)謹(jǐn)?shù)男r?yàn),比如校驗(yàn)用戶(hù)是否禁用等,因?yàn)檫@部分比較繁瑣但是簡(jiǎn)單。
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
public static final String SESSION_MOBILE_KEY = "mobile";
public static final String SESSION_SMS_CODE_KEY = "smsCode";
public static final String FORM_MOBILE_KEY = "mobile";
public static final String FORM_SMS_CODE_KEY = "smsCode";
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
authenticationChecks(authentication);
String mobile = authentication.getName();
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
SmsCodeAuthenticationToken authResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
return authResult;
}
/**
* 認(rèn)證信息校驗(yàn)
* @param authentication
*/
private void authenticationChecks(Authentication authentication) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 表單提交的手機(jī)號(hào)和驗(yàn)證碼
String formMobile = request.getParameter(FORM_MOBILE_KEY);
String formSmsCode = request.getParameter(FORM_SMS_CODE_KEY);
// 會(huì)話(huà)中保存的手機(jī)號(hào)和驗(yàn)證碼
String sessionMobile = (String) request.getSession().getAttribute(SESSION_MOBILE_KEY);
String sessionSmsCode = (String) request.getSession().getAttribute(SESSION_SMS_CODE_KEY);
if (StringUtils.isEmpty(sessionMobile) || StringUtils.isEmpty(sessionSmsCode)) {
throw new BadCredentialsException("為發(fā)送手機(jī)驗(yàn)證碼");
}
if (!formMobile.equals(sessionMobile)) {
throw new BadCredentialsException("手機(jī)號(hào)碼不一致");
}
if (!formSmsCode.equals(sessionSmsCode)) {
throw new BadCredentialsException("驗(yàn)證碼不一致");
}
}
@Override
public boolean supports(Class<?> authentication) {
return (SmsCodeAuthenticationToken.class.isAssignableFrom(authentication));
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}這段代碼的重點(diǎn)有以下幾個(gè):
supports方法用來(lái)判斷這個(gè) Provider 支持的 AuthenticationToken 的類(lèi)型,這里對(duì)應(yīng)我們之前創(chuàng)建的SmsCodeAuthenticationToken。- 在
authenticate方法中,我們將 Token 中的手機(jī)號(hào)和驗(yàn)證碼與 Session 中保存的手機(jī)號(hào)和驗(yàn)證碼進(jìn)行對(duì)比。(向 Session 中保存手機(jī)號(hào)和驗(yàn)證碼的部分在下文中實(shí)現(xiàn))對(duì)比無(wú)誤后,從 UserDetailsService 中獲取對(duì)應(yīng)的用戶(hù),并依此創(chuàng)建通過(guò)認(rèn)證的 Token,并返回,最終到達(dá) Filter 中。
自定義認(rèn)證成功/失敗后的 Handler
之前,我們通過(guò)分析源碼知道,F(xiàn)ilter 中的 doFilter 方法,其實(shí)是在它的父類(lèi)
AbstractAuthenticationProcessingFilter 中的,attemptAuthentication 方法也是在 doFilter 中被調(diào)用的。
當(dāng)我們進(jìn)行完之前的自定義邏輯,無(wú)論是否認(rèn)證成功,attemptAuthentication 方法會(huì)返回認(rèn)證成功的結(jié)果或者拋出認(rèn)證失敗的異常。doFilter 方法中會(huì)根據(jù)認(rèn)證的結(jié)果(成功/失?。?,調(diào)用不同的處理邏輯,這兩個(gè)處理邏輯,我們也可以進(jìn)行自定義。
我直接在下面貼代碼:
public class SmsCodeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write(authentication.getName());
}
}public class SmsCodeAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().write("認(rèn)證失敗");
}
}以上是成功和失敗后的處理邏輯,需要分別實(shí)現(xiàn)對(duì)應(yīng)的接口,并實(shí)現(xiàn)方法。注意,這里只是為了測(cè)試,寫(xiě)了最簡(jiǎn)單的邏輯,以便測(cè)試的時(shí)候能夠區(qū)分兩種情況。真實(shí)的項(xiàng)目中,要根據(jù)具體的業(yè)務(wù)執(zhí)行相應(yīng)的邏輯,比如保存當(dāng)前登錄用戶(hù)的信息等。
配置自定義認(rèn)證的邏輯
為了使我們的自定義認(rèn)證生效,需要將 Filter 和 Provider 添加到 Spring Security 的配置當(dāng)中,我們可以把這一部分配置先單獨(dú)放到一個(gè)配置類(lèi)中:
@Component
@RequiredArgsConstructor
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private final UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) {
SmsCodeAuthenticationProcessingFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationProcessingFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(new SmsCodeAuthenticationSuccessHandler());
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(new SmsCodeAuthenticationFailureHandler());
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}其中,有以下需要注意的地方:
- 一定記得把 AuthenticationManager 提供給 Filter,回顧之前講到的認(rèn)證邏輯,如果沒(méi)有這一步,在 Filter 中完成認(rèn)證信息的封裝后,就沒(méi)辦法去找對(duì)應(yīng)的 Provider。
- 要把成功/失敗后的處理邏輯的兩個(gè)類(lèi)提供給 Filter,否則不會(huì)進(jìn)入這兩個(gè)邏輯,而是會(huì)進(jìn)入默認(rèn)的處理邏輯。
- Provider 中用到了 UserDetailsService,也要記得提供。
- 最后,將兩者添加到 HttpSecurity 對(duì)象中。
接下來(lái),需要在 Spring Security 的主配置中添加如下內(nèi)容。
- 首先,注入
SmsCodeAuthenticationSecurityConfig配置。 - 然后,在
configure(HttpSecurity http)方法中,引入配置:http.apply`` ( ``smsCodeAuthenticationSecurityConfig`` ) ``;。 - 最后,由于在認(rèn)證前,需要請(qǐng)求和校驗(yàn)驗(yàn)證碼,因此,對(duì)
/sms/**路徑進(jìn)行放行。
測(cè)試
大功告成,我們測(cè)試一下,首先需要提供一個(gè)發(fā)送驗(yàn)證碼的接口,由于是測(cè)試,我們直接將驗(yàn)證碼返回。接口代碼如下:
@GetMapping("/getCode")
public String getCode(@RequestParam("mobile") String mobile,
HttpSession session) {
String code = "123456";
session.setAttribute("mobile", mobile);
session.setAttribute("smsCode", code);
return code;
}為了能獲取到相應(yīng)的用戶(hù),如果你還沒(méi)有實(shí)現(xiàn)自己的 UserDetailsService,先寫(xiě)一個(gè)簡(jiǎn)單的邏輯,完成測(cè)試,其中的 loadUserByUsername 方法如下即可:
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO: 臨時(shí)邏輯,之后對(duì)接用戶(hù)管理相關(guān)的服務(wù)
return new User(username, "123456",
AuthorityUtils.createAuthorityList("admin"));
}OK,下面是測(cè)試結(jié)果:



總結(jié)
到此這篇關(guān)于Spring Security自定義認(rèn)證邏輯的文章就介紹到這了,更多相關(guān)Spring Security自定義認(rèn)證邏輯內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java框架學(xué)習(xí)Struts2復(fù)選框?qū)嵗a
這篇文章主要介紹了Java框架學(xué)習(xí)Struts2復(fù)選框?qū)嵗a,分享了相關(guān)代碼示例,小編覺(jué)得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-02-02
Java防止頻繁請(qǐng)求、重復(fù)提交的操作代碼(后端防抖操作)
在客戶(hù)端網(wǎng)絡(luò)慢或者服務(wù)器響應(yīng)慢時(shí),用戶(hù)有時(shí)是會(huì)頻繁刷新頁(yè)面或重復(fù)提交表單的,這樣是會(huì)給服務(wù)器造成不小的負(fù)擔(dān)的,同時(shí)在添加數(shù)據(jù)時(shí)有可能造成不必要的麻煩,今天通過(guò)本文給大家介紹下Java防止頻繁請(qǐng)求、重復(fù)提交的操作代碼,一起看看吧2022-04-04
Java使用策略模式解決商場(chǎng)促銷(xiāo)商品問(wèn)題示例
這篇文章主要介紹了Java使用策略模式解決商場(chǎng)促銷(xiāo)商品問(wèn)題,簡(jiǎn)單描述了策略模式的概念、原理,并結(jié)合實(shí)例形式分析了Java基于策略模式解決商品促銷(xiāo)問(wèn)題的相關(guān)操作技巧,需要的朋友可以參考下2018-05-05
詳解eclipse下創(chuàng)建第一個(gè)spring boot項(xiàng)目
本文詳細(xì)介紹了創(chuàng)建第一個(gè)基于eclipse(eclipse-jee-neon-3-win32-x86_64.zip)+spring boot創(chuàng)建的項(xiàng)目。2017-04-04

