首页 > 代码库 > Spring Security笔记:自定义Login/Logout Filter、AuthenticationProvider、AuthenticationToken

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的列表:

Table 1. Standard Filter Aliases and Ordering
AliasFilter ClassNamespace 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 }
View Code

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

类似的,要自定义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 }
View Code

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

这二个Filter弄好后,剩下的就是改配置:

 1 <?xml version="1.0"?> 2 <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-3.0.xsd     http://www.springframework.org/schema/security     http://www.springframework.org/schema/security/spring-security-3.2.xsd"> 3     <http entry-point-ref="loginEntryPoint"> 4         <custom-filter ref="customLogoutFilter" position="LOGOUT_FILTER"/> 5         <custom-filter ref="customLoginFilter" position="FORM_LOGIN_FILTER"/> 6         <intercept-url pattern="/admin" access="ROLE_USER"/> 7     </http> 8     <authentication-manager alias="authenticationManager"> 9         ...    10     </authentication-manager>11     <beans:bean id="loginEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">12         <beans:constructor-arg value="/login"/>13         <!--登录url-->14     </beans:bean>15     <beans:bean id="customLoginFilter" class="com.cnblogs.yjmyzz.CustomLoginFilter">16         <!--登录验证的内部虚拟url-->17         <beans:property name="filterProcessesUrl" value="/checklogin"/>18         <beans:property name="authenticationManager" ref="authenticationManager"/>19         <beans:property name="usernameParameter" value="username"/>20         <beans:property name="passwordParameter" value="password"/>21         <beans:property name="authenticationSuccessHandler">22             <beans:bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">23                 <!--登录成功后,默认显示页-->24                 <beans:property name="defaultTargetUrl" value="/welcome"/>25             </beans:bean>26         </beans:property>27         <beans:property name="authenticationFailureHandler">28             <beans:bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">29                 <!--登录失败后的显示页-->30                 <beans:property name="defaultFailureUrl" value="/login?error"/>31             </beans:bean>32         </beans:property>33     </beans:bean>34     <beans:bean id="customLogoutFilter" class="com.cnblogs.yjmyzz.CustomLogoutFilter">35         <!--注销处理的内部虚拟url-->36         <beans:property name="filterProcessesUrl" value="/logout"/>37         <!--注销成功后的显示页-->38         <beans:constructor-arg index="0" value="/login?logout"/>39         <beans:constructor-arg index="1">40             <beans:list>41                 <beans:bean id="securityContextLogoutHandler" class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>42                 <!--这里还可以继续添加开发人员自定义的注销成功处理Handler-->43             </beans:list>44         </beans:constructor-arg>45     </beans:bean>46 </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 extends16         AbstractUserDetailsAuthenticationProvider {17 18     @Override19     protected void additionalAuthenticationChecks(UserDetails userDetails,20             UsernamePasswordAuthenticationToken authentication)21             throws AuthenticationException {22         //如果想做点额外的检查,可以在这个方法里处理,校验不通时,直接抛异常即可23         System.out24                 .println("CustomAuthenticationProvider.additionalAuthenticationChecks() is called!");25     }26 27     @Override28     protected UserDetails retrieveUser(String username,29             UsernamePasswordAuthenticationToken authentication)30             throws AuthenticationException {31 32         System.out33                 .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 }
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         <!-- 加入开发人员自定义的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各自对应不同的处理,然后参考刚才的处理就能很轻易的实现认证集成。