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令牌,所以會自動解析通過驗證,最後再訪問客戶端的資源