构建基于maven的综合项目(4)-自定义spring security过滤器

构建基于maven的综合项目(四)--自定义spring security过滤器
一、spring security--自定义过滤器
    1、在上一篇的基础上,我们修改springSecuritySimple-config.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<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 auto-config="true" access-denied-page="/403.jsp" >
		<!-- 拦截器 -->
		<intercept-url pattern="/**" access="ROLE_ADMIN" />
		<intercept-url pattern="/login.jsp" filters="none"/>
		<intercept-url pattern="/loginError.do" filters="none"/>
		<intercept-url pattern="/redirectAddUser.do" filters="none"/>
		<intercept-url pattern="/common/**" filters="none"/>
		<intercept-url pattern="/styles/**" filters="none"/>
		<!-- 用户登陆 -->
		<form-login login-page="/login.jsp" 
			always-use-default-target="true" 
			default-target-url="/securityLogin.do" 
			authentication-failure-url="/loginError.do" />
		<!-- 用户注销 -->
		<logout logout-success-url="/login.jsp" invalidate-session="true" />
		<!-- session管理 -->
		<session-management invalid-session-url="/sessionTimeout.do" 
			session-fixation-protection="migrateSession">
			<concurrency-control max-sessions="1" error-if-maximum-exceeded="true"/>
		</session-management>
		<!-- 在过滤器链中加入自定义过滤器 -->
		<custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/>
		<http-basic/>
	</http>
	<!--  
		在自定义的过滤器中需要三个部分:
			AuthenticationManager认证管理器,实现用户认证入口
			AccessDecisionManager:访问控制策略管理器,决策某个用户拥有的角色
			securityMetadataSource:资源源数据定义,定义某一资源能被什么角色访问
				FilterSecurityInterceptor.setSecurityMetadataSource()需要一个FilterSecurityMetadataSource实例,
				它是SecurityMetadataSource的子类
	-->
	<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
		<beans:property name="authenticationManager" ref="authenticationManager" />
		<beans:property name="accessDecisionManager" ref="accessDecisionManager" />
		<beans:property name="securityMetadataSource" ref="mySecurityMetadataSource" />
	</beans:bean>
	<!-- 认证管理器,别名注入,myUserDetailService通过注解的方式在程序中注入 -->
	<authentication-manager alias="authenticationManager">
		<authentication-provider user-service-ref="myUserDetailService">
			<password-encoder ref="md5Encoder">
				<salt-source user-property="username"/>
			</password-encoder>
		</authentication-provider>
	</authentication-manager>
	<!-- 注入自定义加密器 -->
	<beans:bean id="md5Encoder" class="com.cpkf.notpad.security.impl.MD5EncoderImpl"/>
	<!-- 
		访问控制策略管理器 
		allowIfAllAbstainDecisions:通过投票机制决定是否访问某资源,false-一个以上的decisionVoters通过,则授权通过
	-->
	<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
		<beans:property name="allowIfAllAbstainDecisions" value="false" />
		<beans:property name="decisionVoters">
			<beans:list>
				<beans:bean class="org.springframework.security.access.vote.RoleVoter">
					<beans:property name="rolePrefix" value="ROLE_" />
				</beans:bean>
				<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
			</beans:list>
		</beans:property>
	</beans:bean>
	<!-- 注入资源源数据定义 -->
	<beans:bean id="mySecurityMetadataSource" class="com.cpkf.notpad.security.impl.MySecurityMetadataSource" init-method="loadResourceDefine" />
</beans:beans>

    首先,我们要理解一个过滤器是怎么工作的,它主要依赖于下面三个部分:
        AuthenticationManager认证管理器,实现用户认证入口
        AccessDecisionManager:访问控制策略管理器,决策某个用户拥有的角色
        securityMetadataSource:资源源数据定义,定义某一资源能被什么角色访问
    我们使用的其实依旧是默认过滤器链中org.springframework.security.web.access.intercept.FilterSecurityInterceptor过滤器,只不过我们将其中重要的认证管理和资源源数据定义换成自己的东西,在AuthenticationManager中,我们不再采用以前获取数据的三种方式,而是用自定义的UserDetailService获取登陆用户信息,并赋值给认证管理器,在securityMetadataSource中,我们将url资源库中所有的url以及其匹配的角色进行了初始化,然后将每一个url请求和初始化数据比对,以判断是否授权用户。

    2、MyUserDetailServiceImpl
    我抽取了一个接口MyUserDetailService,接口继承org.springframework.security.core.userdetails.UserDetailsService
package com.cpkf.notpad.security.impl;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
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.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.cpkf.notpad.entity.Account;
import com.cpkf.notpad.entity.Resource;
import com.cpkf.notpad.entity.Role;
import com.cpkf.notpad.security.MyUserDetailService;
import com.cpkf.notpad.server.IAccountService;
/**  
 * Filename:    MyUserDetailServiceImpl.java
 * Description: 用自定义的UserDetailService获取登陆用户信息,并赋值给认证管理器
 * Company:     
 * @author:     Jiang.hu
 * @version:    1.0
 * Create at:   May 26, 2011 4:23:27 PM
 * modified:    
 */
@Service("myUserDetailService")
public class MyUserDetailServiceImpl implements MyUserDetailService {
	private static final Logger logger = Logger.getLogger(MyUserDetailServiceImpl.class);
	@Autowired
	private IAccountService accountService;
	public UserDetails loadUserByUsername(String username) 
		throws UsernameNotFoundException, DataAccessException {
		//取得用户
		Account account = accountService.getAccountByEmail(username);
		if(account == null){
			logger.info("Can not found any Account by given username:" + username);
			throw new UsernameNotFoundException("Can not found any Account by given username:" + username);
		}
		//取得用户角色
		Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
		if(!account.getRoles().isEmpty() && account.getRoles() != null){
			Iterator<Role> roleIterator = account.getRoles().iterator();
			while(roleIterator.hasNext()){
				Role role = roleIterator.next();
				GrantedAuthority grantedAuthority = new GrantedAuthorityImpl(role.getRoleName().toUpperCase());
				System.out.println("用户角色:" + role.getRoleName().toUpperCase());
				authorities.add(grantedAuthority);
			}
		}
		
		return new User(username, account.getPassWord(), true, true, true, true, authorities);
	}

}

    3、MySecurityMetadataSource
package com.cpkf.notpad.security.impl;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.AntUrlPathMatcher;
import org.springframework.security.web.util.UrlMatcher;
import com.cpkf.notpad.entity.Resource;
import com.cpkf.notpad.entity.Role;
import com.cpkf.notpad.server.IResourceService;
/**  
 * Filename:    MySecurityMetadataSource.java
 * Description: 资源源数据定义,决定某一资源能被什么角色访问
 * Company:     
 * @author:     Jiang.hu
 * @version:    1.0
 * Create at:   May 26, 2011 5:04:50 PM
 * modified:    
 */
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
	private static final Logger logger = Logger.getLogger(MySecurityMetadataSource.class);
	@Autowired
	private IResourceService resourceService;
	private static Map<String, Collection<ConfigAttribute>> resourceMap = null;
	private UrlMatcher urlMatcher;
	public MySecurityMetadataSource(){
		urlMatcher = new AntUrlPathMatcher();
		resourceMap = new HashMap<String, Collection<ConfigAttribute>>();
		//可在构造方法中调用初始化方法,也可在配置文件中指定初始化方法
//		loadResourceDefine();
	}
	/* 
	 * method name   : loadResourceDefine
	 * description   : 初始化方法,得到所有资源以及每个资源对应的角色
	 * @author       : Jiang.Hu
	 * @param        : 
	 * @return       : void
	 * Create at     : May 27, 2011 2:38:26 PM
	 * modified      : 
	 */      
	public void loadResourceDefine(){
		List<Resource> resources = resourceService.getAllResources();
		if(resources != null && !resources.isEmpty()){
			for(Resource resource : resources){
				Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
				Iterator<Role> roleIterator = resource.getRoles().iterator();
				while(roleIterator.hasNext()){
					Role role = roleIterator.next();
					ConfigAttribute configAttribute = new SecurityConfig(role.getRoleName());
					configAttributes.add(configAttribute);
				}
				resourceMap.put(resource.getUrl(), configAttributes);
			}
		}
	}
	/* 
	 * method name   : getAttributes
	 * description   : 将请求url和资源库比较,符合则将资源库的角色定义赋值给当前请求url
	 * @author       : Jiang.Hu
	 * modified      : leo ,  May 27, 2011
	 * @see          : @see org.springframework.security.access.SecurityMetadataSource#getAttributes(java.lang.Object)
	 */    
	public Collection<ConfigAttribute> getAttributes(Object object) 
		throws IllegalArgumentException {
		if((object == null) || !this.supports(object.getClass())){
			logger.info("Object must be a FilterInvocation");
			throw new IllegalArgumentException("Object must be a FilterInvocation");
		}
		Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
		if(object instanceof FilterInvocation){
			String requestUrl = ((FilterInvocation)object).getRequestUrl();
			System.out.println("请求url:" + requestUrl);
			//比较请求url和资源库url,匹配则赋予对应的角色权限
			if(resourceMap != null && !resourceMap.isEmpty()){
				Iterator<String> iterator = resourceMap.keySet().iterator();
				while(iterator.hasNext()){
					String resourceUrl = iterator.next();
					if(urlMatcher.pathMatchesUrl(resourceUrl, requestUrl)){
						configAttributes.addAll(resourceMap.get(resourceUrl));
					}
				}
				if(configAttributes.isEmpty()){
					configAttributes.add(new SecurityConfig("nobody"));
				}
				for(ConfigAttribute configAttribute : configAttributes){
					System.out.println("请求url匹配角色:" + configAttribute.toString());
				}
				return configAttributes;
			}
		}
		return null;
	}

	public Collection<ConfigAttribute> getAllConfigAttributes() {
		return new ArrayList<ConfigAttribute>();
	}

	public boolean supports(Class<?> clazz) {
		return FilterInvocation.class.isAssignableFrom(clazz);
	}
}

    4、这样,我们以hj@163.com为例,实现用户角色和资源绑定的效果
    eg.hj@163.com用户拥有ROLE_ADMIN角色,在登录访问"/securityLogin.do"url时,如果resource资源库中"/securityLogin.do"没有ROLE_ADMIN角色,该用户没有权限权限,应跳转到403页面,
        用户角色:ROLE_ADMIN
        用户角色:ROLE_GUEST
        请求url:/securityLogin.do
        请求url匹配角色:nobody
    如果resource资源库中"/securityLogin.do"有ROLE_ADMIN角色,该用户才能访问
        用户角色:ROLE_ADMIN
        用户角色:ROLE_GUEST
        请求url:/securityLogin.do
        请求url匹配角色:ROLE_ADMIN
    5、有了上面的基础,下面来设计用户权限管理
        首先是在无用户状态下需要访问的url,不需要过滤器验证
<intercept-url pattern="/**" access="ROLE_ADMIN" />
		<intercept-url pattern="/login.jsp" filters="none"/>
		<intercept-url pattern="/redirectAddUser.do" filters="none"/>
		<intercept-url pattern="/redirectRegist.do" filters="none"/>
		<intercept-url pattern="/loginError.do" filters="none"/>
		<intercept-url pattern="/sessionTimeout.do" filters="none"/>
		<intercept-url pattern="/visualCode.do" filters="none"/>
		<intercept-url pattern="/addAccount.do" filters="none"/>
		<intercept-url pattern="/styles/**" filters="none"/>
		<intercept-url pattern="/scripts/**" filters="none"/>
		<intercept-url pattern="/images/**" filters="none"/>

        其次设计一些公共url资源,并赋予给每个角色
package com.cpkf.notpad.commons.constants;

/**  
 * Filename:    PublicResourceEnum.java
 * Description: 公共资源
 * 				home-根目录下所有链接(/*不包含二级目录下请求,/**包含二级目录请求)
 * 				owner-owner下所有请求,用户相关资源eg修改个人资料等
 * Company:     
 * @author:     Jiang.hu
 * @version:    1.0
 * Create at:   May 30, 2011 10:53:27 AM
 * modified:    
 */
public enum PublicResourceEnum {
	OWNER("owner","/owner/**"),HOME("home","/*");
	private String publicResourceName;
	private String publicResourcePath;
	private PublicResourceEnum(String publicResourceName, String publicResourcePath) {
		this.publicResourceName = publicResourceName;
		this.publicResourcePath = publicResourcePath;
	}
	public String getPublicResourceName() {
		return publicResourceName;
	}
	public String getPublicResourcePath() {
		return publicResourcePath;
	}
	
}

        在MySecurityMetadataSource类中添加loadPublicResources方法,并在初始化方法loadResourceDefine中调用
public void loadPublicResources(){
		List<Role> roles = roleService.getAllRoles();
		for(PublicResourceEnum publicResourceEnum : PublicResourceEnum.values()){
			Collection<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
			for(Role role : roles){
				ConfigAttribute configAttribute = new SecurityConfig(role.getRoleName());
				configAttributes.add(configAttribute);
			}
			resourceMap.put(publicResourceEnum.getPublicResourcePath(), configAttributes);
		}
	}

        这样一来,每个注册用户可以访问/*一级资源和"/owner/**二级地址下所有资源,其他二级资源就可以在resource表中指定,并指派给相应的role了