把玩 Spring Security [2] 探索 Access Control 功能

- notes spring-security

在先前的實驗中,我們製作了一個新的 Security Filter 放行了任何 HTTP REQUEST。在這個基礎上,我們可以來探索 Spring Security 的 Access Control。

在安全的領域中,有二個主要的術語要認得:

在前一篇介紹,我們知道了 Spring Security 如何得知一個 HTTP REQUEST 有沒有獲得 Authentication,即為 獲得身份驗證。系統知道這一個 HTTP REQUST 是代表者某一個特定的使用者,或特定的裝置,或更通俗一點的 Client。那麼在獲得身份之後,一個安全系統會關心的問題是,用這個身份能做些什麼?即為 Authorization,被 授權 了哪些能力。

路徑與授權控制

將上回的範例修改如下,在 antMatchers 後,我們多加了 hasRole 去限制對應路徑需要的 授權

@Component
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(new FriendlyFilter(), LogoutFilter.class);

        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/users/**").hasRole("USER")
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/info")
                .authenticated()
                .anyRequest().permitAll();
    }
}

這樣的寫法相當直覺好懂:

由於增加了這部分,我們的 Controller 也會加一些新的方法:

@RestController
public class SimpleController {

    @RequestMapping("/")
    public String home() {
        return "home";
    }

    @RequestMapping("/users")
    public String users() {
        return "users";
    }

    @RequestMapping("/admin")
    public String admin() {
        return "admin";
    }

    @RequestMapping("/info")
    public String info() {
        return "info";
    }
}

提供授權資訊

在目前版本的 Security Filter 除了回應 isAuthenticated() 之外沒有額外提供進一步資訊:

public class FriendlyFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        SecurityContextHolder.getContext().setAuthentication(new ApiToken());
        filterChain.doFilter(request, response);
    }
}

我們能透過它來提供授權資料,ApiToken 只是單純實作了 Authentication 介面,並強制 isAuthenticated 回應 true 罷了。這麼作只是為了讓 Spring Security 認為這個 HTTP REQUEST 是已通過驗證。授權資料可以透過覆寫 getAuthorities() 方法來提供:

SecurityContextHolder.getContext().setAuthentication(new ApiToken() {
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(
                new SimpleGrantedAuthority("ROLE_ADMIN"));
    }
});

這簡單地修改,強制了目前的 Authentication 物件,回傳了一個含有 ROLE_ADMIN 的授權資料。這邊比先前的 hasRole 中的設定,多出了 ROLE_ 前綴字串,這是預設的 AccessDecisionVoter 的行為 (RoleVoter)。簡單實測,僅有需要 USER role 的 /users 被阻擋:

$ curl http://127.0.0.1:8787/admin
admin
$ curl http://127.0.0.1:8787/users
{"timestamp":"2021-09-27T12:50:50.232+00:00","status":403,"error":"Forbidden","path":"/users"}
$ curl http://127.0.0.1:8787/info
info

使用標註式授權

除了透過 HttpSecurity 對特定路徑指令可以存取的角色,也能透過 Annotation 來指定授權。這樣,權限就能針對任何 Spring 管理的物件方法進行設定,以下是使用 @PreAuthorize 指定 info 方法需要有 USER role 才能使用:

@PreAuthorize("hasRole('USER')")
@RequestMapping("/info")
public String info() {
    return "info";
}

但別忘了,這種用法需要對 Spring Boot Application 加開新的設定 @EnableGlobalMethodSecurity

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@SpringBootApplication
public class LearningSpringSecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(LearningSpringSecurityApplication.class, args);
    }

}

重點摘要

到目前為止,你可能會發現我們都還沒有談到循序圖中的 AuthenticationManager。先不談它是可以降低學習的門檻,因為身份驗證的方式百百種,看了太多的方式難勉眼花撩亂而壞了學習的興致。我們可以簡略地想成這樣:

public class FriendlyFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 運用 authenticationManager 完成身份驗證
        Authentication authentication = authenticationManager.authenticate(...);

        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    }
}

這段程式範例,它與先前直接 new ApiToken() 沒有差太多,只是改成了可變的,是由查詢而來的!而要去哪查詢就依不同的 AuthenticationProvider 實作來決定了,可以是 OAuth Provider 或是 JDBC 連線查詢,當然也可以是 JWT 解碼後的資料。有了這樣的認知後 Authentication 就不再是難以掌握的主題。

本篇完整範例請參考:

https://github.com/qrtt1/learning-spring-security/tree/lab2/src/main/java/twjug/lite/learningspringsecurity