SpringSecurity安全架構學習
- 1、導入依賴
- 2、springsecurity的過濾器鍊
- 3、使用者認證流程
-
- 3.1 AbstractAuthenticationProcessingFilter過濾器
-
- AbstractAuthenticationProcessingFilter源碼分析:
- 3.1 UsernamePasswordAuthenticationFilter過濾器(springsecurity認證授權的核心過濾器)
-
- UsernamePasswordAuthenticationFilter源碼分析
1、導入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、springsecurity的過濾器鍊
對于以上的每個過濾器的作用,可參看spring security的官方文檔spring security的官方文檔進行學習。
3、使用者認證流程
3.1 AbstractAuthenticationProcessingFilter過濾器
package org.sicFilterBean;
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
......
//使用者認證成功後的結果處理
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
//使用者認證失敗後的結果處理
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
//對請求路徑進行比對,使用者可在配置類中自定義
protected AbstractAuthenticationProcessingFilter(
RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher,
"requiresAuthenticationRequestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}
//核心代碼
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//進行請求過濾位址的比對
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
//Authentication:該對象為Spring Security方式的認證結果主體類
Authentication authResult;
try {
//最為核心的代碼,其認證過程主要是依靠具體的子類實作,包括兩部分,認證(常用的使用者名+密碼,手機号+驗證碼,郵箱+驗證碼),授權(使用者權限的判定)
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
//認證結果放置于session會話中
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
//認證成功後的處理邏輯
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
//将認證結果放置于SecurityContex及安全的上下文環境中
SecurityContextHolder.getContext().setAuthentication(authResult);
//記住我功能
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//調用AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler()的成功處理邏輯
successHandler.onAuthenticationSuccess(request, response, authResult);
}
//認證失敗後的處理邏輯
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}
//記住我
rememberMeServices.loginFail(request, response);
//調用AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler()的失敗處理邏輯
failureHandler.onAuthenticationFailure(request, response, failed);
}
//使用認證管理器
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
//使用記住我服務
public void setRememberMeServices(RememberMeServices rememberMeServices) {
Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
this.rememberMeServices = rememberMeServices;
}
//session環境的管理
public void setSessionAuthenticationStrategy(
SessionAuthenticationStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
//認證成功的處理者
public void setAuthenticationSuccessHandler(
AuthenticationSuccessHandler successHandler) {
Assert.notNull(successHandler, "successHandler cannot be null");
this.successHandler = successHandler;
}
//認證失敗的處理者
public void setAuthenticationFailureHandler(
AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
}
AbstractAuthenticationProcessingFilter源碼分析:
AbstractAuthenticationProcessingFilter 抽象類中的doFilter方法定義了一個完整的使用者認證授權流程:
- 對請求的位址進行比對,位址比對則進行相應的認證權限校驗,不比對直接傳回
//進行請求過濾位址的比對
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
- 對請求進行認證及授權處理 ,這一步是由對應的子類實作去處理的,預設實作是UsernamePasswordAuthenticationFilter
- 将認證結果放置于session會話中
//認證結果放置于session會話中
sessionStrategy.onAuthentication(authResult, request, response);
- 如果在調用認證授權過程中出現任何異常,則會調用相應的認證失敗的業務邏輯,unsuccessfulAuthentication,并且預設調用SimpleUrlAuthenticationFailureHandler接口對象的onAuthenticationFailure方法。
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
===================================================================================================================================
//InternalAuthenticationServiceException 異常,屬于網絡連接配接的異常資訊,主要是用于 OAuth 認證伺服器與業務伺服器通信異常
//AuthenticationException 主要是指認證權限不通過的異常資訊,springsecurity架構已經有部分實作,可通過繼承關系進行檢視,是以在我們代碼過程中我們可以直接使用
===================================================================================================================================
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}
//取消記住我
rememberMeServices.loginFail(request, response);
//調用AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler()的失敗處理邏輯
failureHandler.onAuthenticationFailure(request, response, failed);
}
===========================================================================================================================
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
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);
if (forwardToDestination) {
logger.debug("Forwarding to " + defaultFailureUrl);
request.getRequestDispatcher(defaultFailureUrl)
.forward(request, response);
}
else {
logger.debug("Redirecting to " + defaultFailureUrl);
redirectStrategy.sendRedirect(request, response, defaultFailureUrl);
}
}
}
- 如果在調用認證授權成功,則會調用相應的認證成功的業務邏輯,successfulAuthentication*,并且預設調用SavedRequestAwareAuthenticationSuccessHandler接口對象的onAuthenticationSuccess方法,是以使用者可以自定義認證成功的處理結果業務邏輯,實作SavedRequestAwareAuthenticationSuccessHandler即可。
//認證成功後的處理邏輯
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
//将認證結果放置于SecurityContex及安全的上下文環境中
SecurityContextHolder.getContext().setAuthentication(authResult);
//記住我功能
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//調用AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler()的成功處理邏輯
successHandler.onAuthenticationSuccess(request, response, authResult);
}
==========================================================================================================================
//将本次的request請求從會話緩存中取出
private RequestCache requestCache = new HttpSessionRequestCache();
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
if (isAlwaysUseDefaultTargetUrl()
|| (targetUrlParameter != null && StringUtils.hasText(request
.getParameter(targetUrlParameter)))) {
requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
String targetUrl = savedRequest.getRedirectUrl();
logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
3.1 UsernamePasswordAuthenticationFilter過濾器(springsecurity認證授權的核心過濾器)
package org.springframework.security.web.authentication;
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// 預設頁面表單取值為username和password,
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;
// 預設的登入頁面請求位址為login,post請求
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "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 username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
//使用者名首尾去掉空格
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
//從頁面參數中擷取使用者密碼
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
//從頁面參數中擷取使用者名
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
//設定使用者的token資訊
protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
//給使用者自定義使用者名參數的方法
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
//給使用者自定義使用者密碼參數的方法
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
//對于http請求參數的自定義
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
UsernamePasswordAuthenticationFilter源碼分析
- 對于使用者認證和權限的校驗 ,該核心過濾器給定了請求參數的預設值,如url="/login",使用者名和使用者密碼的取值參數,http的請求方式等,但都給使用者自定義的空間。
- 核心代碼:
username = username.trim();
//根據使用者名和密碼,生産權限認證的主要核心類
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// 将本次請求的詳細資訊,包括使用者通路協定、路徑以及IP位址等放置在認證主體中供下一級的調用
setDetails(request, authRequest);
//真正的權限校驗的方法
return this.getAuthenticationManager().authenticate(authRequest);
-
UsernamePasswordAuthenticationToken的解讀
看一下他的繼承體系
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken
public abstract class AbstractAuthenticationToken implements Authentication,CredentialsContainer
是以說:Authentication 是一個接口,用來表示使用者認證資訊的,在使用者登入認證之前相關資訊會封裝為一個 Authentication 具體實作類的對象,在登入認證成功之後又會生成一個資訊更全面,包含使用者權限等資訊的 Authentication 對象,然後把它儲存在 SecurityContextHolder 所持有的 SecurityContext 中,供後續的程式進行調用,如通路權限的鑒定等。
- 深入了解(預設機制):
//認證開始之前,我們調用的是該構造器,是以預設的principa(需要認證的使用者對象)為我們自己所實作的UserDetils
//而我們自定義的UserDetils實作了,其實主要也就是兩個組成部分,一個是使用者詳情,一個是權限範圍
//credentials(認證憑據)預設給定的就是使用者密碼
//認證狀态給定的是false,未認證
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
- 深入了解(自定義):
//我們使用的是
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
//及我們已經通過密碼規則的校驗,現在進行權限的校驗及配置設定
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails,
userDetails.getPassword(), userDetails.getAuthorities());
- AuthenticationManager 和 AuthenticationProvider
AuthenticationManager 是一個用來處理認證(Authentication)請求的接口。在其中隻定義了一個方法 authenticate(),該方法隻接收一個代表認證請求的 Authentication 對象作為參數,如果認證成功,則會傳回一個封裝了目前使用者權限等資訊的 Authentication 對象進行傳回。
在 Spring Security 中,AuthenticationManager 的預設實作是 ProviderManager,而且它不直接自己處理認證請求,而是委托給其所配置的 AuthenticationProvider 清單,然後會依次使用每一個 AuthenticationProvider 進行認證,如果有一個 AuthenticationProvider 認證後的結果不為 null,則表示該 AuthenticationProvider 已經認證成功,之後的 AuthenticationProvider 将不再繼續認證。然後直接以該 AuthenticationProvider 的認證結果作為 ProviderManager 的認證結果。如果所有的 AuthenticationProvider 的認證結果都為 null,則表示認證失敗,将抛出一個 ProviderNotFoundException。校驗認證請求最常用的方法是根據請求的使用者名加載對應的 UserDetails,然後比對 UserDetails 的密碼與認證請求的密碼是否一緻,一緻則表示認證通過。Spring Security 内部的 DaoAuthenticationProvider 就是使用的這種方式。其内部使用 UserDetailsService 來負責加載 UserDetails,在認證成功以後會使用加載的 UserDetails 來封裝要傳回的 Authentication 對象,加載的 UserDetails 對象是包含使用者權限等資訊的。認證成功傳回的 Authentication 對象将會儲存在目前的 SecurityContext 中。
預設情況下,在認證成功後 ProviderManager 将清除傳回的 Authentication 中的憑證資訊,如密碼。是以如果你在無狀态的應用中将傳回的 Authentication 資訊緩存起來了,那麼以後你再利用緩存的資訊去認證将會失敗,因為它已經不存在密碼這樣的憑證資訊了。是以在使用緩存的時候你應該考慮到這個問題。一種解決辦法是設定 ProviderManager 的 eraseCredentialsAfterAuthentication 屬性為 false,或者想辦法在緩存時将憑證資訊一起緩存。
- AuthenticationEntryPoint 用來解決匿名使用者通路無權限資源時的異常
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage())));
response.getWriter().flush();
}
- AccessDeineHandler 用來解決認證過的使用者通路無權限資源時的異常
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
response.getWriter().flush();
}
- 加入自定義的認證處理
//添加自定義未授權和未登入結果傳回
httpSecurity.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);