应用场景

现有的数据库中包含以下几张表格用于权限管理


spring Security 多级路径匹配 spring security 配置_ide


要求在此基础上集成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,有不理解的可与我联系,需要源码的网友可留邮箱