应用场景
现有的数据库中包含以下几张表格用于权限管理
要求在此基础上集成SpringSecurity,将表格的数据作为数据源来完成登录和权限校验逻辑
SpringSecurity的配置可通过两种方式呈现,基于自身的namespace配置和传统的基于Bean的配置。通过namespace来配置Security非常简洁,隐藏了很多繁琐的实现细节,但也不便于初学者进行理解,而如果要想对Security进行个性化定制(替换现有功能实现),最好还是采用传统的基于Bean的方式进行配置,虽然结构复杂,但是细节清晰明了
以下是两种方式的配置比较:
1.基于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.1.xsd">
<http pattern="/js/**" security="none" />
<http use-expressions="true" access-denied-page="/error.html">
<intercept-url pattern="/peoplemanage/**" access="hasRole('admin')" />
<form-login login-page='/login.jsp'/>
<logout />
</http>
<authentication-manager>
<authentication-provider>
<user-service>
<user name="zhangsan" password="zhangsan" authorities="admin,user"/>
<user name="wangwu" password="wangwu" authorities="user" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
2.同样的配置还原成Bean的方式
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
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.1.xsd">
<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<constructor-arg>
<list>
<sec:filter-chain pattern="/js/**" filters="none"/>
<sec:filter-chain pattern="/**"
filters="securityContextPersistenceFilter,authenticationFilter,exceptionTranslationFilter,filterSecurityInterceptor"/>
</list>
</constructor-arg>
</bean>
<bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="securityMetadataSource">
<sec:filter-security-metadata-source use-expressions="true">
<sec:intercept-url pattern="/peoplemanage/**" access="hasRole('admin')"/>
</sec:filter-security-metadata-source>
</property>
</bean>
<!-- exceptionTranslationFilter -->
<bean id="exceptionTranslationFilter" class="org.springframework.security.web.access.ExceptionTranslationFilter">
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
<property name="accessDeniedHandler" ref="accessDeniedHandler"/>
</bean>
<bean id="authenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>
<bean id="accessDeniedHandler" class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage" value="/error.html"/>
</bean>
<!-- securityContextPersistenceFilter -->
<bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/>
<!-- authenticationFilter -->
<bean id="authenticationFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="filterProcessesUrl" value="/j_spring_security_check"/>
</bean>
<!-- Core Service -->
<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
<property name="providers">
<list>
<ref local="daoAuthenticationProvider"/>
</list>
</property>
</bean>
<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
</bean>
<bean id="inMemoryDaoImpl" class="org.springframework.security.provisioning.InMemoryUserDetailsManager">
<constructor-arg name="users">
<props>
<prop key="zhangsan">zhangsan,enabled</prop>
<prop key="wangwu">wangwu,enabled</prop>
</props>
</constructor-arg>
</bean>
<bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<property name="decisionVoters">
<list>
<bean class="org.springframework.security.web.access.expression.WebExpressionVoter"></bean>
</list>
</property>
</bean>
</beans>
还原成Bean的配置方式之后,在实现个性化的定制就变得清晰明了了。
一、首先需要修改userDetailsService的实现
在上述Demo配置中使用的是Spring内置的InMemoryUserDetailsManager,该类的主要作用是从配置文件加载zhangsan、wangwu等信息来构建用户数据源,而我们的用户数据是存储在数据库里的,因此需要修改实现,实现方式如下:
1.自定义一个Service,实现org.springframework.security.core.userdetails.UserDetailsService接口
public class MyUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
/**
* TODO 从数据库中加载用户信息,并封装成UserDetails对象
*/
}
}
2.替换Demo中的对应的配置
<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="myUserDetailsService"/>
</bean>
<bean id="myUserDetailsService" class="com.youcompany.MyUserDetailsService"/>
二、修改filterSecurityInterceptor中securityMetadataSource属性的注入方式
在Demo配置中securityMetadataSource属性的配置是静态的,将每一个资源和资源对应的角色封装到<sec:intercept-url>标签里
而我们的需求场景是资源信息存储在数据库里,因此不能通过这种静态的方式去描述,修改方式如下:
1.声明一个Service实现org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource接口
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap;
public MyFilterInvocationSecurityMetadataSource(){
requestMap=new HashMap<RequestMatcher, Collection<ConfigAttribute>>();
loadMetadataInfo();//将数据库中的资源和角色实体封装到requestMap里
}
private void loadMetadataInfo() {
List<Resource> resources=...//TODO 获取数据库中所有的资源实体
for(Resource res:resources){
Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>();
List<Role> roles=...//TODO 获取该资源对应的访问角色
for(Role role:roles){
allAttributes.add(new SecurityConfig(role.getRoleName()));
}
RequestMatcher key=new AntPathRequestMatcher(res.getUrl()+"/**");
requestMap.put(key, allAttributes);
}
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>();
List<Role> roles...//TODO 获取库中所有的角色实体
for(Role role:roles){
allAttributes.add(new SecurityConfig(role.getRoleName()));
}
return allAttributes;
}
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return null;
}
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
2.修改Demo中相应的配置
<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="myFilterInvocationSecurityMetadataSource"/>
</bean>
<bean id="myFilterInvocationSecurityMetadataSource" class="com.youcompany.MyFilterInvocationSecurityMetadataSource"/>
三、修改accessDecisionManager中decisionVoters的实现逻辑
SpringSecurity默认使用AffirmativeBased来进行访问权限控制,该类封装了很多AccessDecisionVoter对象,基于投票的机制来决定访问是否通过
AccessDecisionVoter之间是OR的逻辑(只要有一个AccessDecisionVoter判断权限通过,用户便可访问界面)。
在Demo配置里,使用的是WebExpressionVoter基于表达式的权限认证逻辑(hasRole('admin')),而我们的需求是将用户的角色和访问资源需要的角色进行对比,来判断该用户是否具有访问界面的权限,因此需要进行以下修改:
1.声明一个Service实现org.springframework.security.access.AccessDecisionVoter接口
public class MyAccessDecisionVoter implements AccessDecisionVoter<Object> {
public boolean supports(ConfigAttribute attribute) {
return true;
}
public boolean supports(Class<?> clazz) {
return true;
}
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
int result = ACCESS_DENIED;
for (ConfigAttribute attribute : attributes) {//可访问该页面的角色
for (GrantedAuthority authority : authentication.getAuthorities()) {//登录用户具备的角色
if (attribute.getAttribute().equals(authority.getAuthority())) {//判断用户是否具有相应角色
return ACCESS_GRANTED;
}
}
}
return result;
}
}
2.修改Demo中对应的配置
<bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<property name="decisionVoters">
<list>
<bean class="com.youcompany.MyAccessDecisionVoter"></bean>
</list>
</property>
</bean>
至此,SpringSecurity个性化定制修改完成。有点长,部分代码加了TODO,有不理解的可与我联系,需要源码的网友可留邮箱