說說如何使用 Spring Security 保護 web 請求

deniro_li發表於2020-10-01

利用 WebSecurityConfigurerAdapter 類的configure(HttpSecurity http) 方法,可以實現以下功能:

  • 只有滿足特定條件的請求,才允許提供服務;
  • 自定義登入頁;
  • 退出賬戶;
  • 預防跨站請求偽造。

1 許可權配置

對 HTTP 請求路徑進行許可權配置。假設必須具有 ROLE_USER 角色的賬戶才能訪問 /notice 與 /sms 路徑;而其它路徑無限制。具體配置程式碼如下:

 	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/notice", "/sms")
                .hasRole("USER")
                .antMatchers("/", "/**").permitAll();
    }

注意:

  1. 這裡順序很重要,因為先宣告的規則比後宣告的規則具有更高的優先順序。
  2. hasRole() 會自動為入參加上 “ROLE_” 字首,所以我們這裡只傳入 USER 即可。
方法名說明
access(String)如果給定的 SpEL 表示式計算結果為 true ,就允許訪問
anonymous()允許匿名賬戶訪問
authenticated()允許認證過的賬戶訪問
denyAll()拒絕所有訪問
fullyAuthenticated()如果賬戶是經過完整流程認證的,即非使用 “記住我”功能認證的,就允許訪問
hasAnyAuthority(String … )如果賬戶具有給定許可權列表中的某一個許可權,就允許訪問
hasAuthority(String)如果賬戶具有給定許可權,就允許訪問
hasAnyRole(String … )如果賬戶具有給定角色列表中的某一個角色,就允許訪問
hasRole(String)如果賬戶具有給定角色,就允許訪問
hasIpAddress(String)如果請求來自於某個給定的 IP 地址,就允許訪問
not()對前面的判定取反操作
permitAll()無限制訪問

上面這張表中的絕大多數方法都只能完成單一功能,如果需要的安全規則複雜,建議使用 access(String) 方法,這個方法的入參是 SpEL 表示式。Spring Security 還擴充套件了SpEL 表示式,具體說明如下。

安全表示式結果
authentication賬戶的認證物件
denyAll拒絕訪問,返回 false
hasAnyRole(roles)如果賬戶具有角色列表中任意角色,返回 true
hasRole(role)如果賬戶具有指定角色,返回 true
hasIpAddress( ip)如果請求來自於指定 IP ,返回為 true

Craig Walls 舉了這樣一個使用安全表示式示例:

 http.authorizeRequests()
                .antMatchers("/design", "/orders")
                .access("hasRole('USER') &&" +
                        "T(java.util.Calendar).getInstance().get(" +
                        "T(java.util.Calendar).DAY_OF_WEEK)==" +
                        "T(java.util.Calendar).TUESDAY");

access() 中的表示式含義是:只允許具有 ROLE_USER 許可權的賬戶在星期二訪問 /design 或 /orders 地址。

2 自定義登入頁

在configure(Http Securityhttp) 方法中,我們還可以自定義登入頁。

     http.authorizeRequests()
                .antMatchers("/notice", "/sms")
                .hasRole("USER")
                .antMatchers("/", "/**").permitAll()
                .and()
                .formLogin()
                .loginPage("/login");

這裡通過呼叫formLogin() 方法來實現,loginPage() 方法用於配置登入頁 URL 地址。利用and() 方法,我們可以把配置串聯起來。antMatchers() 方法下的每一段不同型別的配置,都可以通過 and() 進行串聯。

通過這樣配置之後,只要賬戶沒有通過認證,就會將地址重定向到該登入路徑。

因為登入頁只是一個檢視,所以我們可以很簡單地在實現了 WebMvcConfigurer 的 WebConfig 中將其宣告為一個檢視控制器。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        …
        registry.addViewController("/login");
    }
}

Spring Security會在指定的登入請求路徑下監聽登入請求,預設的使用者名稱和密碼名稱為 username 和 password。 使用者名稱和密碼引數名稱支援可配置。

        http.authorizeRequests()
                .antMatchers("/notice", "/sms")
                .hasRole("USER")
                .antMatchers("/", "/**").permitAll()
                //登入頁
                .and()
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/authenticate")
                .usernameParameter("user")
                .passwordParameter("pwd");

loginProcessingUrl() 方法用於指定登入請求路徑。usernameParameter() 與passwordParameter() 分別用於自定義使用者名稱和密碼引數名稱。

預設情況下,使用者登入成功之後,將會跳轉到根路徑("/”),一般我們會將其設定為登入後的主頁地址。這個預設的登入成功頁也可自定義。

http.authorizeRequests()
  	...
    .formLogin()
    .loginPage("/login")
    .loginProcessingUrl("/authenticate")
    .usernameParameter("user")
    .passwordParameter("pwd")
    //登入成功後跳轉頁
    .defaultSuccessUrl("/index");

這樣配置之後,只要使用者登入成功,就會跳轉到 “/index”。

defaultSuccessUrl() 方法還提供了一個可選入參(alwaysUse)。如果該引數設定為 True,那麼即使使用者在登入之前正在訪問其他頁面,在登入成功後,也會跳轉到指定頁面。

.defaultSuccessUrl("/index",true);

3 退出賬戶

退出賬戶也是一個常見功能,即使用者點選“登出”按鈕,退出應用。

在 HttpSecurity 物件上,連綴配置登出過濾器,該過濾器會攔截所有針對 “/logout” 的請求。

預設情況下,使用者會被重定向到登入頁,這樣他們就可以重新登入。我們也可以定製登出導航頁,而這是通過 logoutSuccessUrl() 方法來實現的。

http.authorizeRequests()
  	...
    .logout()
    .logoutSuccessUrl("/");

4 預防跨站請求偽造

跨站請求偽造(英語:Cross-site request forgery),也被稱為 one-click attack 或者 session riding,通常縮寫為 CSRF 或者 XSRF, 是一種挾制使用者在當前已登入的Web應用程式上執行非本意的操作的攻擊方法。跟跨網站指令碼(XSS)相比,XSS 利用的是使用者對指定網站的信任,CSRF 利用的是網站對使用者網頁瀏覽器的信任。

為了防止CSRF 攻擊,我們可以在表單渲染的時候生成一個 CSRF token,並將其放入表單隱藏域。提交表單時,會把表單資料與這個token一併傳送到服務端。服務端會首先攔截該請求,並與最初生成的 token 進行比對。如果 token 匹配成功,那麼請求將會被轉發到業務層進行後續處理;如果 token 匹配不成功,說明這個可能是惡意請求,服務端將不再轉發。

Spring Security 本身提供了 CSRF 保護,而且預設是啟用狀態。我們只需要在表單中加入一個名為“_csrf”的隱藏域即可,這個隱藏域用於儲存CSRF token。

比如在 Thymeleaf 模板中,可以這樣定義“_csrf” 隱藏域:


< input type=" hidden" name="_ csrf" th: value="${_ csrf. token}"/>

也可以關閉CSRF 保護,在HttpSecurity物件上呼叫 csrf() 方法開啟 CSRF 配置,然後呼叫disable() 方法關閉CSRF:


http.authorizeRequests()

 ...

 .csrf()

 .disable();

這一般只適用於開發環境,生產環境還是建議開啟 CSRF保護,保障應用安全。

5 獲取登入賬戶

有以下幾種常用的方式:

  1. 在 Controller 方法中注入 Principal 物件;
  2. 在 Controller 方法中注入 Authentication 物件;
  3. 在 Controller 方法的入參中使用 @AuthenticationPrincipal 註解;
  4. 使用 SecurityContextHolder。

(1)注入 Principal 物件

在 Controller 方法的入參中直接注入 Principal 物件,然後再通過Principal 物件內的賬戶名獲取 User 物件。

@PostMapping
public String process(@Valid Notice notice, Errors errors, Principal principal) {
    ...

    User user=userRepository.findByUsername(principal.getName());

   	...
}

這種方式雖然可行,但存在安全程式碼與業務程式碼雜糅的現象。

(2)注入 Authentication物件

在 Controller 方法的入參中直接注入 Authentication物件,然後直接獲取賬戶物件。

@PostMapping
public String process(@Valid Notice notice, Errors errors, Authentication authentication
) {
    ...
    User user= (User) authentication.getPrincipal();
   	...
}

getPrincipal() 方法返回的是 Object 物件,我們需要將其轉為 User 物件。

(3)使用 @AuthenticationPrincipal 註解

在 Controller 方法的賬戶物件入參中使用 @AuthenticationPrincipal 註解。

@PostMapping
public String process(@Valid Notice notice, Errors errors, @AuthenticationPrincipal User user
) {
    ...
   
}

@AuthenticationPrincipal 寫法不需要型別轉換,而且將與安全相關的程式碼限制在賬戶物件上,從而避免了前兩種方式所帶來的問題。另外這種用法也是目前最簡潔的寫法,因此推薦使用。

(4)SecurityContextHolder

可以從SecurityContextHolder中獲取一個 Authentication 物件,然後再獲取其中的 principal。

Authentication authentication= SecurityContextHolder.getContext().getAuthentication();
User user= (User) authentication.getPrincipal();

使用 SecurityContextHolder的好處是:它可以任何地方使用!我們一般會在底層程式碼程式設計中使用到它。

相關文章