web-security第六期:暢談 Spring Security Authorization(授權)

源碼地址:demo-world (spring-sercurity模塊)

目錄

1.簡介

2.GrantedAuthority

3.AccessDecisionManager

4.角色(role)

5.FilterSecurityInterceptor

6.基於Handler方法的權限控制

6.1.@PreAuthorize


1.簡介

前幾期我們瞭解了Spring Security Authentication (認證),今天我們來說道說道  Spring Security Authorization(授權)

這裏舉一個通俗易懂的例子:你去火車站買票乘車,你需要兩樣東西,身份證和車票,在去月臺之前,你需要刷一下身份證,確認你是本人,這就是認證,當你坐上了火車,這時候列車員回來檢查你的車票,來確定你是否可以做這一班車,這就是授權

首先我們來回顧一下 認證的結果:

securitycontextholder

也就是Authentication中的內容,我們來逐一分析一下:

  • Principal:這是登錄用戶的信息,一般是指 UserDetails (它的實現類,在前幾期我們有定義)
  •  Credentials:通常是密碼。在許多情況下,將在驗證用戶身份後清除此內容(設置爲null),以確保它不會泄漏。
  • Authorities:這裏存放的是一個  GrantedAuthority 集合,也就是授予用戶的權限, GrantedAuthority對象由AuthenticationManager插入到Authentication對象中,並在以後做出授權決策時由AccessDecisionManager讀取。這是本節一個重點信息

2.GrantedAuthority

GrantedAuthoritys are high level permissions the user is granted. A few examples are roles or scopes.

可以從Authentication.getAuthorities()方法獲得GrantedAuthoritys。此方法提供了GrantedAuthority對象的集合。GrantedAuthority是授予用戶的權限。GrantedAuthority通常由UserDetailsS​​ervice加載。 此類權限通常有兩種:

  1. 角色:例如Admin或Common_user。稍後這些角色將作爲配置 web authorization(web授權), method authorization(方法授權) domain object authorization(領域模型授權)的依據。 Spring Security的其他部分能夠解釋這些權限。使用基於用戶名/密碼的身份驗證時。
  2. 範圍:它們不特定於給定的域對象。因此,您不太可能就某個用戶可以訪問編號爲18的部門信息而單獨設置一個權限,因爲如果有成千上萬個這樣的權限,您很快就會用光內存(或者至少導致應用程序花費很長時間)時間來認證用戶。當然,Spring Security是專門爲滿足這一通用要求而設計的,但您可以爲此目的使用項目的域對象安全功能。例如所有的用戶都可以訪問部門信息這個域對象,但每個人可以訪問的信息範圍不同,公司高管可以訪問全部部門信息,而部門高管只能訪問本部門的信息;

Spring Security包含一個具體的GrantedAuthority實現,即SimpleGrantedAuthority。它允許將任何用戶指定的String轉換爲GrantedAuthority。安全體系結構中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority來填充Authentication對象。

3.AccessDecisionManager

AccessDecisionManager 由 AbstractSecurityInterceptor 調用,並負責做出最終的訪問控制決策。 AccessDecisionManager接口包含三種方法:

void decide(Authentication authentication, Object secureObject,
    Collection<ConfigAttribute> attrs) throws AccessDeniedException;

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

decide方法的參數包括它進行授權決策所需的所有相關信息。secureObject是安全對象,也就是我要保護的訪問資源,例如 method authorization(方法授權)中方法就是受保護的資源然後在AccessDecisionManager中實現某種安全性邏輯以確保安全訪問。如果訪問被拒絕,則預期實現將引發AccessDeniedException。

在啓動時,AbstractSecurityInterceptor將調用support(ConfigAttribute)方法,以確定AccessDecisionManager是否可以處理傳遞的ConfigAttribute。安全攔截器實現調用support(Class)方法,以確保配置的AccessDecisionManager支持安全攔截器將顯示的安全對象的類型。

4.角色(role)

首先,GrantedAuthority通常由UserDetailsS​​ervice加載的,因此,我們使用SimpleGrantedAuthority來簡單爲用戶分配一個角色

首先我們來改造一下我們的UserDO,增加一個權限集合字段:

    /**
     * 權限集合
     */
    Set<GrantedAuthority> authorities;

然後我們將回到UserDetails的配置中來,實現該接口的方法:

     /**
     * 返回授予用戶的權限
     *
     * @return 權限集合
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return userDO.getAuthorities();
    }

我們還需要修改一下僞數據源

public class DataSource {
    public static UserDO getUserByUsername(String username) {
        SimpleGrantedAuthority roleIntern = new SimpleGrantedAuthority("ROLE_INTERN");
        SimpleGrantedAuthority roleDba = new SimpleGrantedAuthority("ROLE_DBA");
        SimpleGrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN");
        List<UserDO> userList = new ArrayList<>();
        //初始化三個用戶
        userList.add(new UserDO(1L, "swing", new BCryptPasswordEncoder().encode("123456"), 20, Arrays.asList(roleDba, roleIntern)));
        userList.add(new UserDO(2L, "sky", new BCryptPasswordEncoder().encode("123456"), 20, Collections.singletonList(roleIntern)));
        userList.add(new UserDO(3L, "admin", new BCryptPasswordEncoder().encode("123456"), 20, Arrays.asList(roleDba, roleIntern, roleAdmin)));
        return userList.stream().distinct().filter(userDO -> userDO.getUsername().equals(username)).collect(Collectors.toList()).get(0);
    }
}

驗證我們配置的結果: 

OK!成功給用戶分配了角色,角色也是權限的集合,一個角色可以擁有很多權限,例如實習生可以查看文件和修改文件(兩個權限),但不可以增加文件和刪除文件,那麼Spring是如何做到這一點的呢?答案就在 AccessDecisionManager 中,顧名思義,這是訪問決策中心,一個訪問是否被允許,就是由此類決定的

Spring Security提供了攔截器,用於控制對安全對象的訪問,例如方法調用或Web請求。 AccessDecisionManager會做出關於是否允許進行調用的調用前決定。

5.FilterSecurityInterceptor

FilterSecurityInterceptor爲HttpServletRequests提供授權。它作爲安全篩選器之一插入到FilterChainProxy中,要注意不要被名字迷惑,它不是攔截器,是過濾器

filtersecurityinterceptor

  • number 1 First, the FilterSecurityInterceptor obtains an Authentication from the SecurityContextHolder.

  • number 2 Second, FilterSecurityInterceptor creates a FilterInvocation from the HttpServletRequestHttpServletResponse, and FilterChain that are passed into the FilterSecurityInterceptor.

  • number 3 Next, it passes the FilterInvocation to SecurityMetadataSource to get the ConfigAttributes.

  • number 4 Finally, it passes the AuthenticationFilterInvocation, and ConfigAttributes to the AccessDecisionManager.

    • number 5 If authorization is denied, an AccessDeniedException is thrown. In this case the ExceptionTranslationFilter handles the AccessDeniedException.

    • number 6 If access is granted, FilterSecurityInterceptor continues with the FilterChain which allows the application to process normally.

默認情況下,Spring Security 將會對所有的請求進行身份認證,如下:

protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .authorizeRequests(authorize -> authorize
            .anyRequest().authenticated()
        );
}

也可以自定義認證規則,如下:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        //禁用csrf (跨站請求僞造)
        http.csrf().disable();

        http.authorizeRequests(authorize -> authorize
                //允許直接訪問
                .mvcMatchers("/login").permitAll()
                .mvcMatchers("/file/1/**").hasRole("INTERN")
                .mvcMatchers("/file/4/**").hasRole("ADMIN")
                //同時具有兩種角色纔可訪問的api
                .mvcMatchers("/login/page").access("hasRole('INTERN') and hasRole('DBA')")
                //剩下的都拒絕
                .anyRequest().denyAll()
        );
        //將認證設置在UsernamePasswordAuthenticationFilter之前
        http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

這裏有幾個很重要的點要注意一下:

  • 分配角色的時候我們使用 ROLE_角色名 的標準格式,這樣在讀取角色時候,我們只用表示爲 hasRole(角色名)即可
  • anyRequest().denyAll  和 anyRequest.authenticatied 是不同的概念 前者表示對於前面沒有配置訪問策略的URL ,一律拒絕,而後者表示,對所有的url進行認證,認證通過後即可訪問。

這裏由一張由官方提供的內置表達式表格:

Expression Description

hasRole(String role)

Returns true if the current principal has the specified role.

For example, hasRole('admin')

By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

hasAnyRole(String…​ roles)

Returns true if the current principal has any of the supplied roles (given as a comma-separated list of strings).

For example, hasAnyRole('admin', 'user')

By default if the supplied role does not start with 'ROLE_' it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

hasAuthority(String authority)

Returns true if the current principal has the specified authority.

For example, hasAuthority('read')

hasAnyAuthority(String…​ authorities)

Returns true if the current principal has any of the supplied authorities (given as a comma-separated list of strings)

For example, hasAnyAuthority('read', 'write')

principal

Allows direct access to the principal object representing the current user

authentication

Allows direct access to the current Authentication object obtained from the SecurityContext

permitAll

Always evaluates to true

denyAll

Always evaluates to false

isAnonymous()

Returns true if the current principal is an anonymous user

isRememberMe()

Returns true if the current principal is a remember-me user

isAuthenticated()

Returns true if the user is not anonymous

isFullyAuthenticated()

Returns true if the user is not an anonymous or a remember-me user

hasPermission(Object target, Object permission)

Returns true if the user has access to the provided target for the given permission. For example, hasPermission(domainObject, 'read')

hasPermission(Object targetId, String targetType, Object permission)

Returns true if the user has access to the provided target for the given permission. For example, hasPermission(1, 'com.example.domain.Message', 'read')

6.基於Handler方法的權限控制

上面我們使用了Role來控制某個用戶是否可以訪問一個接口(或handler 方法),但很明顯,在實際開發中,這樣使用會有很多侷限性,例如對用戶是否有權訪問某一個接口的判斷過程很複雜,那麼,這種直接在HttpSecurity中配置的方法,顯然就不太實用了,因此需要使用基於 Handler 方法的權限控制,Spring爲我們提供了幾個很有用的註解

要使用註解,首先在SecurityConfig中開啓它

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

6.1.@PreAuthorize

這是一個很實用的註解,它可以決定方法是否可以被調用,如下:

    /**
     * 獲取登錄頁面
     *
     * @return 登錄頁面
     */
    @PreAuthorize("hasRole('INTERN') and  hasRole('DBA')")
    @GetMapping("/page")
    String login() {
        return "login";
    }

這種寫法和之前我們配置的作用相同,當然此註解還有更強大的地方,例如還可以這麼寫(表示只有id =2纔可以訪問):

     @PreAuthorize("hasRole('INTERN') and #id==2")
    @GetMapping("/1/{id}")
    @ResponseBody
    public FileDO getFileNameById(@PathVariable Long id) {
        return new FileDO(id, "這個殺手不太冷.mp4", 1231232423L);
    }

不過,大部分時候這些功能還是不能滿足我們的需求,所以該註解還支持注入Bean,和自定義權限認證,我們來寫一個簡單的例子:

/**
 * 自定義認證方法
 *
 * @author swing
 */
@Service("as")
public class AuthorizeService {

    /**
     * 是否授權
     *
     * @return 是否可以訪問呢
     */
    public boolean hasPermission(String username) {
        UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return userDetails.getUsername().equals(username);
    }
}
    /**
     * 獲取文件
     *
     * @param id 文件Id
     * @return 文件信息
     */
    @PreAuthorize("@as.hasPermission('swing')")
    @GetMapping("/1/{id}")
    @ResponseBody
    public FileDO getFileNameById(@PathVariable Long id) {
        return new FileDO(id, "這個殺手不太冷.mp4", 1231232423L);
    }

以上代碼表示,只有用戶名爲swing的用戶才能訪問此接口

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章