Spring Security笔记:自定义Login/Logout Filter、AuthenticationProvider、AuthenticationToken

http://www.cnblogs.com/yjmyzz/p/3888507.html 来自

https://github.com/spring-projects/



在前面的学习中,配置文件中的...都是采用的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的列表:

Table 1. Standard Filter Aliases and Ordering
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的方式直接登录,可以参考下面的代码:

View Code

即:从UsernamePasswordAuthenticationFilter继承一个类,然后把关于POST方式判断的代码注释掉即可。默认情况下,Spring Security的用户名是区分大小写,如果觉得没必要,上面的代码同时还演示了如何在Filter中自动将其转换成大写。

默认情况下,登录成功后,Spring Security有自己的handler处理类,如果想在登录成功后,加一点自己的处理逻辑,可参考下面的代码:

View Code

类似的,要自定义LogoutFilter,可参考下面的代码:

View Code

即:从LogoutFilter继承一个类,如果还想在退出后加点自己的逻辑(比如注销后,清空额外的Cookie之类\记录退出时间、地点之类),可重写doFilter方法,但不建议这样,有更好的做法,自行定义logoutSuccessHandler,然后在运行时,通过构造函数注入即可。

下面是自定义退出成功处理的handler示例:

View Code

这二个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         
10         <custom-filter ref="customLogoutFilter" position="LOGOUT_FILTER" />
11         
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         
23         <beans:constructor-arg value="/login" />
24     beans:bean>
25 
26     <beans:bean id="customLoginFilter" class="com.cnblogs.yjmyzz.CustomLoginFilter">
27         
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             
34             <beans:bean class="com.cnblogs.yjmyzz.CustomLoginHandler">
35                 
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                 
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         
50         <beans:property name="filterProcessesUrl" value="/logout" />
51         
52         <beans:constructor-arg index="0" value="/login?logout" />
53         <beans:constructor-arg index="1">
54             
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的示例代码:

View Code

这里仅仅只是出于演示目的,人为留了一个后门,只要用户名在白名单之列,不管输入什么密码,都可以通过!(再次提示:只是出于演示目的,千万不要在实际项目中使用

相关的配置节点修改如下:

复制代码
 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         
 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,如果我们想在登录页上加一些额外的输入项(比如:验证码,安全问题之类),

Spring Security笔记:自定义Login/Logout Filter、AuthenticationProvider、AuthenticationToken_第1张图片

为了能让这些额外添加的输入项,传递到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 questionTable = new Hashtable();
 8 
 9     public static Hashtable 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 Pagetitle>
 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=""
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的验证流程。

你可能感兴趣的:(Spring,Security3)