在前面的學習中,配置文件中的<http>...</http>都是采用的auto-config="true"這種自動配置模式,根據Spring Security文檔的說明:
------------------
auto-config Automatically registers a login form, BASIC authentication, logout services. If set to "true", all of these capabilities are added (although you can still customize the configuration of each by providing the respective element).
------------------
可以理解為:
1 <http> 2 <form-login /> 3 <http-basic /> 4 <logout /> 5 </http>
下面是Spring Security Filter Chain的列表:
Alias | Filter Class | Namespace Element or Attribute |
---|---|---|
CHANNEL_FILTER |
|
|
SECURITY_CONTEXT_FILTER |
|
|
CONCURRENT_SESSION_FILTER |
|
|
HEADERS_FILTER |
|
|
CSRF_FILTER |
|
|
LOGOUT_FILTER |
|
|
X509_FILTER |
|
|
PRE_AUTH_FILTER |
|
N/A |
CAS_FILTER |
|
N/A |
FORM_LOGIN_FILTER |
|
|
BASIC_AUTH_FILTER |
|
|
SERVLET_API_SUPPORT_FILTER |
|
|
JAAS_API_SUPPORT_FILTER |
|
|
REMEMBER_ME_FILTER |
|
|
ANONYMOUS_FILTER |
|
|
SESSION_MANAGEMENT_FILTER |
|
|
EXCEPTION_TRANSLATION_FILTER |
|
|
FILTER_SECURITY_INTERCEPTOR |
|
|
SWITCH_USER_FILTER |
|
N/A |
其中紅色標出的二個Filter對應的是 “注銷、登錄”,如果不使用auto-config=true,開發人員可以自行“重寫”這二個Filter來達到類似的目的,比如:默認情況下,登錄表單必須使用post方式提交,在一些安全性相對不那么高的場景中(比如:企業內網應用),如果希望通過類似 http://xxx/login?username=abc&password=123的方式直接登錄,可以參考下面的代碼:

1 package com.cnblogs.yjmyzz; 2 3 import javax.servlet.http.HttpServletRequest; 4 import javax.servlet.http.HttpServletResponse; 5 6 //import org.springframework.security.authentication.AuthenticationServiceException; 7 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 import org.springframework.security.core.Authentication; 9 import org.springframework.security.core.AuthenticationException; 10 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 11 12 public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { 13 14 public Authentication attemptAuthentication(HttpServletRequest request, 15 HttpServletResponse response) throws AuthenticationException { 16 17 // if (!request.getMethod().equals("POST")) { 18 // throw new AuthenticationServiceException( 19 // "Authentication method not supported: " 20 // + request.getMethod()); 21 // } 22 23 String username = obtainUsername(request).toUpperCase().trim(); 24 String password = obtainPassword(request); 25 26 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( 27 username, password); 28 29 setDetails(request, authRequest); 30 return this.getAuthenticationManager().authenticate(authRequest); 31 } 32 33 }
即:從UsernamePasswordAuthenticationFilter繼承一個類,然后把關于POST方式判斷的代碼注釋掉即可。默認情況下,Spring Security的用戶名是區分大小寫,如果覺得沒必要,上面的代碼同時還演示了如何在Filter中自動將其轉換成大寫。
默認情況下,登錄成功后,Spring Security有自己的handler處理類,如果想在登錄成功后,加一點自己的處理邏輯,可參考下面的代碼:

1 package com.cnblogs.yjmyzz; 2 3 import java.io.IOException; 4 5 import javax.servlet.ServletException; 6 import javax.servlet.http.HttpServletRequest; 7 import javax.servlet.http.HttpServletResponse; 8 9 import org.springframework.security.core.Authentication; 10 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 11 12 public class CustomLoginHandler extends 13 SavedRequestAwareAuthenticationSuccessHandler { 14 15 @Override 16 public void onAuthenticationSuccess(HttpServletRequest request, 17 HttpServletResponse response, Authentication authentication) 18 throws ServletException, IOException { 19 super.onAuthenticationSuccess(request, response, authentication); 20 21 //這里可以追加開發人員自己的額外處理 22 System.out 23 .println("CustomLoginHandler.onAuthenticationSuccess() is called!"); 24 } 25 26 }
類似的,要自定義LogoutFilter,可參考下面的代碼:

1 package com.cnblogs.yjmyzz; 2 3 import org.springframework.security.web.authentication.logout.LogoutFilter; 4 import org.springframework.security.web.authentication.logout.LogoutHandler; 5 import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; 6 7 public class CustomLogoutFilter extends LogoutFilter { 8 9 public CustomLogoutFilter(String logoutSuccessUrl, LogoutHandler[] handlers) { 10 super(logoutSuccessUrl, handlers); 11 } 12 13 public CustomLogoutFilter(LogoutSuccessHandler logoutSuccessHandler, 14 LogoutHandler[] handlers) { 15 super(logoutSuccessHandler, handlers); 16 } 17 18 }
即:從LogoutFilter繼承一個類,如果還想在退出后加點自己的邏輯(比如注銷后,清空額外的Cookie之類\記錄退出時間、地點之類),可重寫doFilter方法,但不建議這樣,有更好的做法,自行定義logoutSuccessHandler,然后在運行時,通過構造函數注入即可。
下面是自定義退出成功處理的handler示例:

1 package com.cnblogs.yjmyzz; 2 3 import javax.servlet.http.HttpServletRequest; 4 import javax.servlet.http.HttpServletResponse; 5 6 import org.springframework.security.core.Authentication; 7 import org.springframework.security.web.authentication.logout.LogoutHandler; 8 9 public class CustomLogoutHandler implements LogoutHandler { 10 11 public CustomLogoutHandler() { 12 } 13 14 @Override 15 public void logout(HttpServletRequest request, 16 HttpServletResponse response, Authentication authentication) { 17 System.out.println("CustomLogoutSuccessHandler.logout() is called!"); 18 19 } 20 21 }
這二個Filter弄好后,剩下的就是改配置:
1 <beans:beans xmlns="http://www.springframework.org/schema/security" 2 xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://www.springframework.org/schema/beans 4 http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 5 http://www.springframework.org/schema/security 6 http://www.springframework.org/schema/security/spring-security-3.2.xsd"> 7 8 <http entry-point-ref="loginEntryPoint"> 9 <!-- 替換默認的LogoutFilter --> 10 <custom-filter ref="customLogoutFilter" position="LOGOUT_FILTER" /> 11 <!-- 替換默認的LoginFilter --> 12 <custom-filter ref="customLoginFilter" position="FORM_LOGIN_FILTER" /> 13 <intercept-url pattern="/admin" access="ROLE_USER" /> 14 </http> 15 16 <authentication-manager alias="authenticationManager"> 17 ... 18 </authentication-manager> 19 20 <beans:bean id="loginEntryPoint" 21 class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"> 22 <!-- 默認登錄頁的url --> 23 <beans:constructor-arg value="/login" /> 24 </beans:bean> 25 26 <beans:bean id="customLoginFilter" class="com.cnblogs.yjmyzz.CustomLoginFilter"> 27 <!-- 校驗登錄是否有效的虛擬url --> 28 <beans:property name="filterProcessesUrl" value="/checklogin" /> 29 <beans:property name="authenticationManager" ref="authenticationManager" /> 30 <beans:property name="usernameParameter" value="username" /> 31 <beans:property name="passwordParameter" value="password" /> 32 <beans:property name="authenticationSuccessHandler"> 33 <!-- 自定義登錄成功后的處理handler --> 34 <beans:bean class="com.cnblogs.yjmyzz.CustomLoginHandler"> 35 <!-- 登錄成功后的默認url --> 36 <beans:property name="defaultTargetUrl" value="/welcome" /> 37 </beans:bean> 38 </beans:property> 39 <beans:property name="authenticationFailureHandler"> 40 <beans:bean 41 class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"> 42 <!-- 登錄失敗后的默認Url --> 43 <beans:property name="defaultFailureUrl" value="/login?error" /> 44 </beans:bean> 45 </beans:property> 46 </beans:bean> 47 48 <beans:bean id="customLogoutFilter" class="com.cnblogs.yjmyzz.CustomLogoutFilter"> 49 <!-- 處理退出的虛擬url --> 50 <beans:property name="filterProcessesUrl" value="/logout" /> 51 <!-- 退出處理成功后的默認顯示url --> 52 <beans:constructor-arg index="0" value="/login?logout" /> 53 <beans:constructor-arg index="1"> 54 <!-- 退出成功后的handler列表 --> 55 <beans:array> 56 <beans:bean id="securityContextLogoutHandler" 57 class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" /> 58 <!-- 加入了開發人員自定義的退出成功處理 --> 59 <beans:bean id="customLogoutSuccessHandler" class="com.cnblogs.yjmyzz.CustomLogoutHandler" /> 60 </beans:array> 61 </beans:constructor-arg> 62 </beans:bean> 63 64 </beans:beans>
用戶輸入“用戶名、密碼”,并點擊完登錄后,最終實現校驗的是AuthenticationProvider,而且一個webApp中可以同時使用多個Provider,下面是一個自定義Provider的示例代碼:

1 package com.cnblogs.yjmyzz; 2 3 import java.util.ArrayList; 4 import java.util.Arrays; 5 import java.util.Collection; 6 7 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 8 import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; 9 import org.springframework.security.core.AuthenticationException; 10 import org.springframework.security.core.GrantedAuthority; 11 import org.springframework.security.core.authority.SimpleGrantedAuthority; 12 import org.springframework.security.core.userdetails.User; 13 import org.springframework.security.core.userdetails.UserDetails; 14 15 public class CustomAuthenticationProvider extends 16 AbstractUserDetailsAuthenticationProvider { 17 18 @Override 19 protected void additionalAuthenticationChecks(UserDetails userDetails, 20 UsernamePasswordAuthenticationToken authentication) 21 throws AuthenticationException { 22 //如果想做點額外的檢查,可以在這個方法里處理,校驗不通時,直接拋異常即可 23 System.out 24 .println("CustomAuthenticationProvider.additionalAuthenticationChecks() is called!"); 25 } 26 27 @Override 28 protected UserDetails retrieveUser(String username, 29 UsernamePasswordAuthenticationToken authentication) 30 throws AuthenticationException { 31 32 System.out 33 .println("CustomAuthenticationProvider.retrieveUser() is called!"); 34 35 String[] whiteLists = new String[] { "ADMIN", "SUPERVISOR", "JIMMY" }; 36 37 // 如果用戶在白名單里,直接放行(注:僅僅只是演示,千萬不要在實際項目中這么干!) 38 if (Arrays.asList(whiteLists).contains(username)) { 39 Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); 40 authorities.add(new SimpleGrantedAuthority("ROLE_USER")); 41 UserDetails user = new User(username, "whatever", authorities); 42 return user; 43 } 44 45 return new User(username, "no-password", false, false, false, false, 46 new ArrayList<GrantedAuthority>()); 47 48 } 49 50 }
這里僅僅只是出于演示目的,人為留了一個后門,只要用戶名在白名單之列,不管輸入什么密碼,都可以通過!(再次提示:只是出于演示目的,千萬不要在實際項目中使用)
相關的配置節點修改如下:
1 <authentication-manager alias="authenticationManager"> 2 <authentication-provider> 3 <user-service> 4 <user name="yjmyzz" password="123456" authorities="ROLE_USER" /> 5 </user-service> 6 </authentication-provider> 7 <!-- 加入開發人員自定義的Provider --> 8 <authentication-provider ref="customProvider" /> 9 </authentication-manager> 10 11 <beans:bean id="customProvider" 12 class="com.cnblogs.yjmyzz.CustomAuthenticationProvider" />
運行時,Spring Security將會按照順序,依次從上向下調用所有Provider,只要任何一個Provider校驗通過,整個認證將通過。這也意味著:用戶yjmyzz/123456以及白名單中的用戶名均可以登錄系統。這是一件很有意思的事情,試想一下,如果有二個現成的系統,各有自己的用戶名/密碼(包括不同的存儲機制),想把他們集成在一個登錄頁面使用,技術上講,只要實現二個Provider各自對應不同的處理,可以很輕易的實現多個系統的認證集成。(注:當然實際應用中,多個系統的認證集成,更多的是采用SSO來處理,這里只是提供了另一種思路)
最后來看下如何自定義AuthenticationToken,如果我們想在登錄頁上加一些額外的輸入項(比如:驗證碼,安全問題之類),
為了能讓這些額外添加的輸入項,傳遞到Provider中參與驗證,就需要對UsernamePasswordAuthenticationToken進行擴展,參考代碼如下:

1 package com.cnblogs.yjmyzz; 2 3 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 4 5 public class CustomAuthenticationToken extends 6 UsernamePasswordAuthenticationToken { 7 8 private static final long serialVersionUID = 5414106440823275021L; 9 10 public CustomAuthenticationToken(String principal, String credentials, 11 Integer questionId, String answer) { 12 super(principal, credentials); 13 this.answer = answer; 14 this.questionId = questionId; 15 } 16 17 private String answer; 18 private Integer questionId; 19 20 public String getAnswer() { 21 return answer; 22 } 23 24 public void setAnswer(String answer) { 25 this.answer = answer; 26 } 27 28 public Integer getQuestionId() { 29 return questionId; 30 } 31 32 public void setQuestionId(Integer questionId) { 33 this.questionId = questionId; 34 } 35 36 }
這里擴展了二個屬性:questionId、answer,為了方便后面“詩句問題"的回答進行判斷,還得先做點其它準備工作

1 package com.cnblogs.yjmyzz; 2 3 import java.util.Hashtable; 4 5 public class LoginQuestion { 6 7 private static Hashtable<Integer, String> questionTable = new Hashtable<Integer, String>(); 8 9 public static Hashtable<Integer, String> getQuestions() { 10 if (questionTable.size() <= 0) { 11 questionTable.put(1, "葡萄美酒夜光杯/欲飲琵琶馬上催"); 12 questionTable.put(2, "故人西辭黃鶴樓/煙花三月下揚州"); 13 questionTable.put(3, "孤帆遠影碧空盡/唯見長江天際流"); 14 questionTable.put(4, "相見時難別亦難/東風無力百花殘"); 15 questionTable.put(5, "漁翁夜傍西巖宿/曉汲清湘燃楚竹"); 16 } 17 return questionTable; 18 } 19 20 }
預定義了幾句唐詩,key即為questionId,value為 "題目/答案"格式。此外,如果答錯了,為了方便向用戶提示錯誤原因,還要定義一個異常類:(注:Spring Security中,所有驗證失敗,都是通過直接拋異常來處理的)

1 package com.cnblogs.yjmyzz; 2 3 import org.springframework.security.core.AuthenticationException; 4 5 public class BadAnswerException extends AuthenticationException { 6 7 private static final long serialVersionUID = -3333012976129153127L; 8 9 public BadAnswerException(String msg) { 10 super(msg); 11 12 } 13 14 }
原來的CustomLoginFilter也要相應的修改,以接收額外添加的二個參數:

1 package com.cnblogs.yjmyzz; 2 3 import java.io.UnsupportedEncodingException; 4 5 import javax.servlet.http.HttpServletRequest; 6 import javax.servlet.http.HttpServletResponse; 7 import org.springframework.security.core.Authentication; 8 import org.springframework.security.core.AuthenticationException; 9 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 10 11 public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter { 12 13 public Authentication attemptAuthentication(HttpServletRequest request, 14 HttpServletResponse response) throws AuthenticationException { 15 16 //解決中文詩句的post亂碼問題 17 try { 18 request.setCharacterEncoding("UTF-8"); 19 } catch (UnsupportedEncodingException e) { 20 e.printStackTrace(); 21 } 22 23 // if (!request.getMethod().equals("POST")) { 24 // throw new AuthenticationServiceException( 25 // "Authentication method not supported: " 26 // + request.getMethod()); 27 // } 28 29 String username = obtainUsername(request).toUpperCase().trim(); 30 String password = obtainPassword(request); 31 //獲取用戶輸入的下一句答案 32 String answer = obtainAnswer(request); 33 //獲取問題Id(即: hashTable的key) 34 Integer questionId = obtainQuestionId(request); 35 36 //這里將原來的UsernamePasswordAuthenticationToken換成我們自定義的CustomAuthenticationToken 37 CustomAuthenticationToken authRequest = new CustomAuthenticationToken( 38 username, password, questionId, answer); 39 40 //這里就將token傳到后續驗證環節了 41 setDetails(request, authRequest); 42 return this.getAuthenticationManager().authenticate(authRequest); 43 } 44 45 protected String obtainAnswer(HttpServletRequest request) { 46 return request.getParameter(answerParameter); 47 } 48 49 protected Integer obtainQuestionId(HttpServletRequest request) { 50 return Integer.parseInt(request.getParameter(questionIdParameter)); 51 } 52 53 private String questionIdParameter = "questionId"; 54 private String answerParameter = "answer"; 55 56 public String getQuestionIdParameter() { 57 return questionIdParameter; 58 } 59 60 public void setQuestionIdParameter(String questionIdParameter) { 61 this.questionIdParameter = questionIdParameter; 62 } 63 64 public String getAnswerParameter() { 65 return answerParameter; 66 } 67 68 public void setAnswerParameter(String answerParameter) { 69 this.answerParameter = answerParameter; 70 } 71 72 }
現在,CustomAuthenticationProvider中的additionalAuthenticationChecks方法中,就能拿到用戶提交的下一句答案,進行相關驗證了:

1 @Override 2 protected void additionalAuthenticationChecks(UserDetails userDetails, 3 UsernamePasswordAuthenticationToken authentication) 4 throws AuthenticationException { 5 // 轉換為自定義的token 6 CustomAuthenticationToken token = (CustomAuthenticationToken) authentication; 7 String poem = LoginQuestion.getQuestions().get(token.getQuestionId()); 8 // 校驗下一句的答案是否正確 9 if (!poem.split("/")[1].equals(token.getAnswer())) { 10 throw new BadAnswerException("the answer is wrong!"); 11 } 12 13 }
最后來處理前端的login頁面及Action

1 package com.cnblogs.yjmyzz; 2 3 import java.util.Random; 4 5 import javax.servlet.http.HttpServletRequest; 6 7 import org.springframework.security.authentication.BadCredentialsException; 8 import org.springframework.security.authentication.LockedException; 9 import org.springframework.stereotype.Controller; 10 import org.springframework.web.bind.annotation.RequestMapping; 11 import org.springframework.web.bind.annotation.RequestMethod; 12 import org.springframework.web.bind.annotation.RequestParam; 13 import org.springframework.web.servlet.ModelAndView; 14 15 @Controller 16 public class HelloController { 17 18 @RequestMapping(value = { "/", "/welcome" }, method = RequestMethod.GET) 19 public ModelAndView welcome() { 20 21 ModelAndView model = new ModelAndView(); 22 model.addObject("title", 23 "Welcome - Spring Security Custom login/logout Filter"); 24 model.addObject("message", "This is welcome page!"); 25 model.setViewName("hello"); 26 return model; 27 28 } 29 30 @RequestMapping(value = "/admin", method = RequestMethod.GET) 31 public ModelAndView admin() { 32 33 ModelAndView model = new ModelAndView(); 34 model.addObject("title", 35 "Admin - Spring Security Custom login/logout Filter"); 36 model.addObject("message", "This is protected page!"); 37 model.setViewName("admin"); 38 39 return model; 40 41 } 42 43 @RequestMapping(value = "/login", method = RequestMethod.GET) 44 public ModelAndView login( 45 @RequestParam(value = "error", required = false) String error, 46 @RequestParam(value = "logout", required = false) String logout, 47 HttpServletRequest request) { 48 49 ModelAndView model = new ModelAndView(); 50 if (error != null) { 51 model.addObject("error", 52 getErrorMessage(request, "SPRING_SECURITY_LAST_EXCEPTION")); 53 } 54 55 if (logout != null) { 56 model.addObject("msg", "You've been logged out successfully."); 57 } 58 59 //從預定義的詩句中,隨機挑一個上句 60 Random rnd = new Random(); 61 int questionId = rnd.nextInt(LoginQuestion.getQuestions().size() + 1); 62 if (questionId == 0) { 63 questionId = 1; 64 } 65 model.addObject("questionId", questionId); 66 model.addObject("question", LoginQuestion.getQuestions() 67 .get(questionId).split("/")[0]); 68 69 model.setViewName("login"); 70 71 return model; 72 73 } 74 75 private String getErrorMessage(HttpServletRequest request, String key) { 76 Exception exception = (Exception) request.getSession() 77 .getAttribute(key); 78 String error = ""; 79 if (exception instanceof BadCredentialsException) { 80 error = "Invalid username and password!"; 81 } else if (exception instanceof BadAnswerException) { 82 error = exception.getMessage(); 83 } else if (exception instanceof LockedException) { 84 error = exception.getMessage(); 85 } else { 86 error = "Invalid username and password!"; 87 } 88 89 return error; 90 } 91 92 }
代碼很簡單,從預定義的詩句中,隨機挑一句,并把questionId及question放到model中,傳給view

1 <%@ page language="java" contentType="text/html; charset=UTF-8" 2 pageEncoding="UTF-8"%> 3 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 4 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 5 <html> 6 <head> 7 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 8 <title>Login Page</title> 9 <link rel="Stylesheet" type="text/css" 10 href="${pageContext.request.contextPath}/resources/css/login.css" /> 11 </head> 12 <body onload='document.loginForm.username.focus();'> 13 <h1>Spring Security CustomFilter(XML)</h1> 14 15 <div id="login-box"> 16 17 <c:if test="${not empty error}"> 18 <div class="error">${error}</div> 19 </c:if> 20 <c:if test="${not empty msg}"> 21 <div class="msg">${msg}</div> 22 </c:if> 23 <form name='loginForm' action="<c:url value='checklogin' />" 24 method='POST'> 25 <table> 26 <tr> 27 <td>User:</td> 28 <td><input type='text' name='username' value=''></td> 29 </tr> 30 <tr> 31 <td>Password:</td> 32 <td><input type='password' name='password' /></td> 33 </tr> 34 <tr> 35 <td valign="top">Question:</td> 36 <td>詩句<span style="color:red">"${question}"</span><br/>的下一句是什么?<br /> <input type='text' 37 name='answer' value=''> 38 </td> 39 </tr> 40 <tr> 41 <td colspan='2'><input name="submit" type="submit" 42 value="submit" /></td> 43 </tr> 44 </table> 45 <input type="hidden" name="${_csrf.parameterName}" 46 value="${_csrf.token}" /> <input type="hidden" name="questionId" 47 value="${questionId}" /> 48 </form> 49 </div> 50 </body> 51 </html>
ok,完工!
不過,有一個小問題要提醒一下:對本文所示案例而言,因為同時應用了二個Provider,一個是默認的,一個是我們后來自定義的,而對"下一句"的答案驗證,只在CustomAuthenticationProvider中做了處理,換句話說,如果用戶在界面上輸入的用戶名/密碼是yjmyzz/123456,根據前面講到的規則,默認的Provider會先起作用,認證通過直接忽略”下一句“的驗證,只有輸入白名單中的用戶名時,才會走CustomAuthenticationProvider的驗證流程。
國際慣例,最后附上示例源代碼:SpringSecurity-CustomFilter.zip
文章列表