把玩 Spring Security [1] 先讓一部分動起來

- notes spring-security

看到 Taiwan Backend Group 的朋友分享一個新的 Java 權限認證框架,就打趣地回了:

Spring Security 的吶喊:「你可以討厭我,但請不要討厭其他 Spring 專案」(阿醬語氣)

後端實作認證與權限管理是相當平常的需求,而再多學習一套非 Spring 系列的工具,多半是覺得無法理解 Spring Security 的用法,遭越了許多挫折而尋求新的捷徑。

Spring Security 是一個多數開發者都需要,但總是要一直 Google 查半天還不知道如何上手的 Spring 專案。不知是他的設計過於精巧,亦或他的封裝太過解耦之故。讓想要掌握他的開發者們總是不得其門而入。其實,只要懂弄了一次完整的 HTTP REQUEST 至 HTTP RESPONSE 之間的細節,就能大幅增加對 Spring Security 的理解。

無論你打怎麼驗證使用者或是 API Call 是否擁有權力使用服務,都是以上面這個循序圖的流程來運作的。儘管真實可靠的實作遠比這張圖複雜些,但只要抓住這張圖的精神,你就能掌握如何使用 Spring Security 囉! 作為把玩 Spring Security 系列的第一篇,我們先來玩玩簡單的:驗證。

實作情境

我們先以「極簡」的方式來練習 Spring Security:

我們要實作一種新的驗證方式,來決定 RESTful API 是不是能使用 /users 取得結果。讓我們先挑簡單的部分做 🤤 這個 SimpleController 正式我們要的 API,接著要把它加點「安全」的成份:

@RestController
public class SimpleController {

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

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

若你已經事先加好了 Spring Security 函式庫,那麼 Spring Security 會自動加上預設的 Security Filter,結果就是什麼東西都打不到:

$ curl http://127.0.0.1:8787/
{"timestamp":"2021-09-26T16:08:27.811+00:00","status":401,"error":"Unauthorized","path":"/"}

這時,就要用上我們啟用 Spring Security 的第一個類別 WebSecurityConfigurerAdapter,使用它的 DSL 去設定 Request Filter 的檢查規則:

@Component
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .anyRequest().permitAll();
    }
}

設定完全發現,情況相反了!剛剛是全都打不通,現在是全通了!

$ curl http://127.0.0.1:8787/users
users

這只要稍為修改一下 DSL 就行囉!多加一行 antMatchers 去捕抓 /users 相關 Pattern,令它必需要通過身份驗證:

http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/users**").authenticated()
                .anyRequest().permitAll();
$ curl http://127.0.0.1:8787/users
{"timestamp":"2021-09-26T16:19:04.881+00:00","status":403,"error":"Forbidden","path":"/users"}

製作新的 AuthenticationProvider

接著,我們要設法讓 /users 的 HTTP REQUEST 通過驗證。那麼,我們該怎麼做呢?什麼情況下,Spring Security 會覺得一個 HTTP REQUEST 是可以通過驗證的呢?請看下圖標示:

對於 Spring Security 會將 HTTP REQUEST 交給他有註冊的 Security Filter 處理,只要有任何一個 Security Filter 有設定 Authentication Object,那麼就是通過認證呦!思路就是這麼簡單。

假設,我們在研發一個嶄新的驗證方法,Spring Security 預設的方法都不適合我們,那就來做一個。這個新方法就是「不管你給什麼,我都讓你通過」的驗證方式:

這個友善的 Filter,它的重點只有一個,那就是拿到 SecurityContext 並給它 Authentication Object:

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 認為是通過驗證的:

public boolean isAuthenticated() {
    return true;
}

重點摘要

我們透過了簡單的 Restful API 體驗了 Spring Security 的驗證流程,學習到二個重點:

WebSecurityConfigurerAdapter 為設定 Spring Security 的輔助類別

對於 Spring Security 來說,只要 SecurityContext 含有標示為 已認證 的 Authentication Object 就會放行:

public boolean isAuthenticated() {
    return true;
}

下一篇,我們會繼續目前的範例,探索 Access Control 的使用方式。本篇完整範例請參考:

https://github.com/qrtt1/learning-spring-security/tree/lab1