摘要:過濾器基本都是通過過濾器來完成配置的身份認(rèn)證權(quán)限認(rèn)證以及登出。密碼比對通過進(jìn)行密碼比對注可自定義通過獲取通過獲取生成身份認(rèn)證通過后最終返回的記錄認(rèn)證的身份信息
知彼知己方能百戰(zhàn)百勝,用 Spring Security 來滿足我們的需求最好了解其原理,這樣才能隨意拓展,本篇文章主要記錄 Spring Security 的基本運(yùn)行流程。過濾器
Spring Security 基本都是通過過濾器來完成配置的身份認(rèn)證、權(quán)限認(rèn)證以及登出。
Spring Security 在 Servlet 的過濾鏈(filter chain)中注冊了一個過濾器 FilterChainProxy,它會把請求代理到 Spring Security 自己維護(hù)的多個過濾鏈,每個過濾鏈會匹配一些 URL,如果匹配則執(zhí)行對應(yīng)的過濾器。過濾鏈?zhǔn)怯许樞虻模粋€請求只會執(zhí)行第一條匹配的過濾鏈。Spring Security 的配置本質(zhì)上就是新增、刪除、修改過濾器。
默認(rèn)情況下系統(tǒng)幫我們注入的這 15 個過濾器,分別對應(yīng)配置不同的需求。接下來我們重點(diǎn)是分析下 UsernamePasswordAuthenticationFilter 這個過濾器,他是用來使用用戶名和密碼登錄認(rèn)證的過濾器,但是很多情況下我們的登錄不止是簡單的用戶名和密碼,又可能是用到第三方授權(quán)登錄,這個時候我們就需要使用自定義過濾器,當(dāng)然這里不做詳細(xì)說明,只是說下自定義過濾器怎么注入。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterAfter(...);
...
}
身份認(rèn)證流程
在開始身份認(rèn)證流程之前我們需要了解下幾個基本概念
1.SecurityContextHolderSecurityContextHolder 存儲 SecurityContext 對象。SecurityContextHolder 是一個存儲代理,有三種存儲模式分別是:
MODE_THREADLOCAL:SecurityContext 存儲在線程中。
MODE_INHERITABLETHREADLOCAL:SecurityContext 存儲在線程中,但子線程可以獲取到父線程中的 SecurityContext。
MODE_GLOBAL:SecurityContext 在所有線程中都相同。
SecurityContextHolder 默認(rèn)使用 MODE_THREADLOCAL 模式,SecurityContext 存儲在當(dāng)前線程中。調(diào)用 SecurityContextHolder 時不需要顯示的參數(shù)傳遞,在當(dāng)前線程中可以直接獲取到 SecurityContextHolder 對象。
//獲取當(dāng)前線程里面認(rèn)證的對象 SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); //保存認(rèn)證對象 (一般用于自定義認(rèn)證成功保存認(rèn)證對象) SecurityContextHolder.getContext().setAuthentication(authResult); //清空認(rèn)證對象 (一般用于自定義登出清空認(rèn)證對象) SecurityContextHolder.clearContext();2.Authentication
Authentication 即驗(yàn)證,表明當(dāng)前用戶是誰。什么是驗(yàn)證,比如一組用戶名和密碼就是驗(yàn)證,當(dāng)然錯誤的用戶名和密碼也是驗(yàn)證,只不過 Spring Security 會校驗(yàn)失敗。
Authentication 接口
public interface Authentication extends Principal, Serializable {
//獲取用戶權(quán)限,一般情況下獲取到的是用戶的角色信息
Collection extends GrantedAuthority> getAuthorities();
//獲取證明用戶認(rèn)證的信息,通常情況下獲取到的是密碼等信息,不過登錄成功就會被移除
Object getCredentials();
//獲取用戶的額外信息,比如 IP 地址、經(jīng)緯度等
Object getDetails();
//獲取用戶身份信息,在未認(rèn)證的情況下獲取到的是用戶名,在已認(rèn)證的情況下獲取到的是 UserDetails (暫時理解為,當(dāng)前應(yīng)用用戶對象的擴(kuò)展)
Object getPrincipal();
//獲取當(dāng)前 Authentication 是否已認(rèn)證
boolean isAuthenticated();
//設(shè)置當(dāng)前 Authentication 是否已認(rèn)證
void setAuthenticated(boolean isAuthenticated);
}
3.AuthenticationManager ProviderManager AuthenticationProvider
其實(shí)這三者很好區(qū)分,AuthenticationManager 主要就是為了完成身份認(rèn)證流程,ProviderManager 是 AuthenticationManager 接口的具體實(shí)現(xiàn)類,ProviderManager 里面有個記錄 AuthenticationProvider 對象的集合屬性 providers,AuthenticationProvider 接口類里有兩個方法
public interface AuthenticationProvider {
//實(shí)現(xiàn)具體的身份認(rèn)證邏輯,認(rèn)證失敗拋出對應(yīng)的異常
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
//該認(rèn)證類是否支持該 Authentication 的認(rèn)證
boolean supports(Class> authentication);
}
接下來就是遍歷 ProviderManager 里面的 providers 集合,找到和合適的 AuthenticationProvider 完成身份認(rèn)證。
4.UserDetailsService UserDetails在 UserDetailsService 接口中只有一個簡單的方法
public interface UserDetailsService {
//根據(jù)用戶名查到對應(yīng)的 UserDetails 對象
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
5.流程
對于上面概念有什么不明白的地方,在們在接下來的流程中慢慢分析
在運(yùn)行到 UsernamePasswordAuthenticationFilter 過濾器的時候首先是進(jìn)入其父類 AbstractAuthenticationProcessingFilter 的 doFilter() 方法中
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
...
//首先配對是不是配置的身份認(rèn)證的URI,是則執(zhí)行下面的認(rèn)證,不是則跳過
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
...
Authentication authResult;
try {
//關(guān)鍵方法, 實(shí)現(xiàn)認(rèn)證邏輯并返回 Authentication, 由其子類 UsernamePasswordAuthenticationFilter 實(shí)現(xiàn), 由下面 5.3 詳解
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn"t completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
//認(rèn)證失敗調(diào)用...由下面 5.1 詳解
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
//認(rèn)證失敗調(diào)用...由下面 5.1 詳解
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//認(rèn)證成功調(diào)用...由下面 5.2 詳解
successfulAuthentication(request, response, chain, authResult);
}
5.1 認(rèn)證失敗處理邏輯
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
...
rememberMeServices.loginFail(request, response);
//該 handler 處理失敗界面跳轉(zhuǎn)和響應(yīng)邏輯
failureHandler.onAuthenticationFailure(request, response, failed);
}
這里默認(rèn)配置的失敗處理 handler 是 SimpleUrlAuthenticationFailureHandler,可自定義。
public class SimpleUrlAuthenticationFailureHandler implements
AuthenticationFailureHandler {
...
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
//沒有配置失敗跳轉(zhuǎn)的URL則直接響應(yīng)錯誤
if (defaultFailureUrl == null) {
logger.debug("No failure URL set, sending 401 Unauthorized error");
response.sendError(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase());
}
else {
//否則
//緩存異常
saveException(request, exception);
//根據(jù)配置的異常頁面是重定向還是轉(zhuǎn)發(fā)進(jìn)行不同方式跳轉(zhuǎn)
if (forwardToDestination) {
logger.debug("Forwarding to " + defaultFailureUrl);
request.getRequestDispatcher(defaultFailureUrl)
.forward(request, response);
}
else {
logger.debug("Redirecting to " + defaultFailureUrl);
redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
}
}
}
//緩存異常,轉(zhuǎn)發(fā)則保存在request里面,重定向則保存在session里面
protected final void saveException(HttpServletRequest request,
AuthenticationException exception) {
if (forwardToDestination) {
request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
}
else {
HttpSession session = request.getSession(false);
if (session != null || allowSessionCreation) {
request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,
exception);
}
}
}
}
這里做下小拓展:用系統(tǒng)的錯誤處理handler,指定認(rèn)證失敗跳轉(zhuǎn)的URL,在MVC里面對應(yīng)的URL方法里面可以通過key從request或session里面拿到錯誤信息,反饋給前端
5.2 認(rèn)證成功處理邏輯protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
...
//這里要注意很重要,將認(rèn)證完成返回的 Authentication 保存到線程對應(yīng)的 `SecurityContext` 中
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//該 handler 就是為了完成頁面跳轉(zhuǎn)
successHandler.onAuthenticationSuccess(request, response, authResult);
}
這里默認(rèn)配置的成功處理 handler 是 SavedRequestAwareAuthenticationSuccessHandler,里面的代碼就不做具體展開了,反正是跳轉(zhuǎn)到指定的認(rèn)證成功之后的界面,可自定義。
5.3 身份認(rèn)證詳情public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
...
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
...
//開始身份認(rèn)證邏輯
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 username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
//先用前端提交過來的 username 和 password 封裝一個簡易的 AuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
//具體的認(rèn)證邏輯還是交給 AuthenticationManager 對象的 authenticate(..) 方法完成,接著往下看
return this.getAuthenticationManager().authenticate(authRequest);
}
}
由源碼斷點(diǎn)跟蹤得知,最終解析是由 AuthenticationManager 接口實(shí)現(xiàn)類 ProviderManager 來完成
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
...
private List providers = Collections.emptyList();
...
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
....
//遍歷所有的 AuthenticationProvider, 找到合適的完成身份驗(yàn)證
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
...
try {
//進(jìn)行具體的身份驗(yàn)證邏輯, 這里使用到的是 DaoAuthenticationProvider, 具體邏輯記著往下看
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch
...
}
...
throw lastException;
}
}
DaoAuthenticationProvider 繼承自 AbstractUserDetailsAuthenticationProvider 實(shí)現(xiàn)了 AuthenticationProvider 接口
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
...
private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();
...
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
...
// 獲得提交過來的用戶名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
//根據(jù)用戶名從緩存中查找 UserDetails
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//緩存中沒有則通過 retrieveUser(..) 方法查找 (看下面 DaoAuthenticationProvider 的實(shí)現(xiàn))
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch
...
}
try {
//比對前的檢查,例如賬戶以一些狀態(tài)信息(是否鎖定, 過期...)
preAuthenticationChecks.check(user);
//子類實(shí)現(xiàn)比對規(guī)則 (看下面 DaoAuthenticationProvider 的實(shí)現(xiàn))
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we"re using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//根據(jù)最終user的一些信息重新生成具體詳細(xì)的 Authentication 對象并返回
return createSuccessAuthentication(principalToReturn, authentication, user);
}
//具體生成還是看子類實(shí)現(xiàn)
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
}
接下來我們來看下 DaoAuthenticationProvider 里面的三個重要的方法,比對方式、獲取需要比對的 UserDetails 對象以及生產(chǎn)最終返回 Authentication 的方法。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
...
//密碼比對
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
//通過 PasswordEncoder 進(jìn)行密碼比對, 注: 可自定義
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
//通過 UserDetailsService 獲取 UserDetails
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//通過 UserDetailsService 獲取 UserDetails
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
//生成身份認(rèn)證通過后最終返回的 Authentication, 記錄認(rèn)證的身份信息
@Override
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
}
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/73857.html
摘要:在中加密是一個很簡單卻又不能忽略的模塊,數(shù)據(jù)只有加密起來才更安全,這樣就散算據(jù)庫密碼泄漏也都是密文。當(dāng)然也可以自定義構(gòu)造方法,來制定用其他的方案進(jìn)行加密。應(yīng)用先示范下使用系統(tǒng)的來演示下簡單的注入構(gòu)造加密方案 在 Spring Security 中加密是一個很簡單卻又不能忽略的模塊,數(shù)據(jù)只有加密起來才更安全,這樣就散算據(jù)庫密碼泄漏也都是密文。本文分析對應(yīng)的版本是 5.14。 概念 Spr...
摘要:但是我們最好不要在里面對他進(jìn)行處理,而是放到配置的權(quán)限異常來處理。記得配置登錄認(rèn)證前和過程中的一些請求不需要身份認(rèn)證。登錄認(rèn)證失敗不能直接拋出錯誤,需要向前端響應(yīng)異常。 關(guān)于 Spring Security 的學(xué)習(xí)已經(jīng)告一段落了,剛開始接觸該安全框架感覺很迷茫,總覺得沒有 Shiro 靈活,到后來的深入學(xué)習(xí)和探究才發(fā)現(xiàn)它非常強(qiáng)大。簡單快速集成,基本不用寫任何代碼,拓展起來也非常靈活和強(qiáng)...
摘要:導(dǎo)讀閱讀本文需要有足夠的時間,筆者會由淺到深帶你一步一步了解一個資深架構(gòu)師所要掌握的各類知識點(diǎn),你也可以按照文章中所列的知識體系對比自身,對自己進(jìn)行查漏補(bǔ)缺,覺得本文對你有幫助的話,可以點(diǎn)贊關(guān)注一下。目錄一基礎(chǔ)篇二進(jìn)階篇三高級篇四架構(gòu)篇五擴(kuò) 導(dǎo)讀:閱讀本文需要有足夠的時間,筆者會由淺到深帶你一步一步了解一個資深架構(gòu)師所要掌握的各類知識點(diǎn),你也可以按照文章中所列的知識體系對比自身,對自己...
摘要:哪吒社區(qū)技能樹打卡打卡貼函數(shù)式接口簡介領(lǐng)域優(yōu)質(zhì)創(chuàng)作者哪吒公眾號作者架構(gòu)師奮斗者掃描主頁左側(cè)二維碼,加入群聊,一起學(xué)習(xí)一起進(jìn)步歡迎點(diǎn)贊收藏留言前情提要無意間聽到領(lǐng)導(dǎo)們的談話,現(xiàn)在公司的現(xiàn)狀是碼農(nóng)太多,但能獨(dú)立帶隊(duì)的人太少,簡而言之,不缺干 ? 哪吒社區(qū)Java技能樹打卡?【打卡貼 day2...
摘要:框架具有輕便,開源的優(yōu)點(diǎn),所以本譯見構(gòu)建用戶管理微服務(wù)五使用令牌和來實(shí)現(xiàn)身份驗(yàn)證往期譯見系列文章在賬號分享中持續(xù)連載,敬請查看在往期譯見系列的文章中,我們已經(jīng)建立了業(yè)務(wù)邏輯數(shù)據(jù)訪問層和前端控制器但是忽略了對身份進(jìn)行驗(yàn)證。 重拾后端之Spring Boot(四):使用JWT和Spring Security保護(hù)REST API 重拾后端之Spring Boot(一):REST API的搭建...
閱讀 2485·2021-11-23 10:04
閱讀 1570·2021-09-02 15:21
閱讀 948·2019-08-30 15:44
閱讀 1117·2019-08-30 10:48
閱讀 771·2019-08-29 17:21
閱讀 3632·2019-08-29 13:13
閱讀 2038·2019-08-23 17:17
閱讀 1842·2019-08-23 17:04