研究了好长时间,不知道从哪里下手。新的版本,很多东西在网上找不到,只能看他们的文档,当然这些文档相当不错,就看是否耐心的研究了!总是有急躁的心理作祟,不能专心研读,却处处碰壁,效率上反而未达预期效果!
终于,在无数次的沮丧下,稍微看到了点光亮!前面的文章太过皮毛,接下来的一些,希望能更加实际的,更加深入的分析每一个过程!
一直通过默认配置进行设置:
namespace(是security 3.0,网上也看到一些兄弟描述的是3.0,但是总是不符合我这里的namespace配置):
<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.0.xsd">
按照默认配置的http(这是用来根据namespace设置的基本的security过滤器chain):
auto-config=true时,就相当于
<http>
<form-login />
<http-basic />
<logout />
</http>
也就是使用了默认的过滤器。
我最开始的想法是能够把本地的login信息(不是调用spring security的login方法),传入到spring security的验证过滤器里面。
这里有一个比较关键的问题,就是封装他们的过滤器(或者仅仅是知道他们到底是哪些过滤器在起作用):
表1
Alias | Filter Class | Namespace Element or
Attribute |
CHANNEL_FILTER |
ChannelProcessingFilter |
http/intercept-url@requires-channel |
CONCURRENT_SESSION_FILTER |
ConcurrentSessionFilter
|
session-management/concurrency-control |
SECURITY_CONTEXT_FILTER |
SecurityContextPersistenceFilter |
http |
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 |
SecurityContextHolderAwareFilter |
http/@servlet-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 |
|
|
|
(最开始看的时候,把这个表格忽略了,现在看来这些就是我们想要的!)
我们的验证过程,就是按照这样的顺序进行的。自上而下进行。
如果我们要自己定制相应的验证处理方法(在过滤器里面),我们就可以对照上面的过滤器,覆盖相应的接口方法。
根据我的处理过程,主要是用户名密码的验证过程,我大体描述一下自己的配置和处理过程:
1.配置namespace的标签:
<http use-expressions="true" ><!-- This is not the default value -->
<custom-filter position="FORM_LOGIN_FILTER" ref="myFilter"/> <!--This is my own filter which just extends AbstractAuthenticationProcessingFilter as what UsernamePasswordAuthenticationFilter does.-->
<intercept-url pattern="/test/**" access="hasRole('ROLE_MY')"/><!-- I tested that what is role,and how I can use it.So ROLE_MY is just a role name I defined.-->
<intercept-url pattern="/login.jsp*" access="permitAll" />
<logout />
<anonymous />
<http-basic />
</http>
这里的问题是,要定制自己的过滤器,就要通过<custom-filter/>,然后对照 表1 中指定的position,覆盖默认的filter。
2.我的form-login filter配置(这些配置都是在application-security.xml文件中)为:
<beans:bean id="myFilter"
class="com.saveworld.authentication.filters.MyUsernamePasswordAuthenticationFilter">
<beans:property name="defaultTargetUrl" value="/default.jsp" />
<beans:property name="defaultFailureUrl" value="/error.jsp" />
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="filterProcessesUrl" value="/j_spring_security_check" />
<beans:property name="continueChainBeforeSuccessfulAuthentication" value="false" />
</beans:bean>
NOTE:
在这里有个问题就是: filter position conflicts!
如果使用这样的配置
<http auto-config='true'>
<custom-filter position="FORM_LOGIN_FILTER" ref="myFilter"/>
</http>
自定义的filter对应的position是FORM_LOGIN_FILTER
但是因为使用了auto-config='true',所以默认有<form-login />,which is on the position FORM_LOGIN_FILTER!
这时就会出现position conflicts问题了。当然,如果你没有设置auto-config='true',但是却自己设置了<form-login />,呵呵,这个情况就是自己大意了,还是有了position conflicts的异常,所以,好好看看上面的表格是相当必要的,看清楚每个position默认都对应那些namespace,都是对应的哪些filter!
接着:
我的类MyUsernamePasswordAuthenticationFilter实现(我的说明方式就按照哪里需要,哪里加入的方式了):
package com.saveworld.authentication.filters;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.NullRememberMeServices;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.util.TextEscapeUtils;
import org.springframework.util.Assert;
import com.saveworld.authentication.handlers.MySavedRequestAwareAuthenticationSuccessHandler;
import com.saveworld.authentication.handlers.MySimpleUrlAuthenticationFailureHandler;
public class MyUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter{
//~ Static fields/initializers =====================================================================================
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
public static final String SPRING_SECURITY_LAST_USERNAME_KEY = "SPRING_SECURITY_LAST_USERNAME";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
private boolean allowSessionCreation = true;
private String defaultTargetUrl = "/";
private String defaultFailureUrl = "/login.jsp";
private AuthenticationSuccessHandler successHandler = null;
private AuthenticationFailureHandler failureHandler = null;
private RememberMeServices rememberMeServices = null;
//~ Constructors ===================================================================================================
public MyUsernamePasswordAuthenticationFilter() {
//初始化
super("/j_spring_security_check");
this.rememberMeServices = (super.getRememberMeServices() == null)
? new NullRememberMeServices():super.getRememberMeServices();
}
//~ Methods ========================================================================================================
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Place the last username attempted into HttpSession for views
HttpSession session = request.getSession(false);
if (session != null || getAllowSessionCreation()) {
request.getSession().setAttribute(SPRING_SECURITY_LAST_USERNAME_KEY, TextEscapeUtils.escapeEntities(username));
}
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
Authentication authResult) throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
if(successHandler == null){
successHandler = new MySavedRequestAwareAuthenticationSuccessHandler(getDefaultTargetUrl());
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
public void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
if(failureHandler == null){
failureHandler = new MySimpleUrlAuthenticationFailureHandler(getDefaultFailureUrl());
}
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString());
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler" + failureHandler);
}
HttpSession session = request.getSession(false);
if (session != null || allowSessionCreation) {
request.getSession().setAttribute(SPRING_SECURITY_LAST_EXCEPTION_KEY, failed);
}
rememberMeServices.loginFail(request, response);
failureHandler.onAuthenticationFailure(request, response, failed);
}
/**
* Enables subclasses to override the composition of the password, such as by including additional values
* and a separator.<p>This might be used for example if a postcode/zipcode was required in addition to the
* password. A delimiter such as a pipe (|) should be used to separate the password and extended value(s). The
* <code>AuthenticationDao</code> will need to generate the expected password in a corresponding manner.</p>
*
* @param request so that request attributes can be retrieved
*
* @return the password that will be presented in the <code>Authentication</code> request token to the
* <code>AuthenticationManager</code>
*/
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
/**
* Enables subclasses to override the composition of the username, such as by including additional values
* and a separator.
*
* @param request so that request attributes can be retrieved
*
* @return the username that will be presented in the <code>Authentication</code> request token to the
* <code>AuthenticationManager</code>
*/
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
/**
* Provided so that subclasses may configure what is put into the authentication request's details
* property.
*
* @param request that an authentication request is being created for
* @param authRequest the authentication request object that should have its details set
*/
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
/**
* Sets the parameter name which will be used to obtain the username from the login request.
*
* @param usernameParameter the parameter name. Defaults to "j_username".
*/
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}
/**
* Sets the parameter name which will be used to obtain the password from the login request..
*
* @param passwordParameter the parameter name. Defaults to "j_password".
*/
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}
/**
* Defines whether only HTTP POST requests will be allowed by this filter.
* If set to true, and an authentication request is received which is not a POST request, an exception will
* be raised immediately and authentication will not be attempted. The <tt>unsuccessfulAuthentication()</tt> method
* will be called as if handling a failed authentication.
* <p>
* Defaults to <tt>true</tt> but may be overridden by subclasses.
*/
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getUsernameParameter() {
return usernameParameter;
}
public final String getPasswordParameter() {
return passwordParameter;
}
public String getDefaultTargetUrl() {
return defaultTargetUrl;
}
public void setDefaultTargetUrl(String defaultTargetUrl) {
this.defaultTargetUrl = defaultTargetUrl;
}
public String getDefaultFailureUrl() {
return defaultFailureUrl;
}
public void setDefaultFailureUrl(String defaultFailureUrl) {
this.defaultFailureUrl = defaultFailureUrl;
}
}
这里要关注的就是几个字段:
<beans:property name="defaultTargetUrl" value="/default.jsp" />
<beans:property name="defaultFailureUrl" value="/error.jsp" />
这两个字段是指定验证成功或失败后转向的页面,这里要注意是以“/”开头,否则在AbstractAuthenticationTargetUrlRequestHandler中调用setDefaultTargetUrl方法时会抛出"defaultTarget must start with '/' or with 'http(s)'"的异常!
默认情况下,FORM_LOGIN_FILTER对应的target url和failure url都是通过 <form-login />中的
default-target-url,authentication-failure-url指定,也可以通过指定authentication-success-handler-ref和authentication-failure-handler-ref来实现认证成功和失败之后的处理方式.在我的filter中,是自定义了两个handler分别对应成功的和失败的验证。
3.用户信息获取和验证:
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref='myUserDetailsService'/>
</authentication-manager>
<beans:bean id="myUserDetailsService" class="com.saveworld.userdetails.MyUserDetailsService"></beans:bean>
这个指定的authentication-manager是使用默认的ProviderManager,这个manager是在哪里使用的呢?
看看MyUsernamePasswordAuthenticationFilter中的attemptAuthentication方法的最后一行,这里是获取指定的authentication-manager。getAuthenticationManager是从父类AbstractAuthenticationProcessingFilter继承过来的。所以,我们的<authentication-manager alias="authenticationManager">中就指定了一个别名authenticationManager,在myfilter中设置属性的引用<beans:property name="authenticationManager" ref="authenticationManager" />,然后我们就可以通过Provider引用我们自己的用户信息验证service了(eg:用户信息获取和验证)!这里实际是使用了Method Template模式(AbstractAuthenticationManager中设定模板函数doAuthentication,ProviderManager中做了实现)。
这里难免要说明一下,我们的Service是如何被调用的,我们做了配置<authentication-provider user-service-ref='myUserDetailsService'/>
指定了我们的UserDetailsService,类实现(为了测试和理解,The easier the better!):
package com.saveworld.userdetails;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public class MyUserDetailsService implements UserDetailsService{
private HashMap<String, User> userDetails = new HashMap<String, User>();
private HashMap<String, List<GrantedAuthority>> userAuthorities = new HashMap<String, List<GrantedAuthority>>();
public MyUserDetailsService(){
//Make up a user named 'rod' with 'rod' as his password!
//
String username = "rod";
String password = "1";
boolean enabled = true;
//purview for rod
GrantedAuthority specAuth = new GrantedAuthorityImpl("ROLE_MY");
List<GrantedAuthority> rodsAuthsList = new ArrayList<GrantedAuthority>();
rodsAuthsList.add(specAuth);
// userAuthorities.put("rod", rodsAuthsList);
userDetails.put("rod", new User(username, password, enabled, true, true, true, rodsAuthsList));
}
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
System.out.println("验证从此地过了一遭");
return userDetails.get(username);
}
}
通过DaoAuthenticationProvider中的userDetailsService关联我们的UserDetailsService(不得不提的是,AbstractUserDetailsAuthenticationProvider中有设定了模板函数retrieveUser,DaoAuthenticationProvider进行了实现,通过retrieveUser方法调用UserDetailsService.loadUserByUsername,然后在AbstractUserDetailsAuthenticationProvider.authenticate方法进行验证)。
接下来就是看验证的结果了,是否成功,进入filter chain中。
这一切就这么有条不紊的进行了!呵呵,总算是有点成果了!有了一点点感性的认识了!上面的描述中难免会有些混乱,但是尽量是哪里需要,哪里就做说明!
下篇中说明entrypoint的用处吧!