SpringSecurity總結

基礎

核心

認證與授權

 

與Shiro聯繫

SpringSecurity 在 SpringBoot 出現前因為配置複雜使用較少,但是在SpringBoot 出現後搭配使用開發效率大大提高。是一款重量級框架。而 Shiro 是一款輕量級框架,配置簡單一些,所以如果不使用 SpringBoot,那麼一般搭配 Shiro,而使用SpringBoot 就搭配 SpringSecurity。

 

核心接口

UserDetailsService

定義了SpringSecurity 查詢用戶信息的接口方法,在SpringSecurity 認證時,並不是直接通過用戶名密碼去數據庫比對,沒有對應就返回,而是先通過 username 去數據庫查到對應的用戶信息,然後進行拼接成 SpringSecurity 內部維護的用戶對象,然後由內部方法進行密碼比對。而查詢數據庫返回用戶對象的接口方法就是由 UserDetailsService 接口定義的。

 

UserDetails  

上面說到數據庫查詢用戶信息會返回一個SpringSecurity 內部維護的用戶對象。這個用戶抽象類就是 UserDetails,其內部結構如下

public interface UserDetails extends Serializable {
    // ~ Methods
    // ========================================================================================================

    /**
     * Returns the authorities granted to the user. Cannot return <code>null</code>.
     * 授權列表
     * @return the authorities, sorted by natural key (never <code>null</code>)
     */
    Collection<? extends GrantedAuthority> getAuthorities();

    /**
     * Returns the password used to authenticate the user.
     *
     * @return the password
     */
    String getPassword();

    /**
     * Returns the username used to authenticate the user. Cannot return <code>null</code>.
     *
     * @return the username (never <code>null</code>)
     */
    String getUsername();

    /**
     * Indicates whether the user's account has expired. An expired account cannot be
     * authenticated.
     * 是否過期
     * @return <code>true</code> if the user's account is valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    boolean isAccountNonExpired();

    /**
     * Indicates whether the user is locked or unlocked. A locked user cannot be
     * authenticated.
     * 是否鎖定,如果鎖定就無法驗證
     * @return <code>true</code> if the user is not locked, <code>false</code> otherwise
     */
    boolean isAccountNonLocked();

    /**
     * Indicates whether the user's credentials (password) has expired. Expired
     * credentials prevent authentication.
     * 用戶憑證是否過期,過期的憑證會阻止身份驗證
     * @return <code>true</code> if the user's credentials are valid (ie non-expired),
     * <code>false</code> if no longer valid (ie expired)
     */
    boolean isCredentialsNonExpired();

    /**
     * Indicates whether the user is enabled or disabled. A disabled user cannot be
     * authenticated.
     * 用戶是啟用還是禁用,無法對禁用的用戶進行身份驗證
     * @return <code>true</code> if the user is enabled, <code>false</code> otherwise
     */
    boolean isEnabled();
}

在使用時可以讓自定義用戶來實現這個接口。

 

PasswordEncoder

密碼接口,一般使用 BCryptPasswordEncoder 來作為默認的密碼轉換器。SpringSecurity 在加密時引入鹽,使得加密過程是不可逆的,而加密後的字符串包含鹽信息,在比較方法中會對加密後的密碼進行解析,解析出鹽值,然後對輸入密碼進行加密,比較輸入密碼加密後的結果是否與原密碼加密後的結果一致。使用 encode 方法進行加密, matches 方法進行密碼比較。如果一致返回 true。

 

常用配置

用戶名密碼配置

方式一、配置文件

方式二、配置類

 方式三、自定義配置

 因為一般項目用戶名密碼都是存在數據庫的,所以這是最主流的。

1、配置UserDetails,返回用戶信息

2、添加配置類,將userDetails註冊進 SpringSecurity

 

記住我

原理

在登陸後會向數據庫的 persistent_logins 表中插入一條記錄,表結構如下

series 是主鍵, 隨後將 series 和 token 進行算法轉換成字符串發給客戶端,後面客戶端會攜帶 Cookie ,當下次訪問時後端會解析 Cookie ,解析成 series 和 token ,然後去表中匹配,驗證token是否一致,以及 last_used + 存活時間是否到期,如果都滿足就再以 name 走 UserDetailsService 的方法,返回用戶信息。

 

配置

建表語句:

DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) NOT NULL,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
   @Resource
    private DataSource datasource;

    /**
     * 注入記住我token表的數據源
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(datasource);
//        jdbcTokenRepository.setCreateTableOnStartup(true);      // 是否自動創建token數據表,如果是第一次可以勾選,後面表存在還開啟就會報錯
        return jdbcTokenRepository;
    }

可以使用在配置方法中添加 “.rememberMeParameter(“rem”) ” 配置記住我功能的name

注意:

1、這裡的過期時間是拒上次打開瀏覽器登陸開始計算的,也就是每次打開瀏覽器訪問一次上次登陸時間都會刷新一次。而瀏覽器內部訪問並不會刷新時間。

2、退出後(退出登陸狀態)會清除數據庫的token數據,再次訪問需要重新登錄

 

登陸成功處理器

步驟一:增加組件

方式一、繼承實現類

方式二、實現底層接口

 步驟二:將組件註冊進成功處理器配置中

 

登陸失敗處理器

步驟一:增加組件

方式一、實現接口

 方式二、繼承實現類

 步驟二:將組件註冊進失敗處理器配置中

 

權限認證失敗處理器

1、組件

2、配置

 

用戶退出處理器

1、組件

 

 2、配置

 

 

角色權限

訪問一個需要權限或角色的頁面需要先登陸,如果登陸後還是不能訪問就會返回500.

角色、權限、用戶關係

權限與角色是多對多,角色與用戶也是多對多。權限指的是對某個表具體的增刪改查權限,而角色是一系列權限的集合。比如管理員角色擁有對所有表增刪改查的權限,普通用戶角色只擁有對所有表查詢的權限,而用戶 admin 擁有管理員角色,用戶 A 擁有普通用戶的角色。

定義權限

在config里配置路徑所需權限,在UserDetailsService里配置用戶所擁有的權限。

1、hasAuthority 是與關係,如果在config里配置了多個權限,如」admin,manager」,那麼在UserDetailsService也必須對用戶配置兩個角色權限才可以訪問

2、hasAnyAuthority 是或關係,如果在 config里配置了多個權限,如」admin,manager」,那麼在UserDetailsService只需要對用戶配置一個權限就可以訪問

 

定義角色

 

角色在 UserDetailsService 實現類中配置需要加 “ROLE_” 前綴

而hasRole 和hasAnyRole 對應權限里的hasAuthority 和hasAnyAuthority,是與和或的關係。

 

Access 來定義權限、角色

上面的hasRole、hasAuthority 底層都是使用 access 來實現的,所以我們還可以通過底層的access 方法來主直接定義權限、角色。

那麼 config 里配置就是如下:

 

自定義 Access 校驗規則

1、組件

2、配置

 

基於 IP 來限制

這樣的話只能接收來自 127.0.0.1 的請求。

 

定義角色註解

@Secured單個」」里不支持使用,隔開,也就是不支持與關係。如果要配置多個或關係,可以使用{}, 在UserDetailsService里只要配置一個就可以訪問。

並且只支持定義角色,不支持定義權限,也就是Secured里必須是ROLE_開頭

 

定義角色、權限註解

可以定義角色、也可以定義權限

如果用戶擁有的角色是abc,那麼在這裡可以配置hasRole(『abc』),也可以配置hasRole(『ROLE_abc』),而使用config配置類配置則不可以,會報錯。而大小寫則和配置類一樣會區分

 

先執行後校驗註解

可以用於記錄訪問日誌

 

對返回和傳入數據過濾註解

 

 

CSRF

CSRF 是為了防止用戶在開啟記住登陸後,其他非法用戶截取到登陸用戶的 Cookie ,登陸其他用戶進行非法操作。

默認是開啟的,開啟後用戶登錄時,系統發放一個CsrfToken值(key是 _csrf,value是token值),用戶攜帶該CsrfToken值與用戶名、密碼等參數完成登錄。系統記錄該會話的 CsrfToken 值,之後在用戶的任何請求中,都必須帶上該CsrfToken值,並由系統進行校驗。

配置:

相關依賴:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--對Thymeleaf添加Spring Security標籤支持-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

開啟 CSRF 時配置類不能配置 loginProcessingUrl 和 defaultSuccessUrl 。會影響登陸跳轉邏輯。

 

其他配置

1、如果配置了登陸的URL(也就是loginProcessingUrl),那麼自定義Controller里處理的登陸請求就會用不到,走的是SpringSecurity內部的驗證方法。

2、anyRequest()必須配置在所有的antMatches後面,也就是籠統的權限配置必須放在其他權限的最後

3、and()是用於連接多個http配置。     

4、在開發時需要添加@EnableWebSecurity註解,這個註解會自動配置安全認證策略和認證信息。

 

整合OAuth2

關於 OAuth2 與 JWT 可以移步 淺談常見的認證機制 。

基礎依賴

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency> 

 

基礎配置

因為 OAuth2 涉及到資源服務器和授權服務器,所以除了配置 SpringSecurity ,還需要配置資源服務器和授權服務器。

1、SpringSecurity配置:

2、授權服務器配置:定義 app_id、app_secret,以及重定向地址,授權範圍等

3、資源服務器配置:定義資源服務器資源權限角色配置。

 4、其他:userDetailsService 配置

自定義用戶實體類 user ,權限屬性全部設為 true。

資源服務器的資源Controller

 

授權碼模式

在上面的授權服務器配置中,已將授權類型設為 授權碼模式,所以直接使用上面的配置。

驗證

1、獲取授權碼

訪問 //localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=//www.baidu.com&scope=all ,在登陸成功後(因為走的是上面 userDetailsService 的方法,所以用戶名任意,密碼123456就通過登陸),會重定向授權服務器配置中配置好的 //www.baidu.com 。並且攜帶授權服務器返回的授權碼 code。

2、獲取授權令牌

接下來就可以再次訪問 localhost:8080/oauth/token 攜帶授權碼及其他數據來向授權服務器獲取授權令牌。

 3、通過令牌訪問資源服務器的資源,訪問資源服務器上資源的 URL,並攜帶授權令牌。

 

密碼模式

密碼模式因為是通過密碼直接獲取授權令牌,所以不需要先獲取授權碼,同時需要設置自定義的 userDetailsService 實現類,以及 authenticationManager 組件

1、ServurityConfig里增加配置:

 2、授權服務器增加配置:

這樣配置是同時支持授權碼模式與密碼模式

 

驗證 

通過密碼獲取授權令牌

訪問資源服務器的資源則和授權碼模式驗證一樣。

 

整合 redis 將令牌存入 redis

1、引入依賴

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>    

2、註冊 redis 的 tokenStore 組件進容器

3、在授權服務器里註冊 tokenStore

4、在配置文件里配置 redis 地址密碼等。

 

使用 JWT 作為令牌

1、增加依賴

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>    

2、從容器中移除 redis 的 tokenStore 組件,同時想容器中加入 jwt 的 tokenStore 組件,並且配置 jwt 的轉換器

3、註冊進授權服務器

 

JWT 增加額外信息

1、增加 Jwt 附加信息組件並註冊進容器

2、在授權服務器里配置 jwt 附加信息組件

3、驗證,修改資源服務器資源返回的信息 

 

設置過期時間和刷新令牌

在授權服務器里增加配置:

 

在60s後token令牌(access_token)失效後,可以使用刷新令牌重新獲取新的令牌,新的令牌過期時間也是60s。

因為密碼模式不支持刷新令牌,所以通過授權碼模式使用刷新令牌來獲取新的令牌

通過刷新令牌獲取令牌

 

整合SSO(單點登陸)

1、引入依賴

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

 

2、新建一個模塊,配置 SSO 訪問授權服務器的地址

3、增加 SSO 模塊的資源

4、主程序開啟 OAuth2 自動配置

5、在授權服務器的配置增加配置

隨後訪問客戶端資源 //localhost:8081/user/getCurrentUser 就會先跳轉到 //localhost:8080/login ,也就是授權服務器進行授權驗證,通過後經重定向回到 //localhost:8081/login ,也就是客戶端的登陸頁面,並且攜帶授權服務器提供的jwt令牌,所以會自動解析通過驗證,最後再訪問客戶端的資源