Spring security4.1.0 自定義角色和權(quán)限(長文)

文章大綱:
1.spring security 基本配置介紹
2.自定義角色和權(quán)限配置
3.跟著源碼走一遍頁面請求流程

spring security 基本配置介紹

首先需要創(chuàng)建一個為Spring security的專門的配置文件 然后引入相應(yīng)的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>

根據(jù)你使用的版本配置相應(yīng)的命名空間,我使用的是4.1.0的Spring security。

Spring Security 命名空間的引入可以簡化我們的開發(fā),它涵蓋了大部分 Spring Security 常用的功能。它的設(shè)計是基于框架內(nèi)大范圍的依賴的,可以被劃分為以下幾塊。

1.Web/Http 安全:這是最復(fù)雜的部分。通過建立 filter 和相關(guān)的 service bean 來實現(xiàn)框架的認(rèn)證機(jī)制。當(dāng)訪問受保護(hù)的 URL 時會將用戶引入登錄界面或者是錯誤提示界面。

2.業(yè)務(wù)對象或者方法的安全:控制方法訪問權(quán)限的。
2.1AuthenticationManager:處理來自于框架其他部分的認(rèn)證請求。
2.2AccessDecisionManager:為 Web 或方法的安全提供訪問決策。會注冊一個默認(rèn)的,但是我們也可以通過普通 bean 注冊的方式使用自定義的 AccessDecisionManager。
2.3AuthenticationProvider:AuthenticationManager 是通過它來認(rèn)證用戶的。
2.4UserDetailsService:跟 AuthenticationProvider 關(guān)系密切,用來獲取用戶信息的。
2.5 UserDetails :跟UserDetailsService關(guān)系密切,用于封裝一個用戶信息的實體類接口 ,默認(rèn)實現(xiàn)是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:表示登錄時用戶名使用的是哪個參數(shù),默認(rèn)是 “j_username”。
2.password-parameter:表示登錄時密碼使用的是哪個參數(shù),默認(rèn)是 “j_password”。
3.login-processing-url:表示登錄時提交的地址,默認(rèn)是 “/j-spring-security-check”。這個只是 Spring Security 用來標(biāo)記登錄頁面使用的提交地址,真正關(guān)于登錄這個請求是不需要用戶自己處理的。
4.login-page: 代表你的登錄頁面。

<security:intercept-url 代表你要攔截的url pattern 是攔截的url或者可以是一個正則表達(dá)式 access代表允許請求的用戶角色 我們可以看到/** 需要的是ROLE_USER這樣的權(quán)限,還有
IS_AUTHENTICATED_ANONYMOUSLY 允許匿名用戶進(jìn)入
IS_AUTHENTICATED_FULLY 允許登錄用戶進(jìn)入
IS_AUTHENTICATED_REMEMBERED 允許登錄用戶和rememberMe用戶進(jìn)入

當(dāng)用戶試圖訪問被配置攔截的URL時,security會先去判斷是否登錄,如果沒有則會跳轉(zhuǎn)到登錄界面進(jìn)行登錄認(rèn)證。當(dāng)你登錄成功進(jìn)去之后,會根據(jù)你是否具有相應(yīng)的URL權(quán)限給予放行,如果有則跳轉(zhuǎn),沒有則變成403 Access Denied

訪問了不具有相應(yīng)權(quán)限的URL

那么我們?nèi)绾稳レ`活的控制什么樣的用戶權(quán)限擁有什么樣的可訪問資源呢?這個也是這個文章的重點,需要根據(jù)自己的業(yè)務(wù)去設(shè)置不同用戶訪問不同的頁面。也就是自定義用戶角色和權(quán)限,我們可以把這些定義的數(shù)據(jù)放在數(shù)據(jù)庫中,而不是放在配置文件里面。這樣就可以很靈活的去改變用戶和權(quán)限而不需要改變項目。

自定義角色和權(quán)限配置

接下來我以自己項目中實踐的內(nèi)容為例子來講解配置自定義角色和權(quán)限

1.第一步先把數(shù)據(jù)庫中的表結(jié)構(gòu)準(zhǔn)備好,

用戶表( 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個表就可以滿足整個的權(quán)限控制。而我自己因為一個用戶只有一種角色,所以我實際是沒有用戶角色表的(這個看個人的情況而定吧)

2.我們來看看Spring security的配置文件中定義了哪些用到的bean,根據(jù)這些bean來進(jìn)行說明

    <!-- 登錄頁面不進(jìn)行過濾
    有兩種方法不過濾指定的頁面,一種是 像下面這樣 還有一種是
    <security:intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/> 
    像這樣子的 配置access為允許匿名用戶訪問
    兩者的區(qū)別是 前者none不配置任務(wù)的過濾鏈 后者會進(jìn)入Spring的過濾鏈-->
    <security:http pattern="/index/login" security="none"/>

這個就比較簡單了,就是我不過濾登錄頁面,防止登錄頁面也需要認(rèn)證然后跳轉(zhuǎn)到登錄頁面變成死循環(huán)。


    <security:http use-expressions="false">
        <!--在Spring 4中使用 security時 如果要使用access IS_AUTHENTICATED_ANONYMOUSLY 的
         話 要 配置use-expressions 為false(默認(rèn)為ture) 默認(rèn)是使用Spring EL expression
         而你沒有使用 或者access="hasRole('...').
         解決來源: http://stackoverflow.com/questions/33362315/failed-to-evaluate-expression-is-authenticated-anonymously-spring-4  -->

然后是是否使用Spring的EL表達(dá)式,因為我之前在剛認(rèn)識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。登錄認(rèn)證請求的URL為/index/login.do 然后使用login_name 和password 兩個參數(shù)來驗證用戶名和密碼 如果登錄成功 跳轉(zhuǎn) /index/index 如果登錄失敗跳轉(zhuǎn)/index.jsp

<!--
         當(dāng)你配置了自定義的攔截器的時候,而且我攔截的請求是存放在數(shù)據(jù)庫中的,所有當(dāng)我輸入一個請求URL時
         它會去getAttributes 然后把這個請求和數(shù)據(jù)庫中的資源進(jìn)行匹配 當(dāng)匹配成功時 發(fā)現(xiàn)用戶沒有登錄
         然后就跳轉(zhuǎn)到登錄頁面(個人理解)如果請求的是一個非數(shù)據(jù)庫存儲的URL那么就完全找不到這個請求 404-->
        <security:custom-filter ref="myFilterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/>
        <!-- 禁用csrf機(jī)制 這個機(jī)制是用于防止黑客攻擊  具體介紹參考百度百科 -->
    <security:csrf disabled="true"/>
        <!--停用對匿名認(rèn)證的支持 -->
    <security:anonymous enabled="false"/>
    </security:http>

這個里面的custom filter就是我自定義的過濾器,before代表放在這個指定的過濾器之前。也就是指定一個順序。這個過濾器后面會用到。其他的還有after first last position 等 代表 放在..后面 放在最前面 放在最后面 放在指定的過濾器位置替換掉它。

然后我禁用了csrf (默認(rèn)是true)因為我在練手的時候也沒有使用這么高級的csrf認(rèn)證機(jī)制,如果不禁用 你登錄的時候會報錯。如果你之前已經(jīng)實踐過這個部分,就不虛。我還沒去弄csrf認(rèn)證等后面再搞,這里就先禁用掉了。

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

還有就是匿名認(rèn)證(默認(rèn)是true),之前也是練手的時候不懂使用了false。其實匿名認(rèn)證是在IS_AUTHENTICATED_ANONYMOUSLY這樣的Access是會給予放行。你可以指定一些頁面不需要認(rèn)證登錄就能訪問。所以其實不用配置為false。忽略我的false。

 <!--認(rèn)證管理器 實現(xiàn)用戶進(jìn)行登錄鑒定的類 主要實現(xiàn)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>

認(rèn)證管理器主要實現(xiàn)UserDetailsService這個接口,我自己自定義了一個UserDetailsService。默認(rèn)是使用security中提供的JdbcDaoImpl,我的自定義實現(xiàn)也是參考源碼JdbcDaoImpl的實現(xiàn)來完成的?,F(xiàn)在我們來看看JdbcDaoImpl中有哪些內(nèi)容


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 :默認(rèn)的查詢用戶Query語句
2.DEF_AUTHORITIES_BY_USERNAME_QUERY 默認(rèn)的查詢用戶權(quán)限的Query語句
3.DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY 默認(rèn)的查詢組權(quán)限的Query語句
4.messages 用于記錄一些程序中日志信息和錯誤信息
5.authoritiesByUsernameQuery 用于自定義的權(quán)限查詢Query語句
6.groupAuthoritiesByUsernameQuery 用于自定義的組權(quán)限查詢Query語句
7.usersByUsernameQuery 用于自定義的用戶信息查詢Query語句
8.rolePrefix 角色前綴
9.usernameBasedPrimaryKey 如果為真 則使用query語句查詢出來的用戶名作為用戶實體的username 否則 則使用你用于登錄認(rèn)證時上傳的用戶名作為實體的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 是否支持權(quán)限認(rèn)證
11.enableGroups 是否支持組權(quán)限認(rèn)證

我的自定義MyUserDetailsService實現(xiàn)

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層 進(jìn)行登錄名認(rèn)證的規(guī)則定義
 * 自定義查詢數(shù)據(jù)庫規(guī)則 并返回相應(yīng)的實體類
 */
@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 否則 則使用你用于登錄認(rèn)證時上傳的用戶名作為實體的username
    private boolean enableAuthorities = true;//是否支持權(quán)限驗證
    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);
            }

    }

    /**
     * 根據(jù)用戶名去加載相應(yīng)的認(rèn)證信息 完成登錄認(rèn)證
     * @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);
        }
    }

    /**
     * 根據(jù)用戶名加載用戶的相應(yīng)權(quán)限
     * @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);
        }
    }

    /**
     * 根據(jù)用戶名加載用戶所在組的相應(yīng)權(quán)限
     * @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("權(quán)限為空....");
                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;
    }
}

來看看之前出現(xiàn)過的自定義過濾器

<!--過濾鏈 -->
    <bean id="myFilterSecurityInterceptor" class="Index.MyFilterSecurityInterceptor">
        <property name="accessDecisionManager" ref="myAccessDescisionManager"/>
        <property name="authenticationManager" ref="authenticationManager"/>
        <property name="filterInvocationSecurityMetadataSource" ref="mySecurityMetadataSource"/>
    </bean>
    <!--安全資源元數(shù)據(jù) 用于加載和初始化項目總體資源和權(quán)限的映射集合 -->
    <bean id="mySecurityMetadataSource" class="Index.MySecurityMetadataSource">
    </bean>
authenticationManager

其中有三個bean,其中的一個authenticationManager就是剛剛介紹的那個用于登錄認(rèn)證之后獲取當(dāng)前用戶所擁有的權(quán)限。先梳理一下整個認(rèn)證的流程。

Web項目啟動.png
accessDecisionManager

第二個bean accessDecisionManager 這個決定管理器里面有2個voter投票器,一個是roleVoter 一個是authenticatedVoter。在roleVoter里面配置了角色前綴,我這里配置的是"",這里其實就是文章開頭介紹簡單配置時候<security:intercept-url pattern="/**" access="ROLE_USER" /> Access這里會看到有一個前綴,這個前綴來源就是這個RoleVoter 默認(rèn)是前綴是ROLE

 <!--訪問決定管理器 用于決定是否對請求進(jìn)行拒絕或者允許通行 -->
    <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>
    <!--鑒權(quán)投票器 -->
    <bean id="authenticatedVoter" class="org.springframework.security.access.vote.AuthenticatedVoter">
    </bean>

以上如果實用了useExpressions(有屬性use-expressions指定,默認(rèn)的也是true)即SPEL表達(dá)式,則選擇WebExpressionVoter,否則選擇RoleVoter及AuthenticatedVoter

security中提供了3個accessDecisionManager 的實現(xiàn),我的這次例子也是使用了其中一個(AffirmativeBased)。因為目前還不需要自己重新實現(xiàn)。security自帶的功能一般都比較強(qiáng)大的。

Spring Security內(nèi)置了三個基于投票的AccessDecisionManager實現(xiàn)類,它們分別是AffirmativeBased、ConsensusBased和UnanimousBased。

   AffirmativeBased的邏輯是這樣的:

   (1)只要有AccessDecisionVoter的投票為ACCESS_GRANTED則同意用戶進(jìn)行訪問;

   (2)如果全部棄權(quán)也表示通過;

   (3)如果沒有一個人投贊成票,但是有人投反對票,則將拋出AccessDeniedException。

   ConsensusBased的邏輯是這樣的:

   (1)如果贊成票多于反對票則表示通過。

   (2)反過來,如果反對票多于贊成票則將拋出AccessDeniedException。

   (3)如果贊成票與反對票相同且不等于0,并且屬性allowIfEqualGrantedDeniedDecisions的值為true,則表示通過,否則將拋出異常AccessDeniedException。參數(shù)allowIfEqualGrantedDeniedDecisions的值默認(rèn)為true。

   (4)如果所有的AccessDecisionVoter都棄權(quán)了,則將視參數(shù)allowIfAllAbstainDecisions的值而定,如果該值為true則表示通過,否則將拋出異常AccessDeniedException。參數(shù)allowIfAllAbstainDecisions的值默認(rèn)為false。

   UnanimousBased的邏輯與另外兩種實現(xiàn)有點不一樣,另外兩種會一次性把受保護(hù)對象的配置屬性全部傳遞給AccessDecisionVoter進(jìn)行投票,而UnanimousBased會一次只傳遞一個ConfigAttribute給AccessDecisionVoter進(jìn)行投票。這也就意味著如果我們的AccessDecisionVoter的邏輯是只要傳遞進(jìn)來的ConfigAttribute中有一個能夠匹配則投贊成票,但是放到UnanimousBased中其投票結(jié)果就不一定是贊成了。UnanimousBased的邏輯具體來說是這樣的:

   (1)如果受保護(hù)對象配置的某一個ConfigAttribute被任意的AccessDecisionVoter反對了,則將拋出AccessDeniedException。

   (2)如果沒有反對票,但是有贊成票,則表示通過。

   (3)如果全部棄權(quán)了,則將視參數(shù)allowIfAllAbstainDecisions的值而定,true則通過,false則拋出AccessDeniedException。

大家想再了解跟著走一遍,可以去看security包中相應(yīng)的源碼實現(xiàn)。

filterInvocationSecurityMetadataSource

這個是自定義去加載相應(yīng)的資源和權(quán)限映射,默認(rèn)是使用了Spring的JDBCtemplate 我的DAO是使用Hibernate去持久化的。所以需要重寫這個實現(xiàn)類。繼承相應(yīng)的FilterInvocationSecurityMetadataSource接口,這個類中主要的內(nèi)容儲存在一個靜態(tài)map變量中。

/**
 *  資源源數(shù)據(jù)定義,將所有的資源和權(quán)限對應(yīng)關(guān)系建立起來,即定義某一資源可以被哪些角色訪問
 */
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    //將數(shù)據(jù)庫中的所有權(quán)限和資源查詢出來 建立對應(yīng)關(guān)系
    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的簡單策略。它有很多實現(xiàn)類,對應(yīng)著不同的匹配策略。
value就是匹配資源所對應(yīng)的權(quán)限集合

其他的成員變量就是一些我自定義的用于Query語句查詢所有權(quán)限和資源的字符串常量

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

    /**
     * 根據(jù)相應(yīng)的查詢語句去數(shù)據(jù)庫加載資源和權(quán)限 初始化map集合
     * 這里初始化map的key時 固定使用RequestMatcher接口中的AntPathRequestMatcher
     * RequestMatcher還有很多實現(xiàn)類 不過目前還不是很明確具體是如何使用和配置這些類 暫定固定使用固定使用RequestMatcher接口中的AntPathRequestMatcher
     *
     * 這個類會在web第一次啟動的時候把權(quán)限和資源初始化 并緩存起來
     * 但是如果在后面的權(quán)限發(fā)生改變了,那么就會導(dǎo)致無法更新
     * 一種解決方案是:在getAttributes那里直接從數(shù)據(jù)庫中查詢相應(yīng)的url權(quán)限
     * 另一種解決方案:在有更新權(quán)限和資源集合的時候 再次調(diào)用loadResourcesDefine去重新加載一次新的資源和權(quán)限集合
     */
    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("查詢所有權(quán)限集合失敗... 集合為空 ");
        }
        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("查詢權(quán)限和資源映射集合失敗  集合為空..");
        }

    }

這個方法就是加載初始化的方法,跟著方法走首先我們?nèi)ゼ虞d所有的權(quán)限,并循環(huán)加入allAttribute中。然后我們?nèi)ゼ虞d資源和權(quán)限對應(yīng)的實體類集合,在這個集合中遍歷把同一資源需要的權(quán)限放入一個集合中。然后在把這個資源和權(quán)限集合加入到resourceMap。過程很好理解

這個方法是用來獲取當(dāng)前請求的資源所需要的權(quán)限集合,后面會在流程中涉及到。

    /**
     * 判斷是否當(dāng)前request請求能和map中的資源進(jìn)行匹配,如果匹配成功返回對應(yīng)的需要的權(quán)限集合 否則匹配不到
     * @param o 當(dāng)前object
     * @return 權(quán)限集合
     * @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();
    }

總結(jié)一下:用戶去請求頁面,首先要去判斷有用戶自身的權(quán)限和系統(tǒng)中的權(quán)限是否相對應(yīng),就是去accessDecisionManager這里判斷,那么這里判斷的依據(jù)則來自filterInvocationSecurityMetadataSource,然后用戶自身的權(quán)限來自authenticationManager。

跟著源碼走一遍頁面請求流程

首先一個請求進(jìn)來,被自定義的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);

進(jìn)入beforeinvocation進(jìn)行受保護(hù)對象的權(quán)限校驗

 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);

進(jìn)入getAttributes去獲取相應(yīng)的需要的權(quán)限集合

也就是上面的那段代碼

然后跟著一系列判斷 之前獲取的權(quán)限集合是否為空 還有獲取當(dāng)前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();

進(jìn)入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規(guī)則上面說過了。只要有一個投票器投贊成票,則通過。否則拋出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);
            }

進(jìn)入voter.vote(authentication, object, configAttributes)觀察投票

首先獲取這個用戶所具有的權(quán)限集合,然后循環(huán)判斷當(dāng)前attribute是否support,如果true則不繼續(xù)循環(huán),獲取這個用戶擁有的權(quán)限集合,循環(huán)對比請求的這個資源的權(quán)限是否和用戶真身所擁有的權(quán)限相等,是返回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 則通過。否則計算拒絕次數(shù)。只要拒絕一次拋出AccessDeniedException否則檢測是否支持棄權(quán)。

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();
        }
    }

再返回上一級 創(chuàng)建一個InterceptorStatusToken 然后進(jìn)入doFilter

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容