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、快速入門
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)入門案例。
注意??: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è)面填寫了用戶名密碼后的登陸請(qǐng)求。入門案例的認(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; /** * 部門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; /** * 部門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)文章希望大家以后多多支持腳本之家!
- SpringSecurity多表多端賬戶登錄的實(shí)現(xiàn)
- SpringSecurity集成第三方登錄過(guò)程詳解(最新推薦)
- springsecurity實(shí)現(xiàn)用戶登錄認(rèn)證快速使用示例代碼(前后端分離項(xiàng)目)
- SpringSecurity自動(dòng)登錄流程與實(shí)現(xiàn)詳解
- SpringSecurity6自定義JSON登錄的實(shí)現(xiàn)
- SpringSecurity6.x多種登錄方式配置小結(jié)
- 如何使用JWT的SpringSecurity實(shí)現(xiàn)前后端分離
- SpringSecurity+Redis+Jwt實(shí)現(xiàn)用戶認(rèn)證授權(quán)
- SpringSecurity角色權(quán)限控制(SpringBoot+SpringSecurity+JWT)
- springSecurity之如何添加自定義過(guò)濾器
- springSecurity自定義登錄接口和JWT認(rèn)證過(guò)濾器的流程
相關(guān)文章
jpa?EntityManager?復(fù)雜查詢實(shí)例
這篇文章主要介紹了jpa?EntityManager?復(fù)雜查詢實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12SpringBoot @ModelAttribute使用場(chǎng)景分析
這篇文章主要介紹了SpringBoot @ModelAttribute使用場(chǎng)景分析,文中通過(guò)實(shí)例代碼圖文相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-08-08SpringMVC JSON數(shù)據(jù)交互實(shí)現(xiàn)過(guò)程解析
這篇文章主要介紹了SpringMVC JSON數(shù)據(jù)交互實(shí)現(xiàn)過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10IDEA創(chuàng)建的maven項(xiàng)目中pom.xml增加新依賴無(wú)效問(wèn)題及解決
在IDEA中,解決maven項(xiàng)目pom.xml增加依賴但外部庫(kù)未更新的問(wèn)題,可以通過(guò)設(shè)置"構(gòu)建腳本更改后同步項(xiàng)目"選項(xiàng)為"任何更改",然后刷新Maven項(xiàng)目來(lái)解決2025-01-01idea之Recompile、Rebuild和Build之間的區(qū)別及說(shuō)明
這篇文章主要介紹了idea之Recompile、Rebuild和Build之間的區(qū)別及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08