原文》http://www.cnblogs.com/yjmyzz/p/how-to-custom-filter-provider-and-token-in-spring-security3.html
Spring Security笔记:自定义Login/Logout Filter、AuthenticationProvider、AuthenticationToken
在前面的学习中,配置文件中的<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进行扩展,参考代码如下:
这里扩展了二个属性:questionId、answer,为了方便后面“诗句问题"的回答进行判断,还得先做点其它准备工作
预定义了几句唐诗,key即为questionId,value为 "题目/答案"格式。此外,如果答错了,为了方便向用户提示错误原因,还要定义一个异常类:(注:Spring Security中,所有验证失败,都是通过直接抛异常来处理的)
原来的CustomLoginFilter也要相应的修改,以接收额外添加的二个参数:
现在,CustomAuthenticationProvider中的additionalAuthenticationChecks方法中,就能拿到用户提交的下一句答案,进行相关验证了:
最后来处理前端的login页面及Action
代码很简单,从预定义的诗句中,随机挑一句,并把questionId及question放到model中,传给view
ok,完工!
不过,有一个小问题要提醒一下:对本文所示案例而言,因为同时应用了二个Provider,一个是默认的,一个是我们后来自定义的,而对"下一句"的答案验证,只在CustomAuthenticationProvider中做了处理,换句话说,如果用户在界面上输入的用户名/密码是yjmyzz/123456,根据前面讲到的规则,默认的Provider会先起作用,认证通过直接忽略”下一句“的验证,只有输入白名单中的用户名时,才会走CustomAuthenticationProvider的验证流程。
国际惯例,最后附上示例源代码:SpringSecurity-CustomFilter.zip