1. 程式人生 > >Spring Security 整體配置

Spring Security 整體配置

第一部分: web.xml的配置

使用過SpringSecurity的朋友都知道,首先需要在web.xml進行以下配置:

<filter>
  <filter-name>springSecurityFilterChain</filter-name>
  <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> 
</filter>
 
<filter-mapping>
  <filter-name>springSecurityFilterChain</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping></span>

第二部分: applicationContext-security.xml的配置

1. 配置一些不需要安全驗證的資源

<sec:http pattern="/login" security="none"></sec:http>
<sec:http pattern="/register" security="none"></sec:http>
<sec:http pattern="/**/*.js" security="none"></sec:http>

2. 配置AuthenticationManager

<sec:authentication-manager alias="authenticationManager">
	<sec:authentication-provider ref="authenticationProvider"/>
</sec:authentication-manager>
 
<bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
	<property name="userDetailsService" ref="userService" />
</bean>
 
<bean id="userService" class="com.demo.service.impl.UserService"/>

在Spring Security 3.0之前, AuthenticationManager會被自動建立. 但在3.0之後, 我們需要手動配置<authentication-manager>標籤. 這個標籤會建立一個ProviderManager例項. ProviderManager可以有一個或多個AuthenticationProvider(如: dao,ldap,cas等等). 如果我們把許可權資訊都存在資料庫裡, 那這裡就需要配置一個DaoAuthenticationProvider例項. 

DaoAuthenticationProvider裡需要配置一個實現了UserDetailsService介面的例項, 重寫loadUserByUsername方法. 這其實就是我們經常要在業務層裡寫的東西. 例子如下:
 

@Service
public class UserService implements IUserService, UserDetailsService {
 
	@Autowired
	private SqlSessionTemplate template;
 
	@Override
	public User loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = template.selectOne("getUserByName", username);
		Collection<GrantedAuthority> auths = getUserRoles(username);
		user.setAuthorities(auths);
		return user;
	}
	
	@Override
	public Collection<GrantedAuthority> getUserRoles(String username) {
		List<String> roleList = template.selectList("getUserRoles", username);
		Collection<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
		for (String role : roleList) {
			auths.add(new SimpleGrantedAuthority(role));
		}
		return auths;
	}
	...
}

要注意的的是:
1) User要實現org.springframework.security.core.userdetails.UserDetails介面;

2) User裡面要有一個許可權集合的屬性, 如: private Collection<? extends GrantedAuthority> authorities;

3) loadUserByUsername()方法除了要從資料庫裡拿出使用者的具體資訊之外, 還要拿出使用者的許可權資訊, 這樣後面的AbstractSecurityInterceptor.beforeInvocation(object)方法才能對使用者作許可權驗證.


3. 配置收到HTTP請求時的安全驗證配置:

<sec:http entry-point-ref="myAuthenticationEntryPoint">
	<sec:intercept-url pattern="/**" access="ROLE_USER"/>
	
	<sec:access-denied-handler ref="accessDeniedHandler"/>
 
	<sec:custom-filter ref="loginAuthenticationFilter" position="FORM_LOGIN_FILTER"/>
	<sec:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/>
</sec:http>

3.1 entry-point-ref
配置一個AuthenticationEntryPoint的實現類. 這個類的作用是, 當一個未授權的使用者請求非公有資源時, 這個類的commence方法將會被呼叫, 定義如何處理這個請求. 常用的有LoginUrlAuthenticationEntryPoint實現類, 把請求重定向到登入頁面. 也可以自定義一個實現類, 處理具體的操作, 如記錄日誌, 返回到自定義的403頁面等等.

3.2 <sec:intercept-url pattern="/**" access="ROLE_USER"/>
這個用來配置訪問哪些資源需要哪些許可權/角色, 不多說明.

3.3 access-denied-handler
配置一個AccessDeniedHandler的實現類. 這個類的作用是, 當一個已授權(或已登陸)的使用者請求訪問他許可權之外的資源時, 這個類的handle方法將會被呼叫, 定義如何處理這個請求.


注意AccessDeniedHandler與AuthenticationEntryPoint的區別:
AccessDeniedHandler: 已授權的使用者請求許可權之外的資源時會交給這個類處理.
AuthenticationEntryPoint: 未授權的使用者請求非公共資源時會交給這個類處理.

3.4 custom-filter (重點)
配置自定義的過濾器, 一般要自己配置UsernamePasswordAuthenticationFilter和FilterSecurityInterceptor.


Spring Security的安全驗證是通過過濾器來處理的. 預設情況下Spring會幫我們註冊了很多過濾器, 註冊的順序如下(打星星的是重點):
ChannelProcessingFilter
* SecurityContextPersistenceFilter
ConcurrentSessionFilter
LogoutFilter
* UsernamePasswordAuthenticationFilter/CasAuthenticationFilter/BasicAuthenticationFilter 
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
* ExceptionTranslationFilter
* FilterSecurityInterceptor

SecurityContextPersistenceFilter

用來建立和儲存SecurityContext, 在整個request過程中跟蹤請求者的認證資訊. 當一個request完成時, 它也負責刪除SecurityContextHolder裡的內容.

UsernamePasswordAuthenticationFilter

用來處理表單(通常是登入表單)提交時的驗證. 這個過濾器一般要自己手動配置一下, 如下:

<sec:custom-filter ref="loginAuthenticationFilter" position="FORM_LOGIN_FILTER"/>
 
<bean id="loginAuthenticationFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
	<property name="filterProcessesUrl" value="/my_login"></property> <!-- 表單提交的url, 預設是/j_spring_security_check -->
	<property name="usernameParameter" value="my_username"></property> <!-- 表單裡使用者名稱欄位的name, 預設是j_username -->
	<property name="passwordParameter" value="my_password"></property> <!-- 表單裡密碼欄位的name, 預設是j_password -->
	<property name="authenticationManager" ref="authenticationManager"/> <!-- 一定要配置, 這裡使用上面定義的authenticationManager -->
	<property name="authenticationFailureHandler" ref="authenticationFailureHandler"/> <!-- 驗證失敗時的處理器 -->
	<property name="authenticationSuccessHandler" ref="authenticationSuccessHandler"/> <!-- 驗證成果時的處理器 -->
</bean>
 
<bean id="authenticationSuccessHandler" class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
	<property name="defaultTargetUrl" value="/index"/> <!-- 驗證成功時跳到哪個請求 -->
	<property name="alwaysUseDefaultTargetUrl" value="true"/>
</bean>
 
<bean id="authenticationFailureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler">
	<property name="defaultFailureUrl" value="/login"/> <!-- 驗證失敗時跳到哪個請求 -->
</bean>

上面的配置比較繁瑣, 可以用<form-login>標籤簡化配置:

<sec:http auto-config="true" entry-point-ref="authenticationEntryPointAdapter">
	...
	
	<sec:form-login login-page="login.ftl" username-parameter="my_username" password-parameter ="my_password" 
		authentication-failure-url="/login" 
		login-processing-url="/my_login" 
		always-use-default-target="true" 
		authentication-success-handler-ref="authenticationSuccessHandler"
		authentication-failure-handler-ref="authenticationFailureHandler"/>
	
	...
</sec:http>

ExceptionTranslationFilter
這個過濾器不作具體的驗證操作. 它用來處理Spring Security框架丟擲的異常. 如上文提到, 當丟擲一個AccessDeniedException時, 是交給AuthenticationEntryPoint還是AccessDeniedHandler來處理, 就是由ExceptionTranslationFilter決定.

FilterSecurityInterceptor
這個過濾器非常重要. 它負責處理對所有非公有資源請求的安全驗證. 配置如下: 

<sec:custom-filter ref="filterSecurityInterceptor" <span style="color:#ff0000;"><strong>before="FILTER_SECURITY_INTERCEPTOR"</strong></span>/>
 
<bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
	<property name="authenticationManager" ref="authenticationManager"/>
	<property name="accessDecisionManager" ref="accessDecisionManager"/>
	<property name="securityMetadataSource" ref="mySecurityMetadataSource"/>
</bean>
 
<bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
	<property name="decisionVoters">
		<list>
			<bean class="org.springframework.security.access.vote.RoleVoter">
				<property name="rolePrefix" value=""/>
			</bean>
			<bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
		</list>
	</property>
</bean>
 
<bean id="mySecurityMetadataSource" class="com.demo.security.MySecurityMetadataSource"></bean>

它的三個屬性必須要配置:
authenticationManager: 使用上面定義的authenticationManager
accessDecisionManager: 使用預設的投票器
securityMetadataSource: 用來儲存請求與許可權的對應關係. 一般要自己重寫, 要實現FilterInvocationSecurityMetadataSource介面. 可參考DefaultFilterInvocationSecurityMetadataSource.

FilterInvocationSecurityMetadataSource介面有3個方法:
boolean supports(Class<?> clazz);
Collection<ConfigAttribute> getAllConfigAttributes();
Collection<ConfigAttribute> getAttributes(Object object);

第一個方法不清楚其作用, 一般返回true.
第二個方法是Spring容器啟動時自動呼叫的, 返回所有許可權的集合. 一般把所有請求與許可權的對應關係也要在這個方法裡初始化, 儲存在一個屬性變數裡.
第三個是當接收到一個http請求時, filterSecurityInterceptor會呼叫的方法. 引數object是一個包含url資訊的HttpServletRequest例項. 這個方法要返回請求該url所需要的所有許可權集合.

下面來看看filterSecurityInterceptor具體做了些什麼吧:
doFilter()方法沒什麼好說的, 它呼叫了invoke()方法.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	FilterInvocation fi = new FilterInvocation(request, response, chain);
	invoke(fi);
}

invoke()方法主要看super.beforeInvocation(fi), 它把請求交給下一個filter之前, 驗證當前使用者有沒有許可權訪問這個請求. 如果沒有, 則丟擲AccessDeniedException.

public void invoke(FilterInvocation fi) throws IOException, ServletException {
	if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
			&& observeOncePerRequest) {
		// filter already applied to this request and user wants us to observe
		// once-per-request handling, so don't re-do security checking
		fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
	} else {
		// first time this request being called, so perform security checking
		if (fi.getRequest() != null) {
			fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}
 
		InterceptorStatusToken token = super.beforeInvocation(fi);
 
		try {
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		} finally {
			super.finallyInvocation(token);
		}
 
		super.afterInvocation(token, null);
	}
}

beforeInvocation()方法主要有三步:
第一步找出該請求所需要的全部許可權;
第二步找出當前使用者的的全部許可權;
第三步驗證使用者是否滿足許可權要求.

protected InterceptorStatusToken beforeInvocation(Object object) {
	Assert.notNull(object, "Object was null");
	final boolean debug = logger.isDebugEnabled();
 
	if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
		throw new IllegalArgumentException("Security invocation attempted for object "
				+ object.getClass().getName()
				+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
				+ getSecureObjectClass());
	}
 
	<strong><span style="color:#ff0000;">// 第一步</span>
	<span style="color:#ff0000;">Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);</span></strong>
 
	if (attributes == null || attributes.isEmpty()) {
		if (rejectPublicInvocations) {
			throw new IllegalArgumentException("Secure object invocation " + object +
					" was denied as public invocations are not allowed via this interceptor. "
							+ "This indicates a configuration error because the "
							+ "rejectPublicInvocations property is set to 'true'");
		}
 
		if (debug) {
			logger.debug("Public object - authentication not attempted");
		}
 
		publishEvent(new PublicInvocationEvent(object));
 
		return null; // no further work post-invocation
	}
 
	if (debug) {
		logger.debug("Secure object: " + object + "; Attributes: " + attributes);
	}
 
	if (SecurityContextHolder.getContext().getAuthentication() == null) {
		credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound",
				"An Authentication object was not found in the SecurityContext"), object, attributes);
	}
 
	<strong><span style="color:#ff0000;">// 第二步</span>
	<span style="color:#ff0000;">Authentication authenticated = authenticateIfRequired();</span></strong>
 
	// Attempt authorization
	try {
		<strong><span style="color:#ff0000;">// 第三步</span></strong>
		<span style="color:#ff0000;">this.accessDecisionManager.decide(authenticated, object, attributes);</span>
	}
	catch (AccessDeniedException accessDeniedException) {
		publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException));
 
		throw accessDeniedException;
	}
 
	if (debug) {
		logger.debug("Authorization successful");
	}
 
	if (publishAuthorizationSuccess) {
		publishEvent(new AuthorizedEvent(object, attributes, authenticated));
	}
 
	// Attempt to run as a different user
	Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes);
 
	if (runAs == null) {
		if (debug) {
			logger.debug("RunAsManager did not change Authentication object");
		}
 
		// no further work post-invocation
		return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
	} else {
		if (debug) {
			logger.debug("Switching to RunAs Authentication: " + runAs);
		}
 
		SecurityContext origCtx = SecurityContextHolder.getContext();
		SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
		SecurityContextHolder.getContext().setAuthentication(runAs);
 
		// need to revert to token.Authenticated post-invocation
		return new InterceptorStatusToken(origCtx, true, attributes, object);
	}
}

注意上面紅色粗體的部分: before="FILTER_SECURITY_INTERCEPTOR"

它的意思是我們這個自定義的filterSecurityInterceptor是加在系統預設的filterSecurityInterceptor之前的. 意思是說現在Spring裡有兩個filterSecurityInterceptor. 通過debug我們也看到filterChain裡確實有兩個:

其中第10個是我們自定義的, 第11個是系統預設的. 在上面的例子中, 系統自動加的filterSecurityInterceptor沒有任何用處, 應該刪除, 但暫時找不到方法刪. 如果把配置改成position="FILTER_SECURITY_INTERCEPTOR"的話, Spring啟動時會報錯, 錯誤資訊如下:
 

Configuration problem: Filter beans '<filterSecurityInterceptor>' and '<org.springframework.security.web.access.intercept.FilterSecurityInterceptor#0>' have the same 'order' value. When using custom filters, please make sure the positions do not conflict with default filters. Alternatively you can disable the default filters by removing the corresponding child elements from <http> and avoiding the use of <http auto-config='true'>.