Spring security4.1.0 自定义角色和权限(长文)

文章大纲:
1.spring security 基本配置介绍
2.自定义角色和权限配置
3.跟着源码走一遍页面请求流程

spring security 基本配置介绍

首先需要创建一个为Spring security的专门的配置文件 然后引入相应的namespace

<pre>
<code>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="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.1.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-4.1.xsd">

</code>
</pre>

根据你使用的版本配置相应的命名空间,我使用的是4.1.0的Spring security。

Spring Security 命名空间的引入可以简化我们的开发,它涵盖了大部分 Spring Security 常用的功能。它的设计是基于框架内大范围的依赖的,可以被划分为以下几块。

1.Web/Http 安全:这是最复杂的部分。通过建立 filter 和相关的 service bean 来实现框架的认证机制。当访问受保护的 URL 时会将用户引入登录界面或者是错误提示界面。

2.业务对象或者方法的安全:控制方法访问权限的。
2.1AuthenticationManager:处理来自于框架其他部分的认证请求。
2.2AccessDecisionManager:为 Web 或方法的安全提供访问决策。会注册一个默认的,但是我们也可以通过普通 bean 注册的方式使用自定义的 AccessDecisionManager。
2.3AuthenticationProvider:AuthenticationManager 是通过它来认证用户的。
2.4UserDetailsService:跟 AuthenticationProvider 关系密切,用来获取用户信息的。
2.5 UserDetails :跟UserDetailsService关系密切,用于封装一个用户信息的实体类接口 ,默认实现是security包下的User类

我们来看一个简单的登录配置

<security:http auto-config="true">
      <security:form-login 
         login-page="/login.jsp"
         login-processing-url="/login.do" 
         username-parameter="username"
         password-parameter="password" />
      <!-- 表示匿名用户可以访问 -->
      <security:intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
      <security:intercept-url pattern="/**" access="ROLE_USER" />
   </security:http>

1.username-parameter:表示登录时用户名使用的是哪个参数,默认是 “j_username”。
2.password-parameter:表示登录时密码使用的是哪个参数,默认是 “j_password”。
3.login-processing-url:表示登录时提交的地址,默认是 “/j-spring-security-check”。这个只是 Spring Security 用来标记登录页面使用的提交地址,真正关于登录这个请求是不需要用户自己处理的。
4.login-page: 代表你的登录页面。

<security:intercept-url 代表你要拦截的url pattern 是拦截的url或者可以是一个正则表达式 access代表允许请求的用户角色 我们可以看到/** 需要的是ROLE_USER这样的权限,还有
IS_AUTHENTICATED_ANONYMOUSLY 允许匿名用户进入
IS_AUTHENTICATED_FULLY 允许登录用户进入
IS_AUTHENTICATED_REMEMBERED 允许登录用户和rememberMe用户进入

当用户试图访问被配置拦截的URL时,security会先去判断是否登录,如果没有则会跳转到登录界面进行登录认证。当你登录成功进去之后,会根据你是否具有相应的URL权限给予放行,如果有则跳转,没有则变成403 Access Denied

访问了不具有相应权限的URL

那么我们如何去灵活的控制什么样的用户权限拥有什么样的可访问资源呢?这个也是这个文章的重点,需要根据自己的业务去设置不同用户访问不同的页面。也就是自定义用户角色和权限,我们可以把这些定义的数据放在数据库中,而不是放在配置文件里面。这样就可以很灵活的去改变用户和权限而不需要改变项目。

自定义角色和权限配置

接下来我以自己项目中实践的内容为例子来讲解配置自定义角色和权限

1.第一步先把数据库中的表结构准备好,

用户表( uid,username,password,role_id,....)
角色表 (rid,rname,rdescription)
用户角色表(urid,uid,rid)
资源表(res_id,res_url,res_description)
资源角色表(res_r_id,res_id,r_id)

大部分情况下这5个表就可以满足整个的权限控制。而我自己因为一个用户只有一种角色,所以我实际是没有用户角色表的(这个看个人的情况而定吧)

2.我们来看看Spring security的配置文件中定义了哪些用到的bean,根据这些bean来进行说明

    <!-- 登录页面不进行过滤
    有两种方法不过滤指定的页面,一种是 像下面这样 还有一种是
    <security:intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/> 
    像这样子的 配置access为允许匿名用户访问
    两者的区别是 前者none不配置任务的过滤链 后者会进入Spring的过滤链-->
    <security:http pattern="/index/login" security="none"/>

这个就比较简单了,就是我不过滤登录页面,防止登录页面也需要认证然后跳转到登录页面变成死循环。


    <security:http use-expressions="false">
        <!--在Spring 4中使用 security时 如果要使用access IS_AUTHENTICATED_ANONYMOUSLY 的
         话 要 配置use-expressions 为false(默认为ture) 默认是使用Spring EL expression
         而你没有使用 或者access="hasRole('...').
         解决来源: http://stackoverflow.com/questions/33362315/failed-to-evaluate-expression-is-authenticated-anonymously-spring-4  -->

然后是是否使用Spring的EL表达式,因为我之前在刚认识security的时候练手,配置过access IS_AUTHENTICATED_ANONYMOUSLY然后就报错了。

<security:form-login
            login-page="/index/login"
            login-processing-url="/index/login.do"
            username-parameter="login_name"
            password-parameter="password"
            authentication-failure-forward-url="/index.jsp"
            authentication-success-forward-url="/index/index"/>

这个就是我的登录页面的一个配置情况,登录页面为/index/login。登录认证请求的URL为/index/login.do 然后使用login_name 和password 两个参数来验证用户名和密码 如果登录成功 跳转 /index/index 如果登录失败跳转/index.jsp

<!--
         当你配置了自定义的拦截器的时候,而且我拦截的请求是存放在数据库中的,所有当我输入一个请求URL时
         它会去getAttributes 然后把这个请求和数据库中的资源进行匹配 当匹配成功时 发现用户没有登录
         然后就跳转到登录页面(个人理解)如果请求的是一个非数据库存储的URL那么就完全找不到这个请求 404-->
        <security:custom-filter ref="myFilterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/>
        <!-- 禁用csrf机制 这个机制是用于防止黑客攻击  具体介绍参考百度百科 -->
    <security:csrf disabled="true"/>
        <!--停用对匿名认证的支持 -->
    <security:anonymous enabled="false"/>
    </security:http>

这个里面的custom filter就是我自定义的过滤器,before代表放在这个指定的过滤器之前。也就是指定一个顺序。这个过滤器后面会用到。其他的还有after first last position 等 代表 放在..后面 放在最前面 放在最后面 放在指定的过滤器位置替换掉它。

然后我禁用了csrf (默认是true)因为我在练手的时候也没有使用这么高级的csrf认证机制,如果不禁用 你登录的时候会报错。如果你之前已经实践过这个部分,就不虚。我还没去弄csrf认证等后面再搞,这里就先禁用掉了。

在你的session里面没有找到csrf token

还有就是匿名认证(默认是true),之前也是练手的时候不懂使用了false。其实匿名认证是在IS_AUTHENTICATED_ANONYMOUSLY这样的Access是会给予放行。你可以指定一些页面不需要认证登录就能访问。所以其实不用配置为false。忽略我的false。

 <!--认证管理器 实现用户进行登录鉴定的类 主要实现UserDetailsService接口即可-->
    <security:authentication-manager alias="authenticationManager">
        <security:authentication-provider
         user-service-ref="myUserDetailsService">
        </security:authentication-provider>
    </security:authentication-manager>
    <!--自定义的 UserDetailsService -->
    <bean id="myUserDetailsService" class="Index.MyUserDetailsService">
    </bean>

认证管理器主要实现UserDetailsService这个接口,我自己自定义了一个UserDetailsService。默认是使用security中提供的JdbcDaoImpl,我的自定义实现也是参考源码JdbcDaoImpl的实现来完成的。现在我们来看看JdbcDaoImpl中有哪些内容


public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService {

    public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled from users where username = ?";
    public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority from authorities where username = ?";
    public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";
    protected final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private String authoritiesByUsernameQuery = "select username,authority from authorities where username = ?";
    private String groupAuthoritiesByUsernameQuery = "select g.id, g.group_name, ga.authority from groups g, group_members gm, group_authorities ga where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id";
    private String usersByUsernameQuery = "select username,password,enabled from users where username = ?";
    private String rolePrefix = "";
    private boolean usernameBasedPrimaryKey = true;
    private boolean enableAuthorities = true;
    private boolean enableGroups;

成员变量
1.DEF_USERS_BY_USERNAME_QUERY :默认的查询用户Query语句
2.DEF_AUTHORITIES_BY_USERNAME_QUERY 默认的查询用户权限的Query语句
3.DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY 默认的查询组权限的Query语句
4.messages 用于记录一些程序中日志信息和错误信息
5.authoritiesByUsernameQuery 用于自定义的权限查询Query语句
6.groupAuthoritiesByUsernameQuery 用于自定义的组权限查询Query语句
7.usersByUsernameQuery 用于自定义的用户信息查询Query语句
8.rolePrefix 角色前缀
9.usernameBasedPrimaryKey 如果为真 则使用query语句查询出来的用户名作为用户实体的username 否则 则使用你用于登录认证时上传的用户名作为实体的username

 protected UserDetails createUserDetails(String username, UserDetails userFromUserQuery, List<GrantedAuthority> combinedAuthorities) {
        String returnUsername = userFromUserQuery.getUsername();
        if(!this.usernameBasedPrimaryKey) {
            returnUsername = username;
        }

        return new User(returnUsername, userFromUserQuery.getPassword(), userFromUserQuery.isEnabled(), true, true, true, combinedAuthorities);
    }

10.enableAuthorities 是否支持权限认证
11.enableGroups 是否支持组权限认证

我的自定义MyUserDetailsService实现

package Index;

import Entity.User;
import Tool.HibernateUtil.java.HibernateUtil;
import org.hibernate.*;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.logging.Logger;

/**
 * 作为MyUserDetail 实体类的DAO层 进行登录名认证的规则定义
 * 自定义查询数据库规则 并返回相应的实体类
 */
@Component
public class MyUserDetailsService implements UserDetailsService {

    private static final String USER_BY_USERNAME_QUERY="select new User(u.login_name,u.password,u.username,u.id,u.userType) from User u where login_name=:username";
    public static final String AUTHORITIES_BY_USERNAME_QUERY = "select role,login_name from user u,user_type type where u.login_name=:username and u.user_type=type.id";
    public static final String GROUP_AUTHORITIES_BY_USERNAME_QUERY = "";
    protected final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private static Logger logger=Logger.getLogger(MyUserDetailsService.class.getName());
    private String rolePrefix = "";//角色前缀
    private boolean usernameBasedPrimaryKey = true;//如果为真 则使用query语句查询出来的用户名作为用户实体的username 否则 则使用你用于登录认证时上传的用户名作为实体的username
    private boolean enableAuthorities = true;//是否支持权限验证
    private boolean enableGroups;//是否支持组

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User u = this.loadUsersByUsername(s);
        HashSet dbAuthsSet = new HashSet();
            if(this.enableAuthorities) {
                dbAuthsSet.addAll(this.loadUserAuthorities(u.getLogin_name()));
            }

            if(this.enableGroups) {
                dbAuthsSet.addAll(this.loadGroupAuthorities(u.getLogin_name()));
            }
            ArrayList dbAuths = new ArrayList(dbAuthsSet);
            if(dbAuths.size() == 0) {
                logger.info("User \'" + s + "\' has no authorities and will be treated as \'not found\'");
                throw new UsernameNotFoundException(this.messages.getMessage("MyUserDetailsService.noAuthority", new Object[]{s}, "User {0} has no GrantedAuthority"));
            } else {
                return this.createUserDetail(u.getUsername(),u.getPassword(),u.getEmail(),dbAuths);
            }

    }

    /**
     * 根据用户名去加载相应的认证信息 完成登录认证
     * @param username 登录时的用户名
     * @return 查询出的用户实体类
     */
    protected User loadUsersByUsername(String username){
        Session session= HibernateUtil.getSession();
        Transaction tx=HibernateUtil.getTransaction();
        try {
            Query Query = session.createQuery(USER_BY_USERNAME_QUERY);
            Query.setString("username",username);
            User u = (User) Query.uniqueResult();
            tx.commit();
            return u;
        }catch (HibernateException e){
            e.printStackTrace();
            if (tx!=null){
                tx.rollback();
            }
            throw new UsernameNotFoundException("Hibernate 查询失败  查询不到用户名为 "+username+" 的用户信息");
        }finally {
            HibernateUtil.closeSession(session);
        }
    }

    /**
     * 根据用户名加载用户的相应权限
     * @param username 用户名
     * @return
     */
    public List<SimpleGrantedAuthority> loadUserAuthorities(String username){
        Session session= HibernateUtil.getSession();
        Transaction tx=HibernateUtil.getTransaction();
        try {
            SQLQuery sqlQuery = session.createSQLQuery(AUTHORITIES_BY_USERNAME_QUERY);
            sqlQuery.setString("username",username);
            List<Object []> list = sqlQuery.list();
            tx.commit();
            return createUserAuthorities(list);
        }catch (HibernateException e){
            e.printStackTrace();
            if (tx!=null){
                tx.rollback();
            }
            return null;
        }finally {
            HibernateUtil.closeSession(session);
        }
    }

    /**
     * 根据用户名加载用户所在组的相应权限
     * @param username
     * @return
     */
    protected List<SimpleGrantedAuthority> loadGroupAuthorities(String username) {
        Session session= HibernateUtil.getSession();
        Transaction tx=HibernateUtil.getTransaction();
        try {
            SQLQuery sqlQuery = session.createSQLQuery(GROUP_AUTHORITIES_BY_USERNAME_QUERY);
            sqlQuery.setString("username",username);
            List<Object []> list = sqlQuery.list();
            return createUserAuthorities(list);
        }catch (HibernateException e){
            e.printStackTrace();
            if (tx!=null){
                tx.rollback();
            }
            return null;
        }finally {
            HibernateUtil.closeSession(session);
        }
    }

    public List<SimpleGrantedAuthority> createUserAuthorities(List<Object []> list){
        List<SimpleGrantedAuthority> list1=new ArrayList<>();
        for (Object [] objects:list){
            SimpleGrantedAuthority auto;
            if (objects[0]!=null){
                auto=new SimpleGrantedAuthority(String.valueOf(objects[0]));
            }else {
                logger.info("权限为空....");
                auto=new SimpleGrantedAuthority("");
            }
            list1.add(auto);
        }
        return list1;
    }

    public UserDetails createUserDetail(String username,String password,String email,List<GrantedAuthority> combinedAuthorities){
        return new MyUserDetail(username,password,email,true,true,true,true,combinedAuthorities);
    }

    public boolean isEnableGroups() {
        return enableGroups;
    }

    public void setEnableGroups(boolean enableGroups) {
        this.enableGroups = enableGroups;
    }

    public String getRolePrefix() {
        return rolePrefix;
    }

    public void setRolePrefix(String rolePrefix) {
        this.rolePrefix = rolePrefix;
    }

    public void setUsernameBasedPrimaryKey(boolean usernameBasedPrimaryKey) {
        this.usernameBasedPrimaryKey = usernameBasedPrimaryKey;
    }

    protected boolean isUsernameBasedPrimaryKey() {
        return this.usernameBasedPrimaryKey;
    }
}

来看看之前出现过的自定义过滤器

<!--过滤链 -->
    <bean id="myFilterSecurityInterceptor" class="Index.MyFilterSecurityInterceptor">
        <property name="accessDecisionManager" ref="myAccessDescisionManager"/>
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="filterInvocationSecurityMetadataSource" ref="mySecurityMetadataSource"/>
    </bean>
    <!--安全资源元数据 用于加载和初始化项目总体资源和权限的映射集合 -->
    <bean id="mySecurityMetadataSource" class="Index.MySecurityMetadataSource">
    </bean>
authenticationManager

其中有三个bean,其中的一个authenticationManager就是刚刚介绍的那个用于登录认证之后获取当前用户所拥有的权限。先梳理一下整个认证的流程。

Web项目启动.png
accessDecisionManager

第二个bean accessDecisionManager 这个决定管理器里面有2个voter投票器,一个是roleVoter 一个是authenticatedVoter。在roleVoter里面配置了角色前缀,我这里配置的是"",这里其实就是文章开头介绍简单配置时候<security:intercept-url pattern="/**" access="ROLE_USER" /> Access这里会看到有一个前缀,这个前缀来源就是这个RoleVoter 默认是前缀是ROLE

 <!--访问决定管理器 用于决定是否对请求进行拒绝或者允许通行 -->
    <bean id="myAccessDescisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
        <constructor-arg name="decisionVoters">
            <list>
                <ref bean="roleVoter"/>
                <ref bean="authenticatedVoter"/>
            </list>
        </constructor-arg>
    </bean>
    <!--角色投票器 -->
    <bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter">
        <property name="rolePrefix" value=""/>
    </bean>
    <!--鉴权投票器 -->
    <bean id="authenticatedVoter" class="org.springframework.security.access.vote.AuthenticatedVoter">
    </bean>

以上如果实用了useExpressions(有属性use-expressions指定,默认的也是true)即SPEL表达式,则选择WebExpressionVoter,否则选择RoleVoter及AuthenticatedVoter

security中提供了3个accessDecisionManager 的实现,我的这次例子也是使用了其中一个(AffirmativeBased)。因为目前还不需要自己重新实现。security自带的功能一般都比较强大的。

Spring Security内置了三个基于投票的AccessDecisionManager实现类,它们分别是AffirmativeBased、ConsensusBased和UnanimousBased。

   AffirmativeBased的逻辑是这样的:

   (1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;

   (2)如果全部弃权也表示通过;

   (3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。

   ConsensusBased的逻辑是这样的:

   (1)如果赞成票多于反对票则表示通过。

   (2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException。

   (3)如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。

   (4)如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值为true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。

   UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。UnanimousBased的逻辑具体来说是这样的:

   (1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出AccessDeniedException。

   (2)如果没有反对票,但是有赞成票,则表示通过。

   (3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出AccessDeniedException。

大家想再了解跟着走一遍,可以去看security包中相应的源码实现。

filterInvocationSecurityMetadataSource

这个是自定义去加载相应的资源和权限映射,默认是使用了Spring的JDBCtemplate 我的DAO是使用Hibernate去持久化的。所以需要重写这个实现类。继承相应的FilterInvocationSecurityMetadataSource接口,这个类中主要的内容储存在一个静态map变量中。

/**
 *  资源源数据定义,将所有的资源和权限对应关系建立起来,即定义某一资源可以被哪些角色访问
 */
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    //将数据库中的所有权限和资源查询出来 建立对应关系
    public static final String LOAD_ALL_AUTHORITIES_QUERY = "select role from user_type";
    public static final String LOAD_ALL_AUTHORITIES_AND_RESOURCES_QUERY = "select rr.user_type_id AS user_type_id,rr.resource_id AS resource_id,r.url AS url,ut.role AS role_name from role_resources rr,resources r,user_type ut where rr.user_type_id=ut.id and rr.resource_id=r.rsid";
    private static Map<RequestMatcher,Collection<ConfigAttribute>> resourceMap;
    private Collection<ConfigAttribute> allAttribute = new HashSet<>();
    private static Logger logger=Logger.getLogger(MySecurityMetadataSource.class.getName());

也就是resourcemap 它是一个key为RequestMatcher,value为Collection<ConfigAttribute>的map集合。key是什么呢?
key就是一个URL的匹配器,匹配HttpServletRequest的简单策略。它有很多实现类,对应着不同的匹配策略。
value就是匹配资源所对应的权限集合

其他的成员变量就是一些我自定义的用于Query语句查询所有权限和资源的字符串常量

public MySecurityMetadataSource() {
        this.loadResourcesDefine();
    }

    /**
     * 根据相应的查询语句去数据库加载资源和权限 初始化map集合
     * 这里初始化map的key时 固定使用RequestMatcher接口中的AntPathRequestMatcher
     * RequestMatcher还有很多实现类 不过目前还不是很明确具体是如何使用和配置这些类 暂定固定使用固定使用RequestMatcher接口中的AntPathRequestMatcher
     *
     * 这个类会在web第一次启动的时候把权限和资源初始化 并缓存起来
     * 但是如果在后面的权限发生改变了,那么就会导致无法更新
     * 一种解决方案是:在getAttributes那里直接从数据库中查询相应的url权限
     * 另一种解决方案:在有更新权限和资源集合的时候 再次调用loadResourcesDefine去重新加载一次新的资源和权限集合
     */
    private void loadResourcesDefine(){
        List<String> list1 = load_ALL_AUTORITIES_QUERY();
        if (list1!=null){
            for (String str:list1){
                SecurityConfig config=new SecurityConfig(str);
                allAttribute.add(config);
            }
        }else{
            logger.info("查询所有权限集合失败... 集合为空 ");
        }
        resourceMap=new HashMap<>();
        List<Role_Resource> list = loadAUTHORITIES_AND_RESOURCES_Query();
        if (list!=null){
            for (Role_Resource rr:list){
                System.out.println(rr.getResource_id());
                System.out.println(rr.getUser_type_id());
                System.out.println(rr.getRole_name());
                System.out.println(rr.getUrl());
                long resource_id = rr.getResource_id();
                List<String> authorityByResource = getAuthorityByResource(resource_id, list);
                RequestMatcher matcher=new AntPathRequestMatcher(rr.getUrl());
                System.out.println(authorityByResource.size());
                Collection<ConfigAttribute> arry=new ArrayList<>(authorityByResource.size());
                for (String autority:authorityByResource){
                    SecurityConfig cofig=new SecurityConfig(autority);
                    arry.add(cofig);
                }
                resourceMap.put(matcher,arry);
            }
        }else {
            logger.info("查询权限和资源映射集合失败  集合为空..");
        }

    }

这个方法就是加载初始化的方法,跟着方法走首先我们去加载所有的权限,并循环加入allAttribute中。然后我们去加载资源和权限对应的实体类集合,在这个集合中遍历把同一资源需要的权限放入一个集合中。然后在把这个资源和权限集合加入到resourceMap。过程很好理解

这个方法是用来获取当前请求的资源所需要的权限集合,后面会在流程中涉及到。

    /**
     * 判断是否当前request请求能和map中的资源进行匹配,如果匹配成功返回对应的需要的权限集合 否则匹配不到
     * @param o 当前object
     * @return 权限集合
     * @throws IllegalArgumentException
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        HttpServletRequest request=((FilterInvocation)o).getRequest();
        Collection<ConfigAttribute> arrhashset=new HashSet<>();
        for (Map.Entry<RequestMatcher,Collection<ConfigAttribute>> entry:resourceMap.entrySet()){
            if (entry.getKey().matches(request)){
                logger.info("request matches: "+request.getRequestURL());
                arrhashset.addAll(entry.getValue());
            }
        }
        if (arrhashset.size()>0){
            return new ArrayList<>(arrhashset);
        }
        logger.info("request no matches");
        return Collections.emptyList();
    }

总结一下:用户去请求页面,首先要去判断有用户自身的权限和系统中的权限是否相对应,就是去accessDecisionManager这里判断,那么这里判断的依据则来自filterInvocationSecurityMetadataSource,然后用户自身的权限来自authenticationManager。

跟着源码走一遍页面请求流程

首先一个请求进来,被自定义的filter拦截

@Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        FilterInvocation invocation=new FilterInvocation(servletRequest,servletResponse,filterChain);
        InterceptorStatusToken interceptorStatusToken = super.beforeInvocation(invocation);

进入beforeinvocation进行受保护对象的权限校验

 protected InterceptorStatusToken beforeInvocation(Object object) {
        Assert.notNull(object, "Object was null");
        boolean debug = this.logger.isDebugEnabled();
        if(!this.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: " + this.getSecureObjectClass());
        } else {
            Collection attributes = this.obtainSecurityMetadataSource().getAttributes(object);

进入getAttributes去获取相应的需要的权限集合

也就是上面的那段代码

然后跟着一系列判断 之前获取的权限集合是否为空 还有获取当前SecurityContextHolder中的用户对象

if(attributes != null && !attributes.isEmpty()) {
                if(debug) {
                    this.logger.debug("Secure object: " + object + "; Attributes: " + attributes);
                }

                if(SecurityContextHolder.getContext().getAuthentication() == null) {
                    this.credentialsNotFound(this.messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", "An Authentication object was not found in the SecurityContext"), object, attributes);
                }

                Authentication authenticated = this.authenticateIfRequired();

进入this.accessDecisionManager.decide(authenticated, object, attributes)方法来决定

 try {
                    this.accessDecisionManager.decide(authenticated, object, attributes);
                } catch (AccessDeniedException var7) {
                    this.publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, var7));
                    throw var7;
                }

decide方法前半部分,AffirmativeBased的decide规则上面说过了。只要有一个投票器投赞成票,则通过。否则抛出AccessDeniedException

public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
        int deny = 0;
        Iterator var5 = this.getDecisionVoters().iterator();

        while(var5.hasNext()) {
            AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
            int result = voter.vote(authentication, object, configAttributes);
            if(this.logger.isDebugEnabled()) {
                this.logger.debug("Voter: " + voter + ", returned: " + result);
            }

进入voter.vote(authentication, object, configAttributes)观察投票

首先获取这个用户所具有的权限集合,然后循环判断当前attribute是否support,如果true则不继续循环,获取这个用户拥有的权限集合,循环对比请求的这个资源的权限是否和用户真身所拥有的权限相等,是返回1

    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        if(authentication == null) {
            return -1;
        } else {
            byte result = 0;
            Collection authorities = this.extractAuthorities(authentication);
            Iterator var6 = attributes.iterator();

            while(true) {
                ConfigAttribute attribute;
                do {
                    if(!var6.hasNext()) {
                        return result;
                    }

                    attribute = (ConfigAttribute)var6.next();
                } while(!this.supports(attribute));

                result = -1;
                Iterator var8 = authorities.iterator();

                while(var8.hasNext()) {
                    GrantedAuthority authority = (GrantedAuthority)var8.next();
                    if(attribute.getAttribute().equals(authority.getAuthority())) {
                        return 1;
                    }
                }
            }
        }
    }

向上一级返回到decide方法中去,如果有一个result为1 则通过。否则计算拒绝次数。只要拒绝一次抛出AccessDeniedException否则检测是否支持弃权。

switch(result) {
            case -1:
                ++deny;
                break;
            case 1:
                return;
            }
        }

        if(deny > 0) {
            throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
        } else {
            this.checkAllowIfAllAbstainDecisions();
        }
    }

再返回上一级 创建一个InterceptorStatusToken 然后进入doFilter

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容