SpringSecurity實(shí)現(xiàn)權(quán)限認(rèn)證與授權(quán)的使用示例
一、Spring Security介紹
1、Spring Security簡介
Spring 是非常流行和成功的 Java 應(yīng)用開發(fā)框架,Spring Security 正是 Spring 家族中的成員。Spring Security 基于 Spring 框架,提供了一套 Web 應(yīng)用安全性的完整解決方案。
正如你可能知道的關(guān)于安全方面的兩個(gè)核心功能是認(rèn)證和授權(quán),一般來說,Web 應(yīng)用的安全性包括**用戶認(rèn)證(Authentication)和用戶授權(quán)(Authorization)**兩個(gè)部分,這兩點(diǎn)也是 SpringSecurity 重要核心功能。
(1)用戶認(rèn)證指的是:驗(yàn)證某個(gè)用戶是否為系統(tǒng)中的合法主體,也就是說用戶能否訪問該系統(tǒng)。用戶認(rèn)證一般要求用戶提供用戶名和密碼,系統(tǒng)通過校驗(yàn)用戶名和密碼來完成認(rèn)證過程。
通俗點(diǎn)說就是系統(tǒng)認(rèn)為用戶是否能登錄
(2)用戶授權(quán)指的是驗(yàn)證某個(gè)用戶是否有權(quán)限執(zhí)行某個(gè)操作。在一個(gè)系統(tǒng)中,不同用戶所具有的權(quán)限是不同的。比如對(duì)一個(gè)文件來說,有的用戶只能進(jìn)行讀取,而有的用戶可以進(jìn)行修改。一般來說,系統(tǒng)會(huì)為不同的用戶分配不同的角色,而每個(gè)角色則對(duì)應(yīng)一系列的權(quán)限。
通俗點(diǎn)講就是系統(tǒng)判斷用戶是否有權(quán)限去做某些事情。
2、歷史
“Spring Security 開始于 2003 年年底,““Spring 的 acegi 安全系統(tǒng)”。 起因是 Spring開發(fā)者郵件列表中的一個(gè)問題,有人提問是否考慮提供一個(gè)基于 Spring 的安全實(shí)現(xiàn)。
Spring Security 以“The Acegi Secutity System for Spring” 的名字始于 2013 年晚些時(shí)候。一個(gè)問題提交到 Spring 開發(fā)者的郵件列表,詢問是否已經(jīng)有考慮一個(gè)機(jī)遇 Spring 的安全性社區(qū)實(shí)現(xiàn)。那時(shí)候 Spring 的社區(qū)相對(duì)較?。ㄏ鄬?duì)現(xiàn)在)。實(shí)際上 Spring 自己在2013 年只是一個(gè)存在于 ScourseForge 的項(xiàng)目,這個(gè)問題的回答是一個(gè)值得研究的領(lǐng)域,雖然目前時(shí)間的缺乏組織了我們對(duì)它的探索。
考慮到這一點(diǎn),一個(gè)簡單的安全實(shí)現(xiàn)建成但是并沒有發(fā)布。幾周后,Spring 社區(qū)的其他成員詢問了安全性,這次這個(gè)代碼被發(fā)送給他們。其他幾個(gè)請(qǐng)求也跟隨而來。到 2014 年一月大約有 20 萬人使用了這個(gè)代碼。這些創(chuàng)業(yè)者的人提出一個(gè) SourceForge 項(xiàng)目加入是為了,這是在 2004 三月正式成立。
在早些時(shí)候,這個(gè)項(xiàng)目沒有任何自己的驗(yàn)證模塊,身份驗(yàn)證過程依賴于容器管理的安全性和 Acegi 安全性。而不是專注于授權(quán)。開始的時(shí)候這很適合,但是越來越多的用戶請(qǐng)求額外的容器支持。容器特定的認(rèn)證領(lǐng)域接口的基本限制變得清晰。還有一個(gè)相關(guān)的問題增加新的容器的路徑,這是最終用戶的困惑和錯(cuò)誤配置的常見問題。
Acegi 安全特定的認(rèn)證服務(wù)介紹。大約一年后,Acegi 安全正式成為了 Spring 框架的子項(xiàng)目。1.0.0 最終版本是出版于 2006 -在超過兩年半的大量生產(chǎn)的軟件項(xiàng)目和數(shù)以百計(jì)的改進(jìn)和積極利用社區(qū)的貢獻(xiàn)。
Acegi 安全 2007 年底正式成為了 Spring 組合項(xiàng)目,更名為"Spring Security"。
3、同款產(chǎn)品對(duì)比
3.1、Spring Security
Spring 技術(shù)棧的組成部分。
鏈接: SpringSecurity官網(wǎng)
通過提供完整可擴(kuò)展的認(rèn)證和授權(quán)支持保護(hù)你的應(yīng)用程序。
SpringSecurity 特點(diǎn):
? 和 Spring 無縫整合。
? 全面的權(quán)限控制。
? 專門為 Web 開發(fā)而設(shè)計(jì)。
- 舊版本不能脫離 Web 環(huán)境使用。
- ? 新版本對(duì)整個(gè)框架進(jìn)行了分層抽取,分成了核心模塊和 Web 模塊。單獨(dú)引入核心模塊就可以脫離 Web 環(huán)境。
? 重量級(jí)。
3.2、 Shiro
Apache 旗下的輕量級(jí)權(quán)限控制框架。
特點(diǎn):
? 輕量級(jí)。Shiro 主張的理念是把復(fù)雜的事情變簡單。針對(duì)對(duì)性能有更高要求的互聯(lián)網(wǎng)應(yīng)用有更好表現(xiàn)。
? 通用性。
- 好處:不局限于 Web 環(huán)境,可以脫離 Web 環(huán)境使用。
- 缺陷:在 Web 環(huán)境下一些特定的需求需要手動(dòng)編寫代碼定制。
Spring Security 是 Spring 家族中的一個(gè)安全管理框架,實(shí)際上,在 Spring Boot 出現(xiàn)之前,Spring Security 就已經(jīng)發(fā)展了多年了,但是使用的并不多,安全管理這個(gè)領(lǐng)域,一直是 Shiro 的天下。
相對(duì)于 Shiro,在 SSM 中整合 Spring Security 都是比較麻煩的操作,所以,Spring Security 雖然功能比 Shiro 強(qiáng)大,但是使用反而沒有 Shiro 多(Shiro 雖然功能沒有Spring Security 多,但是對(duì)于大部分項(xiàng)目而言,Shiro 也夠用了)。
自從有了 Spring Boot 之后,Spring Boot 對(duì)于 Spring Security 提供了自動(dòng)化配置方案,可以使用更少的配置來使用 Spring Security。
因此,一般來說,常見的安全管理技術(shù)棧的組合是這樣的:
- SSM + Shiro
- Spring Boot/Spring Cloud + Spring Security
以上只是一個(gè)推薦的組合而已,如果單純從技術(shù)上來說,無論怎么組合,都是可以運(yùn)行的
二、Spring Security實(shí)現(xiàn)權(quán)限
要對(duì)Web資源進(jìn)行保護(hù),最好的辦法莫過于Filter
要想對(duì)方法調(diào)用進(jìn)行保護(hù),最好的辦法莫過于[AOP]
Spring Security進(jìn)行認(rèn)證和鑒權(quán)的時(shí)候,就是利用的一系列的Filter來進(jìn)行攔截的。
如圖所示,一個(gè)請(qǐng)求想要訪問到API就會(huì)從左到右經(jīng)過藍(lán)線框里的過濾器,其中綠色部分是負(fù)責(zé)認(rèn)證的過濾器,藍(lán)色部分是負(fù)責(zé)異常處理,橙色部分則是負(fù)責(zé)授權(quán)。進(jìn)過一系列攔截最終訪問到我們的API。
這里面我們只需要重點(diǎn)關(guān)注兩個(gè)過濾器即可:UsernamePasswordAuthenticationFilter
負(fù)責(zé)登錄認(rèn)證,FilterSecurityInterceptor
負(fù)責(zé)權(quán)限授權(quán)。
說明:Spring Security的核心邏輯全在這一套過濾器中,過濾器里會(huì)調(diào)用各種組件完成功能,掌握了這些過濾器和組件你就掌握了Spring Security!這個(gè)框架的使用方式就是對(duì)這些過濾器和組件進(jìn)行擴(kuò)展。
1、SpringSecurity入門
1.1 添加依賴
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
說明:依賴包(spring-boot-starter-security)導(dǎo)入后,Spring Security就默認(rèn)提供了許多功能將整個(gè)應(yīng)用給保護(hù)了起來:
? 1、要求經(jīng)過身份驗(yàn)證的用戶才能與應(yīng)用程序進(jìn)行交互
? 2、創(chuàng)建好了默認(rèn)登錄表單
? 3、生成用戶名為user
的隨機(jī)密碼并打印在控制臺(tái)上
? 4、CSRF
攻擊防護(hù)、Session Fixation
攻擊防護(hù)
? 5、等等等等…
1.2、啟動(dòng)項(xiàng)目測試
在瀏覽器訪問:http://localhost:8800/doc.html
自動(dòng)跳轉(zhuǎn)到了登錄頁面
默認(rèn)的用戶名:user
密碼在項(xiàng)目啟動(dòng)的時(shí)候在控制臺(tái)會(huì)打印,注意每次啟動(dòng)的時(shí)候密碼都會(huì)發(fā)生變化!
輸入用戶名,密碼,成功訪問到controller方法并返回?cái)?shù)據(jù),說明Spring Security默認(rèn)安全保護(hù)生效。
在實(shí)際開發(fā)中,這些默認(rèn)的配置是不能滿足我們需要的,我們需要擴(kuò)展Spring Security組件,完成自定義配置,實(shí)現(xiàn)我們的項(xiàng)目需求。
2、用戶認(rèn)證
用戶認(rèn)證流程:
概念速查:
**Authentication
**接口: 它的實(shí)現(xiàn)類,表示當(dāng)前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
**AuthenticationManager
**接口:定義了認(rèn)證Authentication的方法
**UserDetailsService
**接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個(gè)根據(jù)用戶名查詢用戶信息的方法。
**UserDetails
**接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對(duì)象返回。然后將這些信息封裝到Authentication對(duì)象中。
2.1、用戶認(rèn)證核心組件
我們系統(tǒng)中會(huì)有許多用戶,確認(rèn)當(dāng)前是哪個(gè)用戶正在使用我們系統(tǒng)就是登錄認(rèn)證的最終目的。這里我們就提取出了一個(gè)核心概念:當(dāng)前登錄用戶/當(dāng)前認(rèn)證用戶。整個(gè)系統(tǒng)安全都是圍繞當(dāng)前登錄用戶展開的,這個(gè)不難理解,要是當(dāng)前登錄用戶都不能確認(rèn)了,那A下了一個(gè)訂單,下到了B的賬戶上這不就亂套了。這一概念在Spring Security中的體現(xiàn)就是 Authentication
,它存儲(chǔ)了認(rèn)證信息,代表當(dāng)前登錄用戶。
我們?cè)诔绦蛑腥绾潍@取并使用它呢?我們需要通過 SecurityContext
來獲取Authentication
,SecurityContext
就是我們的上下文對(duì)象!這個(gè)上下文對(duì)象則是交由 SecurityContextHolder
進(jìn)行管理,你可以在程序任何地方使用它:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SecurityContextHolder
原理非常簡單,就是使用ThreadLocal
來保證一個(gè)線程中傳遞同一個(gè)對(duì)象!
現(xiàn)在我們已經(jīng)知道了Spring Security中三個(gè)核心組件:
? 1、Authentication
:存儲(chǔ)了認(rèn)證信息,代表當(dāng)前登錄用戶
? 2、SeucirtyContext
:上下文對(duì)象,用來獲取Authentication
? 3、SecurityContextHolder
:上下文管理對(duì)象,用來在程序任何地方獲取SecurityContext
Authentication
中是什么信息呢:
? 1、Principal
:用戶信息,沒有認(rèn)證時(shí)一般是用戶名,認(rèn)證后一般是用戶對(duì)象
? 2、Credentials
:用戶憑證,一般是密碼
? 3、Authorities
:用戶權(quán)限
2.2、用戶認(rèn)證
Spring Security是怎么進(jìn)行用戶認(rèn)證的呢?
AuthenticationManager
就是Spring Security用于執(zhí)行身份驗(yàn)證的組件,只需要調(diào)用它的authenticate
方法即可完成認(rèn)證。Spring Security默認(rèn)的認(rèn)證方式就是在UsernamePasswordAuthenticationFilter
這個(gè)過濾器中進(jìn)行認(rèn)證的,該過濾器負(fù)責(zé)認(rèn)證邏輯。
Spring Security用戶認(rèn)證關(guān)鍵代碼如下:
// 生成一個(gè)包含賬號(hào)密碼的認(rèn)證信息 Authentication authenticationToken = new UsernamePasswordAuthenticationToken(username, passwrod); // AuthenticationManager校驗(yàn)這個(gè)認(rèn)證信息,返回一個(gè)已認(rèn)證的Authentication Authentication authentication = authenticationManager.authenticate(authenticationToken); // 將返回的Authentication存到上下文中 SecurityContextHolder.getContext().setAuthentication(authentication);
下面我們來分析一下。
2.2.1、認(rèn)證接口分析
AuthenticationManager
的校驗(yàn)邏輯非常簡單:
根據(jù)用戶名先查詢出用戶對(duì)象(沒有查到則拋出異常)將用戶對(duì)象的密碼和傳遞過來的密碼進(jìn)行校驗(yàn),密碼不匹配則拋出異常。
這個(gè)邏輯沒啥好說的,再簡單不過了。重點(diǎn)是這里每一個(gè)步驟Spring Security都提供了組件:
? 1、是誰執(zhí)行 根據(jù)用戶名查詢出用戶對(duì)象 邏輯的呢?用戶對(duì)象數(shù)據(jù)可以存在內(nèi)存中、文件中、數(shù)據(jù)庫中,你得確定好怎么查才行。這一部分就是交由**UserDetialsService
** 處理,該接口只有一個(gè)方法loadUserByUsername(String username)
,通過用戶名查詢用戶對(duì)象,默認(rèn)實(shí)現(xiàn)是在內(nèi)存中查詢。
? 2、那查詢出來的 用戶對(duì)象 又是什么呢?每個(gè)系統(tǒng)中的用戶對(duì)象數(shù)據(jù)都不盡相同,咱們需要確認(rèn)我們的用戶數(shù)據(jù)是啥樣的才行。Spring Security中的用戶數(shù)據(jù)則是由**UserDetails
** 來體現(xiàn),該接口中提供了賬號(hào)、密碼等通用屬性。
? 3、對(duì)密碼進(jìn)行校驗(yàn)大家可能會(huì)覺得比較簡單,if、else
搞定,就沒必要用什么組件了吧?但框架畢竟是框架考慮的比較周全,除了if、else
外還解決了密碼加密的問題,這個(gè)組件就是**PasswordEncoder
**,負(fù)責(zé)密碼加密與校驗(yàn)。
我們可以看下AuthenticationManager
校驗(yàn)邏輯的大概源碼:
我們可以看下AuthenticationManager
校驗(yàn)邏輯的大概源碼:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { ...省略其他代碼 // 傳遞過來的用戶名 String username = authentication.getName(); // 調(diào)用UserDetailService的方法,通過用戶名查詢出用戶對(duì)象UserDetail(查詢不出來UserDetailService則會(huì)拋出異常) UserDetails userDetails = this.getUserDetailsService().loadUserByUsername(username); String presentedPassword = authentication.getCredentials().toString(); // 傳遞過來的密碼 String password = authentication.getCredentials().toString(); // 使用密碼解析器PasswordEncoder傳遞過來的密碼是否和真實(shí)的用戶密碼匹配 if (!passwordEncoder.matches(password, userDetails.getPassword())) { // 密碼錯(cuò)誤則拋出異常 throw new BadCredentialsException("錯(cuò)誤信息..."); } // 注意哦,這里返回的已認(rèn)證Authentication,是將整個(gè)UserDetails放進(jìn)去充當(dāng)Principal UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userDetails, authentication.getCredentials(), userDetails.getAuthorities()); return result; ...省略其他代碼 }
UserDetialsService
、UserDetails
、PasswordEncoder
,這三個(gè)組件Spring Security都有默認(rèn)實(shí)現(xiàn),這一般是滿足不了我們的實(shí)際需求的,所以這里我們自己來實(shí)現(xiàn)這些組件!
2.2.2、加密器PasswordEncoder
采取MD5加密
自定義加密處理組件:CustomMd5PasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.util.DigestUtils; import java.util.Arrays; /** * 自定義security密碼校驗(yàn) * @author 尹穩(wěn)健~ * @version 1.0 * @time 2023/1/31 */ public class CustomMd5PasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { // 進(jìn)行一個(gè)md5加密 return Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes())); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { // 通過md5校驗(yàn) return encodedPassword.equals(Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes()))); } }
2.2.3、用戶對(duì)象UserDetails
該接口就是我們所說的用戶對(duì)象,它提供了用戶的一些通用屬性,源碼如下:
public interface UserDetails extends Serializable { /** * 用戶權(quán)限集合(這個(gè)權(quán)限對(duì)象現(xiàn)在不管它,到權(quán)限時(shí)我會(huì)講解) */ Collection<? extends GrantedAuthority> getAuthorities(); /** * 用戶密碼 */ String getPassword(); /** * 用戶名 */ String getUsername(); /** * 用戶沒過期返回true,反之則false */ boolean isAccountNonExpired(); /** * 用戶沒鎖定返回true,反之則false */ boolean isAccountNonLocked(); /** * 用戶憑據(jù)(通常為密碼)沒過期返回true,反之則false */ boolean isCredentialsNonExpired(); /** * 用戶是啟用狀態(tài)返回true,反之則false */ boolean isEnabled(); }
實(shí)際開發(fā)中我們的用戶屬性各種各樣,這些默認(rèn)屬性可能是滿足不了,所以我們一般會(huì)自己實(shí)現(xiàn)該接口,然后設(shè)置好我們實(shí)際的用戶實(shí)體對(duì)象。實(shí)現(xiàn)此接口要重寫很多方法比較麻煩,我們可以繼承Spring Security提供的org.springframework.security.core.userdetails.User
類,該類實(shí)現(xiàn)了UserDetails
接口幫我們省去了重寫方法的工作:
import com.sky.model.system.SysUser; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import java.util.Collection; /** * 自定義對(duì)象 * @author 尹穩(wěn)健~ * @version 1.0 * @time 2023/1/31 */ public class CustomUser extends User { private SysUser sysUser; public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) { super(sysUser.getUsername(), sysUser.getPassword(), authorities); this.sysUser = sysUser; } public SysUser getSysUser() { return sysUser; } public void setSysUser(SysUser sysUser) { this.sysUser = sysUser; } }
2.2.4、 業(yè)務(wù)對(duì)象UserDetailsService
該接口很簡單只有一個(gè)方法:
public interface UserDetailsService { /** * 根據(jù)用戶名獲取用戶對(duì)象(獲取不到直接拋異常) */ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
我們實(shí)現(xiàn)該接口,就完成了自己的業(yè)務(wù)
import com.sky.model.system.SysUser; import com.sky.system.custom.CustomUser; import com.sky.system.service.SysUserService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Collections; import java.util.Objects; /** * 實(shí)現(xiàn)UserDetailsService接口,重寫方法 * @author 尹穩(wěn)健~ * @version 1.0 * @time 2023/1/31 */ @Service public class UserDetailsServiceImpl implements UserDetailsService { @Resource private SysUserService sysUserService; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserService.queryByUsername(username); if (Objects.isNull(sysUser)){ throw new UsernameNotFoundException("用戶名不存在!"); } if(sysUser.getStatus() == 0) { throw new RuntimeException("賬號(hào)已停用"); } return new CustomUser(sysUser, Collections.emptyList()); } }
2.2.5、登錄接口
接下我們需要自定義登陸接口,然后讓SpringSecurity對(duì)這個(gè)接口放行,讓用戶訪問這個(gè)接口的時(shí)候不用登錄也能訪問。
? 在接口中我們通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
? 認(rèn)證成功的話要生成一個(gè)jwt,放入響應(yīng)中返回。
@Slf4j @Api(tags = "系統(tǒng)管理-登錄管理") @RequestMapping("/admin/system/index") @RestController public class IndexController { @Resource private SysUserService sysUserService; @ApiOperation("登錄接口") @PostMapping("/login") public Result<Map<String,Object>> login(@RequestBody LoginVo loginVo){ return sysUserService.login(loginVo); } }
2.2.6、 SecurityConfig配置
package com.sky.system.config; import com.sky.system.custom.CustomMd5PasswordEncoder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Collections; /** * Security配置類 * @author 尹穩(wěn)健~ * @version 1.0 * @time 2023/1/31 */ @Configuration /** * @EnableWebSecurity是開啟SpringSecurity的默認(rèn)行為 */ @EnableWebSecurity public class SecurityConfig { /** * 密碼明文加密方式配置 * @return */ @Bean public PasswordEncoder passwordEncoder(){ return new CustomMd5PasswordEncoder(); } /** * 獲取AuthenticationManager(認(rèn)證管理器),登錄時(shí)認(rèn)證使用 * @param authenticationConfiguration * @return * @throws Exception */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http // 基于 token,不需要 csrf .csrf().disable() // 開啟跨域以便前端調(diào)用接口 .cors().and() .authorizeRequests() // 指定某些接口不需要通過驗(yàn)證即可訪問。登錄接口肯定是不需要認(rèn)證的 .antMatchers("/admin/system/index/login").permitAll() // 靜態(tài)資源,可匿名訪問 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**","/doc.html").permitAll() // 這里意思是其它所有接口需要認(rèn)證才能訪問 .anyRequest().authenticated() .and() // 基于 token,不需要 session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // cors security 解決方案 .cors().configurationSource(corsConfigurationSource()) .and() .build(); } /** * 配置跨源訪問(CORS) * @return */ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedHeaders(Collections.singletonList("*")); configuration.setAllowedMethods(Collections.singletonList("*")); configuration.setAllowedOrigins(Collections.singletonList("*")); configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
controller通過login方法調(diào)用實(shí)際業(yè)務(wù)
@Service public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService { @Resource private SysMenuService sysMenuService; /** * 通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證, */ @Resource private AuthenticationManager authenticationManager; @Override public Result<Map<String, Object>> login(LoginVo loginVo) { // 將表單數(shù)據(jù)封裝到 UsernamePasswordAuthenticationToken UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword()); // authenticate方法會(huì)調(diào)用loadUserByUsername Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken); if(Objects.isNull(authenticate)){ throw new RuntimeException("用戶名或密碼錯(cuò)誤"); } // 校驗(yàn)成功,強(qiáng)轉(zhuǎn)對(duì)象 CustomUser customUser = (CustomUser) authenticate.getPrincipal(); SysUser sysUser = customUser.getSysUser(); // 校驗(yàn)通過返回token String token = JwtUtil.createToken(sysUser.getId(), sysUser.getUsername()); Map<String, Object> map = new HashMap<>(); map.put("token",token); return Result.ok(map); } }
2.2.7、認(rèn)證過濾器
我們需要自定義一個(gè)過濾器,這個(gè)過濾器會(huì)去獲取請(qǐng)求頭中的token,對(duì)token進(jìn)行解析取出其中的信息,獲取對(duì)應(yīng)的LoginUser對(duì)象。然后封裝Authentication對(duì)象存入SecurityContextHolder。
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //獲取token String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { //放行 filterChain.doFilter(request, response); return; } //解析token String userid; try { Claims claims = JwtUtil.parseJWT(token); userid = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } //從redis中獲取用戶信息 String redisKey = "login:" + userid; LoginUser loginUser = redisCache.getCacheObject(redisKey); if(Objects.isNull(loginUser)){ throw new RuntimeException("用戶未登錄"); } //存入SecurityContextHolder //TODO 獲取權(quán)限信息封裝到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //放行 filterChain.doFilter(request, response); } }
3、用戶授權(quán)
在SpringSecurity中,會(huì)使用默認(rèn)的FilterSecurityInterceptor來進(jìn)行權(quán)限校驗(yàn)。在FilterSecurityInterceptor中會(huì)從SecurityContextHolder獲取其中的Authentication,然后獲取其中的權(quán)限信息。判斷當(dāng)前用戶是否擁有訪問當(dāng)前資源所需的權(quán)限。
SpringSecurity中的Authentication類:
public interface Authentication extends Principal, Serializable { //權(quán)限數(shù)據(jù)列表 Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean var1) throws IllegalArgumentException; }
前面登錄時(shí)執(zhí)行l(wèi)oadUserByUsername方法時(shí),return new CustomUser(sysUser, Collections.emptyList());后面的空數(shù)據(jù)對(duì)接就是返回給Spring Security的權(quán)限數(shù)據(jù)。
在TokenAuthenticationFilter中怎么獲取權(quán)限數(shù)據(jù)呢?登錄時(shí)我們把權(quán)限數(shù)據(jù)保存到redis中(用戶名為key,權(quán)限數(shù)據(jù)為value即可),這樣通過token獲取用戶名即可拿到權(quán)限數(shù)據(jù),這樣就可構(gòu)成出完整的Authentication對(duì)象。
3.1、修改loadUserByUsername接口方法
@Autowired private SysMenuService sysMenuService;
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserService.getByUsername(username); if(null == sysUser) { throw new UsernameNotFoundException("用戶名不存在!"); } if(sysUser.getStatus().intValue() == 0) { throw new RuntimeException("賬號(hào)已停用"); } List<String> userPermsList = sysMenuService.findUserPermsList(sysUser.getId()); List<SimpleGrantedAuthority> authorities = new ArrayList<>(); for (String perm : userPermsList) { authorities.add(new SimpleGrantedAuthority(perm.trim())); } return new CustomUser(sysUser, authorities); }
3.2、修改配置類
修改WebSecurityConfig類
配置類添加注解:
開啟基于方法的安全認(rèn)證機(jī)制,也就是說在web層的controller啟用注解機(jī)制的安全確認(rèn)
@EnableGlobalMethodSecurity(prePostEnabled = true)
3.3、控制controller層接口權(quán)限
Spring Security默認(rèn)是禁用注解的,要想開啟注解,需要在繼承WebSecurityConfigurerAdapter的類上加@EnableGlobalMethodSecurity注解,來判斷用戶對(duì)某個(gè)控制層的方法是否具有訪問權(quán)限
通過@PreAuthorize標(biāo)簽控制controller層接口權(quán)限
public class SysRoleController { @Autowired private SysRoleService sysRoleService; @PreAuthorize("hasAuthority('bnt.sysRole.list')") @ApiOperation(value = "獲取分頁列表") @GetMapping("/{page}/{limit}") public Result index( @ApiParam(name = "page", value = "當(dāng)前頁碼", required = true) @PathVariable Long page, @ApiParam(name = "limit", value = "每頁記錄數(shù)", required = true) @PathVariable Long limit, @ApiParam(name = "roleQueryVo", value = "查詢對(duì)象", required = false) SysRoleQueryVo roleQueryVo) { Page<SysRole> pageParam = new Page<>(page, limit); IPage<SysRole> pageModel = sysRoleService.selectPage(pageParam, roleQueryVo); return Result.ok(pageModel); } ... }
3.4、測試服務(wù)器端權(quán)限
登錄后臺(tái),分配權(quán)限進(jìn)行測試,頁面如果添加了按鈕權(quán)限控制,可臨時(shí)去除方便測試
測試結(jié)論:
? 1、分配了權(quán)限的能夠成功返回接口數(shù)據(jù)
? 2、沒有分配權(quán)限的會(huì)拋出異常:org.springframework.security.access.AccessDeniedException: 不允許訪問
4、異常處理
我們還希望在認(rèn)證失敗或者是授權(quán)失敗的情況下也能和我們的接口一樣返回相同結(jié)構(gòu)的json,這樣可以讓前端能對(duì)響應(yīng)進(jìn)行統(tǒng)一的處理。要實(shí)現(xiàn)這個(gè)功能我們需要知道SpringSecurity的異常處理機(jī)制。
? 在SpringSecurity中,如果我們?cè)谡J(rèn)證或者授權(quán)的過程中出現(xiàn)了異常會(huì)被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會(huì)去判斷是認(rèn)證失敗還是授權(quán)失敗出現(xiàn)的異常。
? 如果是認(rèn)證過程中出現(xiàn)的異常會(huì)被封裝成AuthenticationException然后調(diào)用AuthenticationEntryPoint對(duì)象的方法去進(jìn)行異常處理。
? 如果是授權(quán)過程中出現(xiàn)的異常會(huì)被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對(duì)象的方法去進(jìn)行異常處理。
? 所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和AccessDeniedHandler然后配置給SpringSecurity即可。
異常處理有2種方式:
? 1、擴(kuò)展Spring Security異常處理類:AccessDeniedHandler、AuthenticationEntryPoint
? 2、在spring boot全局異常統(tǒng)一處理
第一種方案說明:如果系統(tǒng)實(shí)現(xiàn)了全局異常處理,那么全局異常首先會(huì)獲取AccessDeniedException異常,要想Spring Security擴(kuò)展異常生效,必須在全局異常再次拋出該異常。
①自定義實(shí)現(xiàn)類
import com.alibaba.fastjson2.JSON; import com.fasterxml.jackson.databind.ObjectMapper; import com.sky.common.result.ResultCodeEnum; import com.sky.common.util.WebUtils; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * 認(rèn)證失敗處理 * @author 尹穩(wěn)健~ * @version 1.0 * @time 2023/2/1 */ @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setStatus(200); int code = ResultCodeEnum.LOGIN_AUTH.getCode(); String msg = "認(rèn)證失敗,無法訪問系統(tǒng)資源"; response.setContentType("application/json;charset=UTF-8"); Map<String, Object> result = new HashMap<>(); result.put("msg", msg); result.put("code", code); String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); } }
import com.fasterxml.jackson.databind.ObjectMapper; import com.sky.common.result.ResultCodeEnum; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * @author 尹穩(wěn)健~ * @version 1.0 * @time 2023/2/1 */ @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { int code = ResultCodeEnum.PERMISSION.getCode(); response.setStatus(200); response.setContentType("application/json;charset=UTF-8"); String msg = "權(quán)限不足,無法訪問系統(tǒng)資源"; Map<String, Object> result = new HashMap<>(); result.put("msg", msg); result.put("code", code); String s = new ObjectMapper().writeValueAsString(result); response.getWriter().println(s); } }
②配置給SpringSecurity
? 先注入對(duì)應(yīng)的處理器
@Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler;
? 然后我們可以使用HttpSecurity對(duì)象的方法去配置。
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint). accessDeniedHandler(accessDeniedHandler);
到此這篇關(guān)于SpringSecurity實(shí)現(xiàn)權(quán)限認(rèn)證與授權(quán)的使用示例的文章就介紹到這了,更多相關(guān)SpringSecurity 權(quán)限認(rèn)證與授權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringSecurity進(jìn)行認(rèn)證與授權(quán)的示例代碼
- springSecurity用戶認(rèn)證和授權(quán)的實(shí)現(xiàn)
- SpringBoot整合SpringSecurity認(rèn)證與授權(quán)
- 深入淺析springsecurity入門登錄授權(quán)
- SpringSecurityOAuth2實(shí)現(xiàn)微信授權(quán)登錄
- SpringBoot+SpringSecurity實(shí)現(xiàn)基于真實(shí)數(shù)據(jù)的授權(quán)認(rèn)證
- springsecurity第三方授權(quán)認(rèn)證的項(xiàng)目實(shí)踐
- SpringSecurity數(shù)據(jù)庫進(jìn)行認(rèn)證和授權(quán)的使用
- SpringSecurity授權(quán)機(jī)制的實(shí)現(xiàn)(AccessDecisionManager與投票決策)
相關(guān)文章
IntelliJ IDEA 2017.1.4 x64配置步驟(介紹)
下面小編就為大家?guī)硪黄狪ntelliJ IDEA 2017.1.4 x64配置步驟(介紹)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06Java調(diào)用pyzbar解析base64二維碼過程解析
這篇文章主要介紹了Java調(diào)用pyzbar解析base64二維碼過程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08Java 根據(jù)某個(gè) key 加鎖的實(shí)現(xiàn)方式
日常開發(fā)中,有時(shí)候需要根據(jù)某個(gè) key 加鎖,確保多線程情況下,對(duì)該 key 的加鎖和解鎖之間的代碼串行執(zhí)行,這篇文章主要介紹了Java 根據(jù)某個(gè) key 加鎖的實(shí)現(xiàn)方式,需要的朋友可以參考下2023-03-03SpringMVC 中HttpMessageConverter簡介和Http請(qǐng)求415 的問題
本文介紹且記錄如何解決在SpringMVC 中遇到415 Unsupported Media Type 的問題,并且順便介紹Spring MVC的HTTP請(qǐng)求信息轉(zhuǎn)換器HttpMessageConverter2016-07-07