在SpringBoot中使用SpringSecurity

小魚吃貓發表於2020-04-16

@

本教程是基於SpringMVC而建立的,不適用於WebFlux。(如果你不知道這兩者,可以忽略這句提示)

提出一個需求

所有的技術是為了解決實際問題而出現的,所以我們並不空談,也不去講那麼多的概念。在這樣一個系統中,有三個介面,需要授權給三種許可權的人使用,如下表:

介面地址 需要的許可權描述 可訪問的許可權組名稱
visitor/main 不需要許可權,也不用登入,誰都可以訪問
admin/main 必須登入,只有管理員可以訪問 ADMIN
user/main 必須登入,管理員和使用者許可權都能訪問 USER和ADMIN

解決方案:

  • 在Controller中判斷使用者是否登入和使用者的許可權組判斷是否可以訪問

    這是最不現實的解決方案,可是我剛進公司時的專案就是這樣設計的,當時我還覺得很高大尚呢。

  • 使用Web應用的三大元件中和過濾器(Filter)進行判斷

    這是正解,SpringSecurity也正是用的這個原理。如果你的專案足夠簡單,建議你直接使用這種方式就可以了,並不需要整合SpringSecurity。這部分的示例在程式碼中有演示,自己下載程式碼檢視即可。

  • 我們可以直接使用SpringSecurity框架來解決這個問題

使用SpringSecurity進行解決

​ 網上的教程那麼多,但是講的都不清不楚。所以,請仔細閱讀下段這些話,這要比後邊的程式碼重要。

​ SpringSecurity主要有兩部分內容:

  • 認證 (你是誰,說白了就是一個使用者登入的功能,幫我們驗證使用者名稱和密碼)
  • 授權 (你能幹什麼,就是根據當前登入使用者的許可權,說明你能訪問哪些介面,哪些不能訪問。)

這裡的登入是對於瀏覽器訪問來說的,因為如果是前後端分離時,使用的是Token進行授權的,也可以理解為登入使用者,這個後邊會講。這裡只是為了知識的嚴謹性才提到了這點

SpringSecurity和SpringBoot結合

1. 首先在pom.xml中引入依賴:

<!-- 不用寫版本,繼承Springboot的版本-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. 配置使用者角色和介面的許可權關係

是支援使用xml進行配置的,但是在SpringBoot中更建議使用Java註解配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 配置使用者許可權組和介面路徑的關係
     * 和一些其他配置
     *
     * @param http
     * @throws Exception
     */
    @Override
     protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()     // 對請求進行驗證
                .antMatchers("/visitor/**").permitAll()
                .antMatchers("/admin/**").hasRole("ROLE_ADMIN")     // 必須有ADMIN許可權
                .antMatchers("/user/**").hasAnyRole("ROLE_USER", "ROLE_ADMIN")       //有任意一種許可權
                .anyRequest()     //任意請求(這裡主要指方法)
                .authenticated()   //// 需要身份認證
                .and()   //表示一個配置的結束
                .formLogin().permitAll()  //開啟SpringSecurity內建的表單登入,會提供一個/login介面
                .and()
                .logout().permitAll()  //開啟SpringSecurity內建的退出登入,會為我們提供一個/logout介面
                .and()
                .csrf().disable();    //關閉csrf跨站偽造請求
    }

}

上邊的配置主要內容有兩個:

  1. 配置訪問三個介面(實際上不僅僅是3個,/**是泛指)需要的許可權;
  2. 配置了使用SpringSecurity的內建/login和/loginout介面(這個是完全可以自定義的)
  3. 許可權被拒絕後的返回結果也可以自定義,它當許可權被拒絕後,會丟擲異常

說明:

  1. 上邊的配置中,其實就是呼叫http的這個物件的方法;
  2. 使用.and()只為了表示一上配置結束,並滿足鏈式呼叫的要求,不然之前的物件可能並不能進行鏈式呼叫
  3. 這個配置在SpringBoot應用啟動的時候就會呼叫,也就是會將這些配置載入進記憶體,當使用者呼叫對應的介面的時候,就會判斷它的角色是否可以呼叫這個介面,流程圖如下(我覺得圖要比文字更能說明過程):

3. 配置使用者名稱和密碼

​ 配置了上邊的介面和使用者許可權角色的關係後,就是要配置我們的使用者名稱和密碼了。如果沒有正確的使用者名稱和密碼,神仙也登入不上去。

​ 關於這個,網上的教程有各種各樣的配置,其實就一個介面,我們只需要實現這個介面中的方法就可以了。介面程式碼如下:

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
  	/**
  	 * 在登入的時候,就會呼叫這個方法,它的返回結果是一個UserDetails介面類
  	 */
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

​ 來看一下這個介面,如果想擴充套件,可以自己寫一個實現類,也可以使用SpringSecurity提供的實現

public interface UserDetails extends Serializable {
  	// 使用者授權集合
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

​ UserDetailsServicer介面的實現類

@Configuration
public class UserDetailsServiceImpl implements UserDetailsService {
    /**
     * 這個方法要返回一個UserDetails物件
     * 其中包括使用者名稱,密碼,授權資訊等
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /**
         * 將我們的登入邏輯寫在這裡
         * 我是直接在這裡寫的死程式碼,其實應該從資料庫中根據使用者名稱去查
         */
        if (username == null) {
            //返回null時,後邊就會丟擲異常,就會登入失敗。但這個異常並不需要我們處理
            return null;
        }
        if (username.equals("lyn4ever")) {
            //這是構造使用者許可權組的程式碼
            //但是這個許可權上加了ROLE_字首,而在之前的配置上卻沒有加。
            //與其說這不好理解,倒不如說這是他設計上的一個小缺陷
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
            List<SimpleGrantedAuthority> list = new ArrayList<>();
            list.add(authority);
            //這個user是UserDetails的一個實現類
            //使用者密碼實際是lyn4ever,前邊加{noop}是不讓SpringSecurity對密碼進行加密,使用明文和輸入的登入密碼比較
            //如果不寫{noop},它就會將表表單密碼進行加密,然後和這個對比
            User user = new User("lyn4ever", "{noop}lyn4ever", list);
            return user;
        }
        if (username.equals("admin")) {
            SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
            SimpleGrantedAuthority authority1 = new SimpleGrantedAuthority("ROLE_ADMIN");
            List<SimpleGrantedAuthority> list = new ArrayList<>();
            list.add(authority);
            list.add(authority1);
            User user = new User("admiin", "{noop}admin", list);
            return user;
        }

        //其他返回null
        return null;
    }
}

4.進行測試

​ 分別訪問上邊三個介面,可以看到訪問結果和上邊的流程是一樣的。

總結:

  • 仔細閱讀上邊的那個流程圖,是理解SpringSecurity最重要的內容,程式碼啥的都很簡單;上邊也就兩個類,一個配置介面與角色的關係,一個實現了UserDetailsService類中的方法。
  • 前邊說了,SpringSecurity主要就是兩個邏輯:
    • 使用者登入後,將使用者的角色資訊儲存在伺服器(session中);
    • 使用者訪問介面後,從session中取出使用者資訊,然後和配置的角色和許可權進行比對是否有這個許可權訪問
  • 上述方法中,我們只重寫了使用者登入時的邏輯。而根據訪問介面來判斷當前使用者是否擁有這個介面的訪問許可權部分,我們並沒有進行修改。所以這隻適用於可以使用session的專案中。
  • 對於前後端分離的專案,一般是利用JWT進行授權的,所以它的主要內容就在判斷token中的資訊是否有訪問這個介面的許可權,而並不在使用者登入這一部分。
  • 解決訪問的方案有很多種,選擇自己最適合自己的才是最好了。SpringSecurity只是提供了一系列的介面,他自己內部也有一些實現,你也可以直接使用。
  • 上邊配置和使用者登入邏輯部分的內容是完全可以從資料庫中查詢出來進行配置的。

程式碼地址

相關文章