在前面的学习中,配置文件中的<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 | ChannelProcessingFilter | http/intercept-url@requires-channel |
SECURITY_CONTEXT_FILTER | SecurityContextPersistenceFilter | http |
CONCURRENT_SESSION_FILTER | ConcurrentSessionFilter | session-management/concurrency-control |
HEADERS_FILTER | HeaderWriterFilter | http/headers |
CSRF_FILTER | CsrfFilter | http/csrf |
LOGOUT_FILTER | LogoutFilter | http/logout |
X509_FILTER | X509AuthenticationFilter | http/x509 |
PRE_AUTH_FILTER | AstractPreAuthenticatedProcessingFilter Subclasses | N/A |
CAS_FILTER | CasAuthenticationFilter | N/A |
FORM_LOGIN_FILTER | UsernamePasswordAuthenticationFilter | http/form-login |
BASIC_AUTH_FILTER | BasicAuthenticationFilter | http/http-basic |
SERVLET_API_SUPPORT_FILTER | SecurityContextHolderAwareRequestFilter | http/@servlet-api-provision |
JAAS_API_SUPPORT_FILTER | JaasApiIntegrationFilter | http/@jaas-api-provision |
REMEMBER_ME_FILTER | RememberMeAuthenticationFilter | http/remember-me |
ANONYMOUS_FILTER | AnonymousAuthenticationFilter | http/anonymous |
SESSION_MANAGEMENT_FILTER | SessionManagementFilter | session-management |
EXCEPTION_TRANSLATION_FILTER | ExceptionTranslationFilter | http |
FILTER_SECURITY_INTERCEPTOR | FilterSecurityInterceptor | http |
SWITCH_USER_FILTER | SwitchUserFilter | 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