基于Springboot實現(xiàn)JWT認(rèn)證的示例代碼
最近一直想寫一個類似于待辦的東西,由于不想用傳統(tǒng)的session,就卡住了,后來在各種群里扯皮,發(fā)現(xiàn)除了用緩存之外,還可以通過 JWT 來實現(xiàn)。
一、了解JWT
概念
json web token 用于在各方之間以 json 對象安全地傳輸信息,比如在前端和后端進(jìn)行傳輸,或者在A系統(tǒng)與B系統(tǒng)之間進(jìn)行傳輸。因為它是用的數(shù)字簽名,所以此信息能夠進(jìn)行驗證的,驗證的成功與否決定是否信任。
作用
- 授權(quán):這是jwt應(yīng)用最廣泛的。一旦用戶登錄,每個后續(xù)請求都要包含jwt,從而驗證用戶是否有權(quán)限訪問資源。單點登錄是jwt應(yīng)用最廣泛的一個代表功能。
- 信息交換:在前后端之間、系統(tǒng)之間,對jwt進(jìn)行簽名,比如使用非對稱加密算法(含有公鑰和私鑰的方式),可以保證信息被指定的人給獲取到。
1.1 為什么授權(quán)要使用jwt
比較傳統(tǒng)session認(rèn)證與jwt認(rèn)證的區(qū)別
基于傳統(tǒng)的session認(rèn)證
認(rèn)證方式:http本身是一種是一種無狀態(tài)的協(xié)議。這就意味著,每次進(jìn)行請求,都要帶著用戶名和密碼來進(jìn)行用戶認(rèn)證。為了解決這個問題,我們需要在服務(wù)端存儲一份用戶登錄的信息,這個登錄信息會在響應(yīng)時傳遞給客戶端,保存成cookie,以便下次攜帶發(fā)送給服務(wù)端。這份服務(wù)端存儲的登錄信息就是session。
缺點
- 每個用戶進(jìn)行認(rèn)證之后,服務(wù)器都要在服務(wù)端做一次記錄,以便鑒別下次用戶請求時的身份。通常而言,session都是保存在內(nèi)存中,而隨著認(rèn)證用戶的增多,服務(wù)端的開銷會增大。
- 用戶認(rèn)證之后,服務(wù)端在內(nèi)存中做認(rèn)證記錄,如果下次請求,還需要去訪問有記錄的服務(wù)器才行,這對于分布式的應(yīng)用來說,體驗不好。
- 基于Cookie識別用戶,如果Cookie被抓包到,容易造成跨站請求偽造的攻擊。
基于jwt的認(rèn)證
認(rèn)證方式:前端攜帶用戶名密碼發(fā)送到后端接口,后端核對用戶名和密碼成功后,會將用戶的id等信息,作為payload,將其與header分別進(jìn)行base64加密之后,拼接起來,進(jìn)行加密,形成一種格式xxx.xxx.xxx的jwt字符串(三部分組成,header、payload、signature,中間用點隔開),返回給前端。前端可以將該信息存儲在localStorage或者sessionStorage中,請求時,一般將jwt放入請求頭里authorization中。后端會校驗jwt是否正確、是否過期,然后拿jwt內(nèi)部包含的用戶信息去進(jìn)行其他認(rèn)證通過后的操作。
優(yōu)點
- 簡潔:jwt可以放在url、請求體、請求頭中發(fā)送
- 自包含:payload中包含了用戶所需要的所有信息,避免了多次查詢數(shù)據(jù)庫
- 跨語言:jwt是以json的格式保存在客戶端的,原則上支持所有形式
- 分布式:不需要在服務(wù)端保存會話信息,特別適合分布式微服務(wù)
二、JWT結(jié)構(gòu)
jwt的組成
- header:標(biāo)頭
- payload:負(fù)載
- signature:簽名
jwt通常如下所示,xxxx.xxxx.xxxx,也就是header.payload.signature
2.1 header
header通常是由兩部分組成json。然后進(jìn)行Base64編碼,組成jwt的第一部分
- 令牌的類型
- 使用的簽名算法,例如HmacSha256、Rsa。jwt推薦使用HmacSha256算法
header中也可以加入一些自定義的內(nèi)容
例如下面的這種格式
{
"alg": "HS256",
"typ": "JWT"
}
jwt為了保證編碼的簡短,一般會簡寫過長的單詞,如:
- alg:algorithm,算法
- typ:type,類型
2.2 payload
payload主要存儲用戶和其他的一些數(shù)據(jù),但是不能存放敏感信息,如密碼。然后進(jìn)行Base64編碼,組成jwt的第二部分
payload在java代碼里面也叫做claim,即聲明。
{
"userId": "000001",
"userName": "CCC",
"admin": 1
}
2.3 signature
header與payload是通過Base64進(jìn)行編碼的,前端是可以解開知道里面的內(nèi)容的。
signature就是使用header中提供的算法,對經(jīng)過Base64進(jìn)行編碼過的header和payload,使用我們提供的密鑰來進(jìn)行簽名。簽名的作用是保證jwt沒有被篡改過,如果將signature解密后,與headerBase64.payloadBase64不一致,就是被篡改過的。signature是jwt的第三部分。
如:
String headerPayload=base64UrlEncodeHeader+"."+base64UrlEncodePayload; String signature=HMACSHA256(headerPayload,secret)
jwt最終的結(jié)構(gòu),就是將header的base64,payload的base64,signature加密后的值,用.來分割,拼成一串字符串。

三、使用JWT
3.1 上手
引入依賴
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency>
生成jwt
public class GenerateJWT {
public static void main(String[] args) {
Map<String,Object> map=new HashMap<>();
//獲取過去時間
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND,10);
String jwtToken = JWT.create()
//header,map里面?zhèn)髦担硎驹诔齮ype、algorithm之外,添加自定義的內(nèi)容
.withHeader(map)
//payload
.withClaim("userId", "000001")
.withClaim("userName", "CCC")
.withClaim("admin", 1)
//指定過期時間
.withExpiresAt(calendar.getTime())
//signature
.sign(Algorithm.HMAC384("meethigher"));
System.out.println(jwtToken);
}
}
校驗jwt
public class VerifyJWT {
public static void main(String[] args) {
Map<String,Object> map=new HashMap<>();
//獲取過去時間
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND,60);
String jwtToken = JWT.create()
//header,map里面?zhèn)髦担硎驹诔齮ype、algorithm之外,添加自定義的內(nèi)容
.withHeader(map)
//payload
.withClaim("userId", "000001")
.withClaim("userName", "CCC")
.withClaim("admin", 1)
//指定過期時間
.withExpiresAt(calendar.getTime())
//signature
.sign(Algorithm.HMAC384("meethigher"));
System.out.println(jwtToken);
//創(chuàng)建驗證對象
JWTVerifier verifier = JWT.require(Algorithm.HMAC384("meethigher")).build();
DecodedJWT decodedJWT = verifier.verify(jwtToken);
System.out.println(decodedJWT.getClaim("userId").asString());
System.out.println(decodedJWT.getClaim("userName").asString());
//注意,如果是個整型,用asString會返回一個null。可以點進(jìn)去查看源碼注釋
System.out.println(decodedJWT.getClaim("admin").asInt());
System.out.println(decodedJWT.getExpiresAt());
}
}
3.2 封裝工具類
JWTUtils.java
public class JWTUtils {
private static String SECRET = "meethigher";
/**
* 傳入Payload生成jwt
*
* @param map
* @return
*/
public static String getToken(Map<String, String> map) {
Calendar calendar = Calendar.getInstance();
//7天過期
calendar.add(Calendar.DAY_OF_MONTH, 7);
//第一種存payload方式:遍歷map存入
// JWTCreator.Builder builder = JWT.create();
// map.forEach(builder::withClaim);
//第二種存payload方式:直接放map,底層采用的也是第一種方式
return JWT.create()
.withPayload(map)
.withExpiresAt(calendar.getTime())
.sign(Algorithm.HMAC256(SECRET));
}
/**
* 校驗簽名是否正確,并返回token信息
*
* @param token
* @return
*/
public static DecodedJWT getTokenInfo(String token) {
return JWT.require(Algorithm.HMAC256(SECRET))
.build()
.verify(token);
}
}
3.3 整合springboot
關(guān)鍵代碼
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>top.meethigher</groupId>
<artifactId>springboot-jwt</artifactId>
<version>1.0.0</version>
<name>springboot-jwt</name>
<description>chenchuancheng's demo</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.34.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.gwenn/sqlite-dialect -->
<dependency>
<groupId>com.github.gwenn</groupId>
<artifactId>sqlite-dialect</artifactId>
<version>0.1.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>3.0.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
#logging:
# config: classpath:logback.xml
server:
port: 9090
ssl:
enabled: false
spring:
datasource:
driver-class-name: org.sqlite.JDBC
url: jdbc:sqlite:D:/sqliteData/jwt.db
jpa:
database-platform: org.sqlite.hibernate.dialect.SQLiteDialect
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
mvc:
async:
request-timeout: 30000
SwaggerConfig
@Configuration
public class SwaggerConfig {
//配置swagger的實例
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//只顯示包含Api注解的,如果不加這個,會有basic-error-controller顯示
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("API接口文檔")
.description("API接口文檔")
.version("1.0")
.build();
}
}
InterceptorConfig
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/user/*")
.excludePathPatterns("/user/login");
}
}
LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
HashMap<String, String> map = new HashMap<>();
try {
DecodedJWT tokenInfo = JWTUtils.getTokenInfo(token);
}catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("desc","無效簽名");
}catch (TokenExpiredException e) {
e.printStackTrace();
map.put("desc","token過期");
}catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("desc","token算法不一致");
}catch (Exception e) {
e.printStackTrace();
map.put("desc","無效token");
}
//如果沒有異常,就放行
if(!ObjectUtils.isEmpty(map)) {
//轉(zhuǎn)為json,發(fā)回給前端
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(json);
return false;
}
return true;
}
}
參考
JSON Web Token Introduction - jwt.io
Spring Data JPA(二):SpringBoot集成H2_鄭龍飛-CSDN博客
到此這篇關(guān)于基于Springboot實現(xiàn)JWT認(rèn)證的文章就介紹到這了,更多相關(guān)基于Springboot實現(xiàn)JWT認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloud Netfilx Ribbon負(fù)載均衡工具使用方法介紹
Ribbon是Netflix的組件之一,負(fù)責(zé)注冊中心的負(fù)載均衡,有助于控制HTTP和TCP客戶端行為。Spring Cloud Netflix Ribbon一般配合Ribbon進(jìn)行使用,利用在Eureka中讀取的服務(wù)信息,在調(diào)用服務(wù)節(jié)點時合理進(jìn)行負(fù)載2022-12-12
SpringBoot實現(xiàn)無限級評論回復(fù)的項目實踐
本文主要介紹了SpringBoot實現(xiàn)無限級評論回復(fù)的項目實踐,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03
SpringBoot整合mybatis-plus實現(xiàn)分頁查詢功能
這篇文章主要介紹了SpringBoot整合mybatis-plus實現(xiàn)分頁查詢功能,pringBoot分頁查詢的兩種寫法,一種是手動實現(xiàn),另一種是使用框架實現(xiàn),現(xiàn)在我將具體的實現(xiàn)流程分享一下,需要的朋友可以參考下2023-11-11
Java多線程高并發(fā)中的Fork/Join框架機制詳解
本文主要介紹了 Java 多線程高并發(fā)中的 Fork/Join 框架的基本原理和其使用的工作竊取算法(work-stealing)、設(shè)計方式和部分實現(xiàn)源碼,感興趣的朋友跟隨小編一起看看吧2021-11-11
EL調(diào)用Java方法_動力節(jié)點Java學(xué)院整理
簡單來說,我們在一個類中的某個方法,可以使用EL進(jìn)行調(diào)用,這個能被EL表達(dá)式調(diào)用的方法稱之為EL函數(shù),但是這種方式必須滿足兩點要求,具體哪兩點,大家可以參考下本文2017-07-07
前端dist包放到后端springboot項目下一起打包圖文教程
這篇文章主要介紹了前端dist包放到后端springboot項目下一起打包的相關(guān)資料,具體步驟包括前端打包、將前端文件復(fù)制到后端項目的static目錄、后端打包、驗證部署成功等,需要的朋友可以參考下2025-01-01
SpringAnimation 實現(xiàn)菜單從頂部彈出從底部消失動畫效果
最近做項目遇到這樣一個需求,要求實現(xiàn)一種菜單,菜單從頂部彈入,然后從底部消失,頂部彈入時,有一個上下抖動的過程,底部消失時,先向上滑動,然后再向下滑動消失。下面給大家?guī)砹藢崿F(xiàn)代碼,感興趣的朋友一起看看吧2018-05-05

