SpringBoot3.0+SpringSecurity6.0+JWT的實(shí)現(xiàn)
JWT_SpringSecurity
SpringBoot3.0 + SpringSecurity6.0+JWT
Spring Security 是 Spring 家族中的一個(gè)安全管理框架。
一般Web應(yīng)用的需要進(jìn)行認(rèn)證和授權(quán)。
認(rèn)證:驗(yàn)證當(dāng)前訪問(wèn)系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認(rèn)具體是哪個(gè)用戶
授權(quán):經(jīng)過(guò)認(rèn)證后判斷當(dāng)前用戶是否有權(quán)限進(jìn)行某個(gè)操作
1、快速入門(mén)
1.1、準(zhǔn)備工作
搭建一個(gè)SpringBoot工程
① 設(shè)置父工程 添加依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- DB相關(guān) -->
<!-- JDBC操作數(shù)據(jù)庫(kù) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL依賴 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mp依賴,簡(jiǎn)化crud -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- SpringSecurity依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- SpringWeb依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 熱部署依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- 懶人神器lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- API文檔 - swagger -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
配置文件application.yml
# 端口號(hào)
server:
port: 48080
--- #################### 數(shù)據(jù)庫(kù)相關(guān)配置 ####################
spring:
# 數(shù)據(jù)源配置項(xiàng)
datasource:
url: jdbc:mysql://127.0.0.1:3306/auth-system?useSSL=false&serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true # MySQL Connector/J 8.X 連接的示例
driver-class-name: com.mysql.cj.jdbc.Driver
username: root # 數(shù)據(jù)庫(kù)賬號(hào)
password: 123123123 # 數(shù)據(jù)庫(kù)密碼
# HikariCP 自定義配置,對(duì)應(yīng) HikariConfig 配置屬性類
hikari:
minimum-idle: 10 # 池中維護(hù)的最小空閑連接數(shù),默認(rèn)為 10 個(gè)。
maximum-pool-size: 10 # 池中最大連接數(shù),包括閑置和使用中的連接,默認(rèn)為 10 個(gè)。
# springdoc-openapi項(xiàng)目配置
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs
group-configs:
- group: 'default'
paths-to-match: '/**'
packages-to-scan: org.pp.boot3
# knife4j的增強(qiáng)配置,不需要增強(qiáng)可以不配
knife4j:
enable: true
setting:
language: zh_cn
② 創(chuàng)建啟動(dòng)類
/**
* @author ss_419
*/
@SpringBootApplication
public class SpringSecurity6JwtBoot3Application {
public static void main(String[] args) {
SpringApplication.run(SpringSecurity6JwtBoot3Application.class, args);
}
}
③ 創(chuàng)建Controller
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* TODO 測(cè)試接口
*
* @author ss_419
* @version 1.0
* @date 2023/3/2 20:27
*/
@RestController
@RequestMapping("/api/v1/")
@Tag(name = "測(cè)試接口")
public class GreetingController {
@GetMapping(value = "/hello")
@Operation(summary = "hello")
public ResponseEntity<String> sayHello() {
String message = "Hello World!";
return ResponseEntity.ok(message);
}
}
啟動(dòng)項(xiàng)目,查看接口文檔地址:http://localhost:48080/doc.html#/home
Knife4j的文檔地址:http://ip:port/doc.html即可查看文檔

出現(xiàn)測(cè)試接口,表示項(xiàng)目啟動(dòng)成功
1.2引入SpringSecurity
在SpringBoot項(xiàng)目中使用SpringSecurity我們只需要引入依賴即可實(shí)現(xiàn)入門(mén)案例。
注意??:1.1創(chuàng)建項(xiàng)目時(shí)已經(jīng)引入過(guò)依賴
<!-- SpringSecurity依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入依賴后我們?cè)趪L試去訪問(wèn)之前的接口就會(huì)自動(dòng)跳轉(zhuǎn)到一個(gè)SpringSecurity的默認(rèn)登陸頁(yè)面,默認(rèn)用戶名是user,密碼會(huì)輸出在控制臺(tái)。

須登陸之后才能對(duì)接口進(jìn)行訪問(wèn)
2、認(rèn)證
2.1、原理初探
用戶認(rèn)證流程:
SpringSecurity的原理是一個(gè)過(guò)濾器鏈,內(nèi)部包含了提供各種功能的過(guò)濾器。

- UsernamePasswordAuthenticationFilter:負(fù)責(zé)處理我們?cè)诘顷戫?yè)面填寫(xiě)了用戶名密碼后的登陸請(qǐng)求。入門(mén)案例的認(rèn)證工作主要有它負(fù)責(zé)。
- ExceptionTranslationFilter: 處理過(guò)濾器鏈中拋出的任何AccessDeniedException和AuthenticationException 。
- FilterSecurityInterceptor: 負(fù)責(zé)權(quán)限校驗(yàn)的過(guò)濾器。
- Authentication接口: 它的實(shí)現(xiàn)類,表示當(dāng)前訪問(wèn)系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
- AuthenticationManager接口:定義了認(rèn)證Authentication的方法
- UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個(gè)根據(jù)用戶名查詢用戶信息的方法
- UserDetails接口:提供核心用戶信息。通過(guò)UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成
- UserDetails對(duì)象返回。然后將這些信息封裝到Authentication對(duì)象中。
2.2、用戶認(rèn)證核心組件
Authentication`**,它存儲(chǔ)了認(rèn)證信息,代表當(dāng)前登錄用戶。
我們?cè)诔绦蛑腥绾潍@取并使用它呢?我們需要通過(guò) SecurityContext 來(lái)獲取Authentication,SecurityContext就是我們的上下文對(duì)象!這個(gè)上下文對(duì)象則是交由 SecurityContextHolder 進(jìn)行管理,你可以在程序任何地方使用它:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SecurityContextHolder原理非常簡(jiǎn)單,就是使用ThreadLocal來(lái)保證一個(gè)線程中傳遞同一個(gè)對(duì)象!
現(xiàn)在我們已經(jīng)知道了Spring Security中三個(gè)核心組件:
? 1、Authentication:存儲(chǔ)了認(rèn)證信息,代表當(dāng)前登錄用戶
? 2、SeucirtyContext:上下文對(duì)象,用來(lái)獲取Authentication
? 3、SecurityContextHolder:上下文管理對(duì)象,用來(lái)在程序任何地方獲取SecurityContext
Authentication中是什么信息呢:
? 1、Principal:用戶信息,沒(méi)有認(rèn)證時(shí)一般是用戶名,認(rèn)證后一般是用戶對(duì)象
? 2、Credentials:用戶憑證,一般是密碼
? 3、Authorities:用戶權(quán)限
用戶認(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è)過(guò)濾器中進(jìn)行認(rèn)證的,該過(guò)濾器負(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);
接下來(lái)我們分析一下一個(gè)請(qǐng)求發(fā)送到服務(wù)器都經(jīng)歷了什么:

上圖中當(dāng)有請(qǐng)求發(fā)送給服務(wù)器都要經(jīng)過(guò)Check JWT Token機(jī)制,需要每次收到請(qǐng)求的時(shí)候,過(guò)濾器都處于活動(dòng)狀態(tài)。因此每次用戶發(fā)送請(qǐng)求時(shí)希望過(guò)濾器被觸發(fā)并完成要做的所有工作。
- 如果我們有我們的用戶電子郵箱并且用戶未通過(guò)身份驗(yàn)證,我們會(huì)從數(shù)據(jù)庫(kù)中獲取用戶詳細(xì)信息(loadUserByUsername --> UserDetails)
- 然后我們需要做的是檢查用戶是否有效,如果用戶和令牌有效,我們創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken對(duì)象,傳遞UserDetails & 憑證 & 權(quán)限信息
- 擴(kuò)展上面生成的authToken,包含我們請(qǐng)求的詳細(xì)信息,然后更新安全上下文中的身份驗(yàn)證令牌
- 最后一步執(zhí)行過(guò)濾器chain,別忘記放行,將請(qǐng)求通過(guò)DispatchServlet分發(fā)響應(yīng)給客戶端
登錄認(rèn)證流程
登錄
①自定義登錄接口
調(diào)用ProviderManager的方法進(jìn)行認(rèn)證 如果認(rèn)證通過(guò)生成jwt 把用戶信息存入redis中
②自定義UserDetailsService
在這個(gè)實(shí)現(xiàn)類中去查詢數(shù)據(jù)庫(kù)
校驗(yàn):
①定義Jwt認(rèn)證過(guò)濾器
獲取token
解析token獲取其中的userid
從redis中獲取用戶信息
存入SecurityContextHolder
3、JWT_Security整合流程
3.1、什么是JWT
JWT 主要用于用戶登錄鑒權(quán),所以我們從最傳統(tǒng)的 session 認(rèn)證開(kāi)始說(shuō)起。
Session認(rèn)證:
眾所周知,http 協(xié)議本身是無(wú)狀態(tài)的協(xié)議,那就意味著當(dāng)有用戶向系統(tǒng)使用賬戶名稱和密碼進(jìn)行用戶認(rèn)證之后,下一次請(qǐng)求還要再一次用戶認(rèn)證才行。因?yàn)槲覀儾荒芡ㄟ^(guò) http 協(xié)議知道是哪個(gè)用戶發(fā)出的請(qǐng)求,所以如果要知道是哪個(gè)用戶發(fā)出的請(qǐng)求,那就需要在服務(wù)器保存一份用戶信息(保存至 session ),然后在認(rèn)證成功后返回 cookie 值傳遞給瀏覽器,那么用戶在下一次請(qǐng)求時(shí)就可以帶上 cookie 值,服務(wù)器就可以識(shí)別是哪個(gè)用戶發(fā)送的請(qǐng)求,是否已認(rèn)證,是否登錄過(guò)期等等。這就是傳統(tǒng)的 session 認(rèn)證方式。
session 認(rèn)證的缺點(diǎn)其實(shí)很明顯,由于 session 是保存在服務(wù)器里,所以如果分布式部署應(yīng)用的話,會(huì)出現(xiàn)session不能共享的問(wèn)題,很難擴(kuò)展。于是乎為了解決 session 共享的問(wèn)題,又引入了 redis,接著往下看。
Session認(rèn)證還會(huì)引發(fā)CSRF(跨站請(qǐng)求偽造攻擊),因?yàn)槭腔赾ookie來(lái)進(jìn)行用戶識(shí)別的, cookie如果被截獲,用戶就會(huì)很容易受到跨站請(qǐng)求偽造的攻擊。
Token認(rèn)證:
這種方式跟Session的方式流程差不多,不同的地方在于保存的是一個(gè)token值,token一般是一串隨機(jī)的字符(比如UUID),value 一般是用戶ID,并且設(shè)置一個(gè)過(guò)期時(shí)間。
每次請(qǐng)求服務(wù)的時(shí)候帶上 token 在請(qǐng)求頭,后端接收到token 則根據(jù) token 查一下 redis 是否存在,如果存在則表示用戶已認(rèn)證,如果 token 不存在則跳到登錄界面讓用戶重新登錄,登錄成功后返回一個(gè) token 值給客戶端。
JWT認(rèn)證:
JWT(全稱Json Web Token),是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開(kāi)放標(biāo)準(zhǔn).該token被設(shè)計(jì)為緊湊且安全的,特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場(chǎng)景。JWT的聲明一般被用來(lái)在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息,該token也可直接被用于認(rèn)證,也可被加密。
3.1.1、JWT的數(shù)據(jù)結(jié)構(gòu):
JWT 一般是這樣一個(gè)字符串,分為三個(gè)部分,以 “.” 隔開(kāi):
xxxxx.yyyyy.zzzzz
JWT官網(wǎng):https://jwt.io/
進(jìn)入官網(wǎng)我們可以看到首頁(yè)有這樣一個(gè)頁(yè)面:

其中左側(cè)是生成的jwt編碼,我們可以看到它生成的格式就如上述所描述那樣,分成了三段
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
右側(cè)是對(duì)jwt字符串進(jìn)行解碼
HEADER
jwt的頭部承載兩部分信息:
聲明類型,這里是jwt
聲明加密的算法 通常直接使用 HMAC SHA256
完整的頭部就像下面這樣的JSON:

然后將頭部進(jìn)行base64加密(該加密是可以對(duì)稱解密的),構(gòu)成了第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
PLAYLOAD:
載荷就是存放有效信息的地方。這個(gè)名字像是特指飛機(jī)上承載的貨品,這些有效信息包含三個(gè)部分
標(biāo)準(zhǔn)中注冊(cè)的聲明
公共的聲明
私有的聲明
標(biāo)準(zhǔn)中注冊(cè)的聲明 (建議但不強(qiáng)制使用) :
- iss: jwt簽發(fā)者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過(guò)期時(shí)間,這個(gè)過(guò)期時(shí)間必須要大于簽發(fā)時(shí)間
- nbf: 定義在什么時(shí)間之前,該jwt都是不可用的.
- iat: jwt的簽發(fā)時(shí)間
- jti: jwt的唯一身份標(biāo)識(shí),主要用來(lái)作為一次性token,從而回避重放攻擊。
公共的聲明 : 公共的聲明可以添加任何的信息,一般添加用戶的相關(guān)信息或其他業(yè)務(wù)需要的必要信息.但不建議添加敏感信息,因?yàn)樵摬糠衷诳蛻舳丝山饷?
私有的聲明 : 私有聲明是提供者和消費(fèi)者所共同定義的聲明,一般不建議存放敏感信息,因?yàn)閎ase64是對(duì)稱解密的,意味著該部分信息可以歸類為明文信息。

然后將其進(jìn)行base64加密,得到JWT的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
VERIFY SIGNATURE:
JWT的第三部分是一個(gè)簽證信息,這個(gè)簽證信息由三部分組成:
header (base64后的)
payload (base64后的)
secret
這個(gè)部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過(guò)header中聲明的加密方式進(jìn)行加鹽secret組合加密,然后就構(gòu)成了jwt的第三部分:
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
注意:secret是保存在服務(wù)器端的,jwt的簽發(fā)生成也是在服務(wù)器端的,secret就是用來(lái)進(jìn)行jwt的簽發(fā)和jwt的驗(yàn)證,所以,它就是你服務(wù)端的私鑰,在任何場(chǎng)景都不應(yīng)該流露出去。一旦客戶端得知這個(gè)secret, 那就意味著客戶端是可以自我簽發(fā)jwt了。
如何應(yīng)用:
一般是在請(qǐng)求頭里加入Authorization,并加上Bearer標(biāo)注:
'Authorization': 'Bearer ' + token
3.1.2、簽名密鑰
在Json網(wǎng)絡(luò)令牌的安全上下文中,簽名密鑰是用于對(duì)JWT進(jìn)行數(shù)字簽名的加密信息,簽名密鑰用于創(chuàng)建JWT的簽名部分,用于驗(yàn)證JWT的發(fā)送者是否是已經(jīng)經(jīng)過(guò)確認(rèn)的用戶,并確保消息在整個(gè)過(guò)程中沒(méi)有被更改(保證一致性),因此我們要確保發(fā)送此JWT密鑰的用戶是同一個(gè)人。
簽名密鑰通常與JWT標(biāo)頭中指定的登錄算法結(jié)合使用,以創(chuàng)建簽名具體的登錄算法,密鑰大小將取決于應(yīng)用程序的安全要求和信任級(jí)別(簽名方)
可以在allkeysgenertor中生成任意大小的簽名密鑰
注意??:在JWT中最低安全級(jí)別是256bit,因此在本教程中,我們將采用256bit的簽名密鑰,如下所示:

3.2、準(zhǔn)備工作
①添加依賴
<!-- JWT 相關(guān) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
<!--redis依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依賴-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
② 添加Redis相關(guān)配置
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
/**
* Redis使用FastJson序列化
* @author ss_419
*/
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class<T> clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJsonRedisSerializer(Class<T> clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
protected JavaType getJavaType(Class<?> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* TODO Redis配置
*
* @author ss_419
* @version 1.0
* @date 2023/3/3 10:24
*/
@Configuration
public class RedisConfig {
/**
* Redis配置
*/
@Bean
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object,Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class);
// 使用StringRedisSerializer來(lái)序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
package org.pp.boot3.config.redis;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author ss_419
*/
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
@RequiredArgsConstructor
public class RedisCache {
private final RedisTemplate redisTemplate;
/**
* 緩存基本的對(duì)象,Integer、String、實(shí)體類等
*
* @param key 緩存的鍵值
* @param value 緩存的值
*/
public <T> void setCacheObject(final String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 緩存基本的對(duì)象,Integer、String、實(shí)體類等
*
* @param key 緩存的鍵值
* @param value 緩存的值
* @param timeout 時(shí)間
* @param timeUnit 時(shí)間顆粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 設(shè)置有效時(shí)間
*
* @param key Redis鍵
* @param timeout 超時(shí)時(shí)間
* @return true=設(shè)置成功;false=設(shè)置失敗
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 設(shè)置有效時(shí)間
*
* @param key Redis鍵
* @param timeout 超時(shí)時(shí)間
* @param unit 時(shí)間單位
* @return true=設(shè)置成功;false=設(shè)置失敗
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 獲得緩存的基本對(duì)象。
*
* @param key 緩存鍵值
* @return 緩存鍵值對(duì)應(yīng)的數(shù)據(jù)
*/
public <T> T getCacheObject(final String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 刪除單個(gè)對(duì)象
*
* @param key
*/
public boolean deleteObject(final String key) {
return redisTemplate.delete(key);
}
/**
* 刪除集合對(duì)象
*
* @param collection 多個(gè)對(duì)象
* @return
*/
public long deleteObject(final Collection collection) {
return redisTemplate.delete(collection);
}
/**
* 緩存List數(shù)據(jù)
*
* @param key 緩存的鍵值
* @param dataList 待緩存的List數(shù)據(jù)
* @return 緩存的對(duì)象
*/
public <T> long setCacheList(final String key, final List<T> dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 獲得緩存的list對(duì)象
*
* @param key 緩存的鍵值
* @return 緩存鍵值對(duì)應(yīng)的數(shù)據(jù)
*/
public <T> List<T> getCacheList(final String key) {
return redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 緩存Set
*
* @param key 緩存鍵值
* @param dataSet 緩存的數(shù)據(jù)
* @return 緩存數(shù)據(jù)的對(duì)象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}
/**
* 獲得緩存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key) {
return redisTemplate.opsForSet().members(key);
}
/**
* 緩存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 獲得緩存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入數(shù)據(jù)
*
* @param key Redis鍵
* @param hKey Hash鍵
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 獲取Hash中的數(shù)據(jù)
*
* @param key Redis鍵
* @param hKey Hash鍵
* @return Hash中的對(duì)象
*/
public <T> T getCacheMapValue(final String key, final String hKey) {
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 刪除Hash中的數(shù)據(jù)
*
* @param key
* @param hkey
*/
public void delCacheMapValue(final String key, final String hkey) {
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hkey);
}
/**
* 獲取多個(gè)Hash中的數(shù)據(jù)
*
* @param key Redis鍵
* @param hKeys Hash鍵集合
* @return Hash對(duì)象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 獲得緩存的基本對(duì)象列表
*
* @param pattern 字符串前綴
* @return 對(duì)象列表
*/
public Collection<String> keys(final String pattern) {
return redisTemplate.keys(pattern);
}
}
③ 響應(yīng)類&工具類
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* 統(tǒng)一響應(yīng)類
* @author ss_419
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
/**
* 狀態(tài)碼
*/
private Integer code;
/**
* 提示信息,如果有錯(cuò)誤時(shí),前端可以獲取該字段進(jìn)行提示
*/
private String msg;
/**
* 查詢到的結(jié)果數(shù)據(jù),
*/
private T data;
public ResponseResult(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public ResponseResult(Integer code, T data) {
this.code = code;
this.data = data;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public ResponseResult(Integer code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
}
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* TODO Web工具
*
* @author ss_419
* @version 1.0
* @date 2023/3/3 10:39
*/
public class WebUtil {
/**
* 將字符串渲染到客戶端
*
* @param response 渲染對(duì)象
* @param string 待渲染的字符串
* @return null
*/
public static String renderString(HttpServletResponse response, String string) {
try
{
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
}
catch (IOException e)
{
e.printStackTrace();
}
return null;
}
}
用戶實(shí)體類SysUser
/**
* 用戶表
* @author ss_419
* @TableName sys_user
*/
@TableName(value ="sys_user")
@Data
public class SysUser implements Serializable {
/**
* 會(huì)員id
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用戶名
*/
private String username;
/**
* 密碼
*/
private String password;
/**
* 姓名
*/
private String name;
/**
* 手機(jī)
*/
private String phone;
/**
* 頭像地址
*/
private String headUrl;
/**
* 部門(mén)id
*/
private Long deptId;
/**
* 崗位id
*/
private Long postId;
/**
* 描述
*/
private String description;
/**
* 狀態(tài)(1:正常 0:停用)
*/
private Integer status;
/**
* 創(chuàng)建時(shí)間
*/
private Date createTime;
/**
* 更新時(shí)間
*/
private Date updateTime;
/**
* 刪除標(biāo)記(0:可用 1:已刪除)
*/
private Integer isDeleted;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
SysUser other = (SysUser) that;
return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
&& (this.getUsername() == null ? other.getUsername() == null : this.getUsername().equals(other.getUsername()))
&& (this.getPassword() == null ? other.getPassword() == null : this.getPassword().equals(other.getPassword()))
&& (this.getName() == null ? other.getName() == null : this.getName().equals(other.getName()))
&& (this.getPhone() == null ? other.getPhone() == null : this.getPhone().equals(other.getPhone()))
&& (this.getHeadUrl() == null ? other.getHeadUrl() == null : this.getHeadUrl().equals(other.getHeadUrl()))
&& (this.getDeptId() == null ? other.getDeptId() == null : this.getDeptId().equals(other.getDeptId()))
&& (this.getPostId() == null ? other.getPostId() == null : this.getPostId().equals(other.getPostId()))
&& (this.getDescription() == null ? other.getDescription() == null : this.getDescription().equals(other.getDescription()))
&& (this.getStatus() == null ? other.getStatus() == null : this.getStatus().equals(other.getStatus()))
&& (this.getCreateTime() == null ? other.getCreateTime() == null : this.getCreateTime().equals(other.getCreateTime()))
&& (this.getUpdateTime() == null ? other.getUpdateTime() == null : this.getUpdateTime().equals(other.getUpdateTime()))
&& (this.getIsDeleted() == null ? other.getIsDeleted() == null : this.getIsDeleted().equals(other.getIsDeleted()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
result = prime * result + ((getUsername() == null) ? 0 : getUsername().hashCode());
result = prime * result + ((getPassword() == null) ? 0 : getPassword().hashCode());
result = prime * result + ((getName() == null) ? 0 : getName().hashCode());
result = prime * result + ((getPhone() == null) ? 0 : getPhone().hashCode());
result = prime * result + ((getHeadUrl() == null) ? 0 : getHeadUrl().hashCode());
result = prime * result + ((getDeptId() == null) ? 0 : getDeptId().hashCode());
result = prime * result + ((getPostId() == null) ? 0 : getPostId().hashCode());
result = prime * result + ((getDescription() == null) ? 0 : getDescription().hashCode());
result = prime * result + ((getStatus() == null) ? 0 : getStatus().hashCode());
result = prime * result + ((getCreateTime() == null) ? 0 : getCreateTime().hashCode());
result = prime * result + ((getUpdateTime() == null) ? 0 : getUpdateTime().hashCode());
result = prime * result + ((getIsDeleted() == null) ? 0 : getIsDeleted().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", username=").append(username);
sb.append(", password=").append(password);
sb.append(", name=").append(name);
sb.append(", phone=").append(phone);
sb.append(", headUrl=").append(headUrl);
sb.append(", deptId=").append(deptId);
sb.append(", postId=").append(postId);
sb.append(", description=").append(description);
sb.append(", status=").append(status);
sb.append(", createTime=").append(createTime);
sb.append(", updateTime=").append(updateTime);
sb.append(", isDeleted=").append(isDeleted);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
用戶表Mapper:
/**
* @author ss_419
* @description 針對(duì)表【sys_user(用戶表)】的數(shù)據(jù)庫(kù)操作Mapper
* @createDate 2023-03-03 10:41:42
* @Entity org.pp.boot3.domain.SysUser
*/
public interface SysUserMapper extends BaseMapper<SysUser> {
}
用戶Service:
/**
* @author ss_419
* @description 針對(duì)表【sys_user(用戶表)】的數(shù)據(jù)庫(kù)操作Service
* @createDate 2023-03-03 10:41:42
*/
public interface SysUserService extends IService<SysUser> {
}
用戶ServiceImpl:
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser>
implements SysUserService{
}
在啟動(dòng)類上配置mapper掃描:
/**
* @author ss_419
*/
@SpringBootApplication
@ComponentScan("org.pp.boot3.mapper")
public class SpringSecurity6JwtBoot3Application {
public static void main(String[] args) {
SpringApplication.run(SpringSecurity6JwtBoot3Application.class, args);
}
}
創(chuàng)建一個(gè)用戶表,sql如下:
CREATE TABLE `sys_user` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用戶名', `nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵稱', `password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密碼', `status` CHAR(1) DEFAULT '0' COMMENT '賬號(hào)狀態(tài)(0正常 1停用)', `email` VARCHAR(64) DEFAULT NULL COMMENT '郵箱', `phonenumber` VARCHAR(32) DEFAULT NULL COMMENT '手機(jī)號(hào)', `sex` CHAR(1) DEFAULT NULL COMMENT '用戶性別(0男,1女,2未知)', `avatar` VARCHAR(128) DEFAULT NULL COMMENT '頭像', `user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用戶類型(0管理員,1普通用戶)', `create_by` BIGINT(20) DEFAULT NULL COMMENT '創(chuàng)建人的用戶id', `create_time` DATETIME DEFAULT NULL COMMENT '創(chuàng)建時(shí)間', `update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人', `update_time` DATETIME DEFAULT NULL COMMENT '更新時(shí)間', `del_flag` INT(11) DEFAULT '0' COMMENT '刪除標(biāo)志(0代表未刪除,1代表已刪除)', PRIMARY KEY (`id`) ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用戶表'
3.3、核心代碼實(shí)現(xiàn)部分
1.創(chuàng)建ApplicationConfig提供全局的Bean對(duì)象,以供使用
/**
* TODO 全局的Bean對(duì)象提供者
* @author ss_419
*
* @RequiredArgsConstructor --> 代替原本的@Autowired
*/
@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
// 注入數(shù)據(jù)庫(kù)操作DAO
private final SysUserMapper repository;
/**
*
* @return 用戶詳細(xì)信息 -> jwt身份驗(yàn)證過(guò)濾器
*/
@Bean
public UserDetailsService userDetailsService() {
return username -> repository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
/**
* TODO 四 4.2
* @return 身份校驗(yàn)機(jī)制、身份驗(yàn)證提供程序
*/
@Bean
public AuthenticationProvider authenticationProvider() {
// 創(chuàng)建一個(gè)用戶認(rèn)證提供者
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
// 設(shè)置用戶相信信息,可以從數(shù)據(jù)庫(kù)中讀取、或者緩存、或者配置文件
authProvider.setUserDetailsService(userDetailsService());
// 設(shè)置加密機(jī)制,若想要嘗試對(duì)用戶進(jìn)行身份驗(yàn)證,我們需要知道使用的是什么編碼
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
/**
* TODO 四 4.4 基于用戶名和密碼或使用用戶名和密碼進(jìn)行身份驗(yàn)證
* @param config
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/**
* TODO 四 4.3提供編碼機(jī)制
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.創(chuàng)建JWT工具類(Service)
package org.pp.boot3.config.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
import java.util.function.Function;
/**
* TODO 完成JWT的驗(yàn)證服務(wù)
* JWT工具類
*
* @author ss_419
* @version 1.0
* @date 2023/3/3 11:16
*/
@Service
public class JwtService {
/**
* 創(chuàng)建一個(gè)最終字符串,這個(gè)字符串稱為密鑰
* https://allkeysgenerator.com/
*
* JWT最低要求的安全級(jí)別是256bit
*/
private static final String SECRET_KEY = "3F4428472B4B6250655368566D5971337336763979244226452948404D635166";
/**
* 1、解析token字符串中的加密信息【加密算法&加密密鑰】, 提取所有聲明的方法
* @param token
* @return
*/
private Claims extractAllClaims(String token){
return Jwts
.parserBuilder()
// 獲取alg開(kāi)頭的信息
.setSigningKey(getSignInKey())
.build()
// 解析token字符串
.parseClaimsJws(token)
.getBody();
}
/**
* 2、獲取簽名密鑰的方法
* @return 基于指定的密鑰字節(jié)數(shù)組創(chuàng)建用于HMAC-SHA算法的新SecretKey實(shí)例
*/
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 3、解析token字符串中的權(quán)限信息
* @param token
* @return
*/
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
/**
* 4、從token中解析出username
* @param token
* @return
*/
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
/**
* 5、判斷token是否過(guò)期
* @param
* @return
*/
public boolean isTokenValid(String token, UserDetails userDetails) {
// 從token中獲取用戶名
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) &&!isTokenExpired(token);
}
/**
* 6、驗(yàn)證token是否過(guò)期
* @param token
* @return
*/
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
/**
* 6.1、從授權(quán)信息中獲取token過(guò)期時(shí)間
*/
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
}
3.完成JwtAuthenticationFilter身份驗(yàn)證過(guò)濾器
/**
* TODO 一、JWT身份驗(yàn)證過(guò)濾器
*
* @author ss_419
* @version 1.0
* @date 2023/3/3 10:56
*/
@Component
// 使用final,將服務(wù)注入class
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
/**
* 需要每次收到請(qǐng)求的時(shí)候,過(guò)濾器都處于活動(dòng)狀態(tài)
* 因此每次用戶發(fā)送請(qǐng)求時(shí)希望過(guò)濾器被觸發(fā)并完成要做的所有工作
*/
private final JwtService jwtService;
/**
* 加載用戶特定數(shù)據(jù)的核心接口。
* 它作為用戶DAO在整個(gè)框架中使用,并且是DaoAuthenticationProvider使用的策略
*/
private final UserDetailsService userDetailsService;// 從ApplicationConfig中創(chuàng)建的Bean對(duì)象獲取
/**
* 總體流程:
* 如果我們有我們的用戶電子郵箱并且用戶未通過(guò)身份驗(yàn)證,我們會(huì)從數(shù)據(jù)庫(kù)中獲取用戶詳細(xì)信息(loadUserByUsername --> UserDetails)
* 然后我們需要做的是檢查用戶是否有效,如果用戶和令牌有效,我們創(chuàng)建一個(gè)UsernamePasswordAuthenticationToken對(duì)象,傳遞UserDetails & 憑證 & 權(quán)限信息
* 擴(kuò)展上面生成的authToken,包含我們請(qǐng)求的詳細(xì)信息,然后更新安全上下文中的身份驗(yàn)證令牌
* 最后一步執(zhí)行過(guò)濾器chain,別忘記放行
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 從請(qǐng)求頭中獲取認(rèn)證信息
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String username;
if(authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7);
// 從token中解析出username
username = jwtService.extractUsername(jwt);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null){
// 根據(jù)jwt解析出來(lái)的username,獲取數(shù)據(jù)庫(kù)中的用戶信息,封裝UserDetails對(duì)象
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// TODO 此處token有效性可以從redis|數(shù)據(jù)庫(kù)中獲取
Boolean isTokenValid = true;
if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) {
// TODO 如果令牌有效,封裝一個(gè)UsernamePasswordAuthenticationToken對(duì)象
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails,
// 用戶憑證
null,
userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
// 更新安全上下文的持有用戶
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
}
4.改造自動(dòng)生成的SysUser
package org.pp.boot3.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import com.github.xiaoymin.knife4j.annotations.Ignore;
import lombok.Data;
import org.pp.boot3.domain.enums.Role;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
/**
* 用戶表
* @author ss_419
* @TableName sys_user
*/
@TableName(value ="sys_user")
@Data
public class SysUser implements UserDetails {
/**
* 會(huì)員id
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用戶名
*/
private String username;
/**
* 密碼
*/
private String password;
/**
* 姓名
*/
private String name;
/**
* 手機(jī)
*/
private String phone;
/**
* 頭像地址
*/
private String headUrl;
/**
* 部門(mén)id
*/
private Long deptId;
/**
* 崗位id
*/
private Long postId;
/**
* 描述
*/
private String description;
/**
* 狀態(tài)(1:正常 0:停用)
*/
private Integer status;
/**
* 創(chuàng)建時(shí)間
*/
private Date createTime;
/**
* 更新時(shí)間
*/
private Date updateTime;
/**
* 刪除標(biāo)記(0:可用 1:已刪除)
*/
private Integer isDeleted;
/**
* 角色集合
*/
private Role role;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
SysUser other = (SysUser) that;
return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
&& (this.getUsername() == null ? other.getUsername() == null : this.getUsername().equals(other.getUsername()))
&& (this.getPassword() == null ? other.getPassword() == null : this.getPassword().equals(other.getPassword()))
&& (this.getName() == null ? other.getName() == null : this.getName().equals(other.getName()))
&& (this.getPhone() == null ? other.getPhone() == null : this.getPhone().equals(other.getPhone()))
&& (this.getHeadUrl() == null ? other.getHeadUrl() == null : this.getHeadUrl().equals(other.getHeadUrl()))
&& (this.getDeptId() == null ? other.getDeptId() == null : this.getDeptId().equals(other.getDeptId()))
&& (this.getPostId() == null ? other.getPostId() == null : this.getPostId().equals(other.getPostId()))
&& (this.getDescription() == null ? other.getDescription() == null : this.getDescription().equals(other.getDescription()))
&& (this.getStatus() == null ? other.getStatus() == null : this.getStatus().equals(other.getStatus()))
&& (this.getCreateTime() == null ? other.getCreateTime() == null : this.getCreateTime().equals(other.getCreateTime()))
&& (this.getUpdateTime() == null ? other.getUpdateTime() == null : this.getUpdateTime().equals(other.getUpdateTime()))
&& (this.getIsDeleted() == null ? other.getIsDeleted() == null : this.getIsDeleted().equals(other.getIsDeleted()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
result = prime * result + ((getUsername() == null) ? 0 : getUsername().hashCode());
result = prime * result + ((getPassword() == null) ? 0 : getPassword().hashCode());
result = prime * result + ((getName() == null) ? 0 : getName().hashCode());
result = prime * result + ((getPhone() == null) ? 0 : getPhone().hashCode());
result = prime * result + ((getHeadUrl() == null) ? 0 : getHeadUrl().hashCode());
result = prime * result + ((getDeptId() == null) ? 0 : getDeptId().hashCode());
result = prime * result + ((getPostId() == null) ? 0 : getPostId().hashCode());
result = prime * result + ((getDescription() == null) ? 0 : getDescription().hashCode());
result = prime * result + ((getStatus() == null) ? 0 : getStatus().hashCode());
result = prime * result + ((getCreateTime() == null) ? 0 : getCreateTime().hashCode());
result = prime * result + ((getUpdateTime() == null) ? 0 : getUpdateTime().hashCode());
result = prime * result + ((getIsDeleted() == null) ? 0 : getIsDeleted().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", username=").append(username);
sb.append(", password=").append(password);
sb.append(", name=").append(name);
sb.append(", phone=").append(phone);
sb.append(", headUrl=").append(headUrl);
sb.append(", deptId=").append(deptId);
sb.append(", postId=").append(postId);
sb.append(", description=").append(description);
sb.append(", status=").append(status);
sb.append(", createTime=").append(createTime);
sb.append(", updateTime=").append(updateTime);
sb.append(", isDeleted=").append(isDeleted);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//
return List.of(new SimpleGrantedAuthority(role.name()));
}
/**
* 用戶沒(méi)有過(guò)期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public String getUsername(){
return username;
}
@Override
public String getPassword() {
return password;
}
/**
* 用戶沒(méi)有鎖定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 用戶憑證沒(méi)有過(guò)期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 用戶是否啟用
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}
創(chuàng)建角色枚舉:
package org.pp.boot3.domain.enums;
/**
* 用戶角色信息枚舉
* @author ss_419
*/
public enum Role {
USER,
ADMIN
}
5.配置Security以啟用上面配置的JwtAuthenticationFilter
package org.pp.boot3.config.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
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.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
/**
* TODO 安全配置
*
* @author ss_419
* @version 1.0
* @date 2023/3/3 14:04
*/
@Configuration
@EnableWebSecurity// 開(kāi)啟網(wǎng)絡(luò)安全注解
@RequiredArgsConstructor
public class SecurityConfiguration {
// 將自定義JwtAuthenticationFilter注入
private final JwtAuthenticationFilter jwtAuthenticationFilter;
// 在ApplicationConfig中提供Bean
private final AuthenticationProvider authenticationProvider;
private final LogoutHandler logoutHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
//禁用csrf(防止跨站請(qǐng)求偽造攻擊)
.csrf()
.disable()
// 設(shè)置白名單
.authorizeHttpRequests()
.requestMatchers("/api/v1/auth/**")
.permitAll()
// 對(duì)于其他任何請(qǐng)求,都保護(hù)起來(lái)
.anyRequest()
.authenticated()
.and()
// 禁用緩存
.sessionManagement()
// 使用無(wú)狀態(tài)session,即不使用session緩存數(shù)據(jù)
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 添加身份驗(yàn)證
.and()
// TODO 添加身份驗(yàn)證1
.authenticationProvider(authenticationProvider)
// 添加JWT過(guò)濾器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 登出操作
.logout()
.logoutUrl("/api/v1/auth/logout")
.addLogoutHandler(logoutHandler)
.logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext())
;
return http.build();
}
}
定義請(qǐng)求響應(yīng)實(shí)體
/**
* 驗(yàn)證請(qǐng)求實(shí)體
* @author ss_419
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationRequest {
private String username;
String password;
}
/**
* 請(qǐng)求響應(yīng)實(shí)體
* @author ss_419
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthenticationResponse {
private String token;
}
/**
* 注冊(cè)請(qǐng)求實(shí)體
* @author ss_419
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RegisterRequest {
private String firstname;
private String lastname;
private String username;
private String password;
}
定義注冊(cè)認(rèn)證服務(wù)
import org.pp.boot3.domain.AuthenticationRequest;
import org.pp.boot3.domain.AuthenticationResponse;
import org.pp.boot3.domain.RegisterRequest;
import org.pp.boot3.domain.SysUser;
/**
* 授權(quán)測(cè)試服務(wù)
* @author ss_419
*/
public interface AuthenticationService {
/**
* 注冊(cè)
* @param request
* @return
*/
public AuthenticationResponse register(RegisterRequest request);
/**
* 登錄|認(rèn)證
* @param request
* @return
*/
public AuthenticationResponse authenticate(AuthenticationRequest request);
/**
* 保存用戶token信息
* @param user
* @param jwtToken
*/
// void saveUserToken(SysUser user, String jwtToken);
/**
* 刪除用戶token信息
* @param user
*/
// void revokeAllUserTokens(SysUser user);
}
/**
* TODO
*
* @author ss_419
* @version 1.0
* @date 2023/3/3 14:27
*/
@Service
@RequiredArgsConstructor
public class AuthenticationServiceImpl implements AuthenticationService {
private final SysUserMapper repository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final RedisCache redisCache;
private final AuthenticationManager authenticationManager;
@Override
public AuthenticationResponse register(RegisterRequest request) {
SysUser user = SysUser.builder()
.username(request.getUsername())
.password(passwordEncoder.encode(request.getPassword()))
.role(Role.USER)
.build();
repository.insert(user);
String jwtToken = jwtService.generateToken(user);
// 將token存儲(chǔ)
redisCache.setCacheObject("token:" ,jwtToken);
// 將token返回響應(yīng)
return AuthenticationResponse
.builder()
.token(jwtToken)
.build();
}
@Override
public AuthenticationResponse authenticate(AuthenticationRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
SysUser user = repository.findByUsername(request.getUsername());
var jwtToken = jwtService.generateToken(user);
// 將token存儲(chǔ)
redisCache.setCacheObject("token:" ,jwtToken);
// 將token返回響應(yīng)
return AuthenticationResponse.builder()
.token(jwtToken)
.build();
}
private void saveUserToken(SysUser user, String jwtToken) {
}
private void revokeAllUserTokens(SysUser user) {
}
}
到此這篇關(guān)于SpringBoot3.0+SpringSecurity6.0+JWT的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringBoot SpringSecurity JWT內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合SpringSecurity和JWT的示例
- SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實(shí)現(xiàn)
- Springboot WebFlux集成Spring Security實(shí)現(xiàn)JWT認(rèn)證的示例
- 詳解SpringBoot+SpringSecurity+jwt整合及初體驗(yàn)
- SpringBoot集成Spring security JWT實(shí)現(xiàn)接口權(quán)限認(rèn)證
- SpringBoot3.x接入Security6.x實(shí)現(xiàn)JWT認(rèn)證的完整步驟
- SpringBoot+SpringSecurity+jwt實(shí)現(xiàn)驗(yàn)證
- SpringBoot Security+JWT簡(jiǎn)單搭建的實(shí)現(xiàn)示例
相關(guān)文章
Java?中的?switch?語(yǔ)句:類型支持與限制詳解
Java?中的?switch?語(yǔ)句是一種強(qiáng)大的多分支選擇結(jié)構(gòu),它支持多種數(shù)據(jù)類型,包括基本數(shù)據(jù)類型、字符串和枚舉類型,本文給大家介紹Java?中的?switch?語(yǔ)句:類型支持與限制,感興趣的朋友一起看看吧2024-08-08
springBoot?@Scheduled實(shí)現(xiàn)多個(gè)任務(wù)同時(shí)開(kāi)始執(zhí)行
這篇文章主要介紹了springBoot?@Scheduled實(shí)現(xiàn)多個(gè)任務(wù)同時(shí)開(kāi)始執(zhí)行,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
SpringBoot3實(shí)現(xiàn)統(tǒng)一結(jié)果封裝的示例代碼
Spring Boot進(jìn)行統(tǒng)一結(jié)果封裝的主要目的是提高開(kāi)發(fā)效率、降低代碼重復(fù)率,并且提供一致的API響應(yīng)格式,從而簡(jiǎn)化前后端交互和錯(cuò)誤處理,所以本文給大家介紹了SpringBoot3實(shí)現(xiàn)統(tǒng)一結(jié)果封裝的方法,需要的朋友可以參考下2024-03-03
Java實(shí)現(xiàn)英文句子中的單詞順序逆序輸出的方法
這篇文章主要介紹了Java實(shí)現(xiàn)英文句子中的單詞順序逆序輸出的方法,涉及java字符串遍歷、判斷、截取、輸出等相關(guān)操作技巧,需要的朋友可以參考下2018-01-01
SpringMVC Json自定義序列化和反序列化的操作方法
這篇文章主要介紹了SpringMVC Json自定義序列化和反序列化的操作方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01

