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閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容