Spring Security4.1.3實現攔截登錄後向登錄頁面跳轉方式(redirect或forward)返回被攔截界面
一、看下內部原理
簡化後的認證過程分為7步:
-
用戶訪問網站,打開了一個鏈接(origin url)。
-
請求發送給服務器,服務器判斷用戶請求了受保護的資源。
-
由於用戶沒有登錄,服務器重定向到登錄頁面
-
填寫表單,點擊登錄
-
瀏覽器將用戶名密碼以表單形式發送給服務器
-
服務器驗證用戶名密碼。成功,進入到下一步。否則要求用戶重新認證(第三步)
-
服務器對用戶擁有的權限(角色)判定: 有權限,重定向到origin url; 權限不足,返回狀態碼403("forbidden").
從第3步,我們可以知道,用戶的請求被中斷了。
用戶登錄成功後(第7步),會被重定向到origin url,spring security通過使用緩存的request,使得被中斷的請求能夠繼續執行。
使用緩存
用戶登錄成功後,頁面重定向到origin url。瀏覽器發出的請求優先被攔截器RequestCacheAwareFilter攔截,RequestCacheAwareFilter通過其持有的RequestCache對象實現request的恢復。
[java] view plain copy- public void doFilter(ServletRequest request, ServletResponse response,
- FilterChain chain) throws IOException, ServletException {
- // request匹配,則取出,該操作同時會將緩存的request從session中刪除
- HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
- (HttpServletRequest) request, (HttpServletResponse) response);
- // 優先使用緩存的request
- chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
- response);
- }
何時緩存
首先,我們需要了解下RequestCache以及ExceptionTranslationFilter。
RequestCache
RequestCache接口聲明了緩存與恢復操作。默認實現類是HttpSessionRequestCache
。HttpSessionRequestCache的實現比較簡單,這裏只列出接口的聲明:
[java] view plain copy
- public interface RequestCache {
- // 將request緩存到session中
- void saveRequest(HttpServletRequest request, HttpServletResponse response);
- // 從session中取request
- SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response);
- // 獲得與當前request匹配的緩存,並將匹配的request從session中刪除
- HttpServletRequest getMatchingRequest(HttpServletRequest request,
- HttpServletResponse response);
- // 刪除緩存的request
- void removeRequest(HttpServletRequest request, HttpServletResponse response);
- }
ExceptionTranslationFilter
ExceptionTranslationFilter 是Spring Security的核心filter之一,用來處理AuthenticationException和AccessDeniedException兩種異常。
在我們的例子中,AuthenticationException指的是未登錄狀態下訪問受保護資源,AccessDeniedException指的是登陸了但是由於權限不足(比如普通用戶訪問管理員界面)。
ExceptionTranslationFilter 持有兩個處理類,分別是AuthenticationEntryPoint和AccessDeniedHandler。
ExceptionTranslationFilter 對異常的處理是通過這兩個處理類實現的,處理規則很簡單:
[java] view plain copy- 規則1. 如果異常是 AuthenticationException,使用 AuthenticationEntryPoint 處理
- 規則2. 如果異常是 AccessDeniedException 且用戶是匿名用戶,使用 AuthenticationEntryPoint 處理
- 規則3. 如果異常是 AccessDeniedException 且用戶不是匿名用戶,如果否則交給 AccessDeniedHandler 處理。
對應以下代碼
- private void handleSpringSecurityException(HttpServletRequest request,
- HttpServletResponse response, FilterChain chain, RuntimeException exception)
- throws IOException, ServletException {
- if (exception instanceof AuthenticationException) {
- logger.debug(
- "Authentication exception occurred; redirecting to authentication entry point",
- exception);
- sendStartAuthentication(request, response, chain,
- (AuthenticationException) exception);
- }
- else if (exception instanceof AccessDeniedException) {
- if (authenticationTrustResolver.isAnonymous(SecurityContextHolder
- .getContext().getAuthentication())) {
- logger.debug(
- "Access is denied (user is anonymous); redirecting to authentication entry point",
- exception);
- sendStartAuthentication(
- request,
- response,
- chain,
- new InsufficientAuthenticationException(
- "Full authentication is required to access this resource"));
- }
- else {
- logger.debug(
- "Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
- exception);
- accessDeniedHandler.handle(request, response,
- (AccessDeniedException) exception);
- }
- }
- }
AccessDeniedHandler 默認實現是 AccessDeniedHandlerImpl。該類對異常的處理是返回403錯誤碼。
[java] view plain copy- public void handle(HttpServletRequest request, HttpServletResponse response,
- AccessDeniedException accessDeniedException) throws IOException,
- ServletException {
- if (!response.isCommitted()) {
- if (errorPage != null) { // 定義了errorPage
- // errorPage中可以操作該異常
- request.setAttribute(WebAttributes.ACCESS_DENIED_403,
- accessDeniedException);
- // 設置403狀態碼
- response.setStatus(HttpServletResponse.SC_FORBIDDEN);
- // 轉發到errorPage
- RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage);
- dispatcher.forward(request, response);
- }
- else { // 沒有定義errorPage,則返回403狀態碼(Forbidden),以及錯誤信息
- response.sendError(HttpServletResponse.SC_FORBIDDEN,
- accessDeniedException.getMessage());
- }
- }
- }
AuthenticationEntryPoint 默認實現是 LoginUrlAuthenticationEntryPoint, 該類的處理是轉發或重定向到登錄頁面
[java] view plain copy
- public void commence(HttpServletRequest request, HttpServletResponse response,
- AuthenticationException authException) throws IOException, ServletException {
- String redirectUrl = null;
- if (useForward) {
- if (forceHttps && "http".equals(request.getScheme())) {
- // First redirect the current request to HTTPS.
- // When that request is received, the forward to the login page will be
- // used.
- redirectUrl = buildHttpsRedirectUrlForRequest(request);
- }
- if (redirectUrl == null) {
- String loginForm = determineUrlToUseForThisRequest(request, response,
- authException);
- if (logger.isDebugEnabled()) {
- logger.debug("Server side forward to: " + loginForm);
- }
- RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
- // 轉發
- dispatcher.forward(request, response);
- return;
- }
- }
- else {
- // redirect to login page. Use https if forceHttps true
- redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
- }
- // 重定向
- redirectStrategy.sendRedirect(request, response, redirectUrl);
- }
了解完這些,回到我們的例子。
第3步時,用戶未登錄的情況下訪問受保護資源,ExceptionTranslationFilter會捕獲到AuthenticationException異常(規則1)。頁面需要跳轉,ExceptionTranslationFilter在跳轉前使用requestCache緩存request。
[java] view plain copy- protected void sendStartAuthentication(HttpServletRequest request,
- HttpServletResponse response, FilterChain chain,
- AuthenticationException reason) throws ServletException, IOException {
- // SEC-112: Clear the SecurityContextHolder‘s Authentication, as the
- // existing Authentication is no longer considered valid
- SecurityContextHolder.getContext().setAuthentication(null);
- // 緩存 request
- requestCache.saveRequest(request, response);
- logger.debug("Calling Authentication entry point.");
- authenticationEntryPoint.commence(request, response, reason);
- }
二、了解了以上原理以及上篇的forward和redirect的區別,配置實現如下,基於springsecurity4.1.3版本
配置文件:完整的
[html] view plain copy
- <?xml version="1.0" encoding="UTF-8"?>
- <beans:beans xmlns="http://www.springframework.org/schema/security"
- xmlns:beans="http://www.springframework.org/schema/beans"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans.xsd
- http://www.springframework.org/schema/security
- http://www.springframework.org/schema/security/spring-security.xsd">
- <http auto-config="true" use-expressions="true" entry-point-ref="myLoginUrlAuthenticationEntryPoint">
- <form-login
- login-page="/login"
- authentication-failure-url="/login?error"
- login-processing-url="/login"
- authentication-success-handler-ref="myAuthenticationSuccessHandler" />
- <!-- 認證成功用自定義類myAuthenticationSuccessHandler處理 -->
- <logout logout-url="/logout"
- logout-success-url="/"
- invalidate-session="true"
- delete-cookies="JSESSIONID"/>
- <csrf disabled="true" />
- <intercept-url pattern="/order/*" access="hasRole(‘ROLE_USER‘)"/>
- </http>
- <!-- 使用自定義類myUserDetailsService從數據庫獲取用戶信息 -->
- <authentication-manager>
- <authentication-provider user-service-ref="myUserDetailsService">
- <!-- 加密 -->
- <password-encoder hash="md5">
- </password-encoder>
- </authentication-provider>
- </authentication-manager>
- <!-- 被認證請求向登錄界面跳轉采用forward方式 -->
- <beans:bean id="myLoginUrlAuthenticationEntryPoint"
- class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
- <beans:constructor-arg name="loginFormUrl" value="/login"></beans:constructor-arg>
- <beans:property name="useForward" value="true"/>
- </beans:bean>
- </beans:beans>
主要配置
[html] view plain copy
- <pre code_snippet_id="1902646" snippet_file_name="blog_20160927_9_7050170" class="html" name="code"><http auto-config="true" use-expressions="true" entry-point-ref="myLoginUrlAuthenticationEntryPoint">
- <!-- 被認證請求向登錄界面跳轉采用forward方式 -->
- <beans:bean id="myLoginUrlAuthenticationEntryPoint"
- class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
- <beans:constructor-arg name="loginFormUrl" value="/login"></beans:constructor-arg>
- <beans:property name="useForward" value="true"/>
- </beans:bean></pre><br>
- <pre></pre>
- <p>從上面的分析可知,默認情況下采用的是redirect方式,這裏通過配置從而實現了forward方式,這裏還是直接利用的security自帶的類LoginUrlAuthenticationEntryPoint,只不過進行了以上配置:</p>
- <p></p><pre code_snippet_id="1902646" snippet_file_name="blog_20161215_10_1004934" class="java" name="code">/**
- * Performs the redirect (or forward) to the login form URL.
- */
- public void commence(HttpServletRequest request, HttpServletResponse response,
- AuthenticationException authException) throws IOException, ServletException {
- String redirectUrl = null;
- if (useForward) {
- if (forceHttps && "http".equals(request.getScheme())) {
- // First redirect the current request to HTTPS.
- // When that request is received, the forward to the login page will be
- // used.
- redirectUrl = buildHttpsRedirectUrlForRequest(request);
- }
- if (redirectUrl == null) {
- String loginForm = determineUrlToUseForThisRequest(request, response,
- authException);
- if (logger.isDebugEnabled()) {
- logger.debug("Server side forward to: " + loginForm);
- }
- RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
- dispatcher.forward(request, response);
- return;
- }
- }
- else {
- // redirect to login page. Use https if forceHttps true
- redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
- }
- redirectStrategy.sendRedirect(request, response, redirectUrl);
- }</pre><br>
- <p></p>
- <p></p>
- <p></p>
- <p></p>
- <p>登錄成功後的類配置,存入登錄user信息後交給認證成功後的處理類MyAuthenticationSuccessHandler,該類集成了SavedRequestAwareAuthenticationSuccessHandler,他會從緩存中提取請求,從而可以恢復之前請求的數據</p>
- <p></p><pre code_snippet_id="1902646" snippet_file_name="blog_20161215_11_4222490" class="java" name="code">/**
- * 登錄後操作
- *
- * @author HHL
- * @date
- *
- */
- @Component
- public class MyAuthenticationSuccessHandler extends
- SavedRequestAwareAuthenticationSuccessHandler {
- @Autowired
- private IUserService userService;
- @Override
- public void onAuthenticationSuccess(HttpServletRequest request,
- HttpServletResponse response, Authentication authentication)
- throws IOException, ServletException {
- // 認證成功後,獲取用戶信息並添加到session中
- UserDetails userDetails = (UserDetails) authentication.getPrincipal();
- MangoUser user = userService.getUserByName(userDetails.getUsername());
- request.getSession().setAttribute("user", user);
- super.onAuthenticationSuccess(request, response, authentication);
- }
- }</pre><p></p>
- <p></p>
- <p></p>
- <p></p>
- <p>SavedRequestAwareAuthenticationSuccessHandler中的onAuthenticationSuccess方法;</p>
- <p></p><pre code_snippet_id="1902646" snippet_file_name="blog_20161215_12_7440047" class="java" name="code">@Override
- 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);
- }</pre>4.1.3中如果默認不配置的話也是采用的SavedRequestAwareAuthenticationSuccessHandler進行處理,詳情可參見:<a target="_blank" href="http://blog.csdn.net/honghailiang888/article/details/53541664">Spring實戰篇系列----源碼解析Spring Security中的過濾器Filter初始化
- </a><br>
- <br>
- <br>
- 上述實現了跳轉到登錄界面采用forward方式,就是瀏覽器地址欄沒有變化,當然也可采用redirect方式,地址欄變為登錄界面地址欄,當登錄完成後恢復到原先的請求頁面,請求信息會從requestCache中還原回來。可參考<a target="_blank" href="http://blog.csdn.net/honghailiang888/article/details/53520557"> Spring實戰篇系列----spring security4.1.3配置以及踩過的坑</a><br>
- <p></p>
- <p></p>
- <p></p>
- <p></p>
- <p></p>
- <p></p>
- <p><br>
- </p>
- <p><br>
- </p>
- <p>參考:</p>
- <p><a target="_blank" href="https://segmentfault.com/a/1190000004183264">https://segmentfault.com/a/1190000004183264</a></p>
- <p><a target="_blank" href="http://gtbald.iteye.com/blog/1214132">http://gtbald.iteye.com/blog/1214132</a></p>
- <p><br>
- <br>
- <br>
- </p>
- <div style="top:0px"></div>
- <div style="top:4827px"></div>
- <div style="top:3353px"></div>
Spring Security4.1.3實現攔截登錄後向登錄頁面跳轉方式(redirect或forward)返回被攔截界面