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的恢復。
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的實現比較簡單,這裡只列出介面的宣告:
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 對異常的處理是通過這兩個處理類實現的,處理規則很簡單:
規則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錯誤碼。
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, 該類的處理是轉發或重定向到登入頁面
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。
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版本
配置檔案:完整的
<?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>
主要配置
<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>
從上面的分析可知,預設情況下采用的是redirect方式,這裡通過配置從而實現了forward方式,這裡還是直接利用的security自帶的類LoginUrlAuthenticationEntryPoint,只不過進行了以上配置:
/**
* 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);
}
登入成功後的類配置,存入登入user資訊後交給認證成功後的處理類MyAuthenticationSuccessHandler,該類集成了SavedRequestAwareAuthenticationSuccessHandler,他會從快取中提取請求,從而可以恢復之前請求的資料
/**
* 登入後操作
*
* @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);
}
}
SavedRequestAwareAuthenticationSuccessHandler中的onAuthenticationSuccess方法;
@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);
}
4.1.3中如果預設不配置的話也是採用的SavedRequestAwareAuthenticationSuccessHandler進行處理,詳情可參見:Spring實戰篇系列----原始碼解析Spring Security中的過濾器Filter初始化
上述實現了跳轉到登入介面採用forward方式,就是瀏覽器位址列沒有變化,當然也可採用redirect方式,位址列變為登入介面位址列,當登入完成後恢復到原先的請求頁面,請求資訊會從requestCache中還原回來。可參考 Spring實戰篇系列----spring security4.1.3配置以及踩過的坑
參考: