基于Springboot實(shí)現(xiàn)JWT認(rèn)證的示例代碼
最近一直想寫一個(gè)類似于待辦的東西,由于不想用傳統(tǒng)的session,就卡住了,后來在各種群里扯皮,發(fā)現(xiàn)除了用緩存之外,還可以通過 JWT 來實(shí)現(xiàn)。
一、了解JWT
概念
json web token 用于在各方之間以 json 對象安全地傳輸信息,比如在前端和后端進(jìn)行傳輸,或者在A系統(tǒng)與B系統(tǒng)之間進(jìn)行傳輸。因?yàn)樗怯玫臄?shù)字簽名,所以此信息能夠進(jìn)行驗(yàn)證的,驗(yàn)證的成功與否決定是否信任。
作用
- 授權(quán):這是jwt應(yīng)用最廣泛的。一旦用戶登錄,每個(gè)后續(xù)請求都要包含jwt,從而驗(yàn)證用戶是否有權(quán)限訪問資源。單點(diǎn)登錄是jwt應(yīng)用最廣泛的一個(gè)代表功能。
- 信息交換:在前后端之間、系統(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)證。為了解決這個(gè)問題,我們需要在服務(wù)端存儲(chǔ)一份用戶登錄的信息,這個(gè)登錄信息會(huì)在響應(yīng)時(shí)傳遞給客戶端,保存成cookie,以便下次攜帶發(fā)送給服務(wù)端。這份服務(wù)端存儲(chǔ)的登錄信息就是session。
缺點(diǎn)
- 每個(gè)用戶進(jìn)行認(rèn)證之后,服務(wù)器都要在服務(wù)端做一次記錄,以便鑒別下次用戶請求時(shí)的身份。通常而言,session都是保存在內(nèi)存中,而隨著認(rèn)證用戶的增多,服務(wù)端的開銷會(huì)增大。
- 用戶認(rèn)證之后,服務(wù)端在內(nèi)存中做認(rèn)證記錄,如果下次請求,還需要去訪問有記錄的服務(wù)器才行,這對于分布式的應(yīng)用來說,體驗(yàn)不好。
- 基于Cookie識(shí)別用戶,如果Cookie被抓包到,容易造成跨站請求偽造的攻擊。
基于jwt的認(rèn)證
認(rèn)證方式:前端攜帶用戶名密碼發(fā)送到后端接口,后端核對用戶名和密碼成功后,會(huì)將用戶的id等信息,作為payload,將其與header分別進(jìn)行base64加密之后,拼接起來,進(jìn)行加密,形成一種格式xxx.xxx.xxx的jwt字符串(三部分組成,header、payload、signature,中間用點(diǎn)隔開),返回給前端。前端可以將該信息存儲(chǔ)在localStorage或者sessionStorage中,請求時(shí),一般將jwt放入請求頭里authorization中。后端會(huì)校驗(yàn)jwt是否正確、是否過期,然后拿jwt內(nèi)部包含的用戶信息去進(jìn)行其他認(rèn)證通過后的操作。
優(yōu)點(diǎn)
- 簡潔:jwt可以放在url、請求體、請求頭中發(fā)送
- 自包含:payload中包含了用戶所需要的所有信息,避免了多次查詢數(shù)據(jù)庫
- 跨語言:jwt是以json的格式保存在客戶端的,原則上支持所有形式
- 分布式:不需要在服務(wù)端保存會(huì)話信息,特別適合分布式微服務(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為了保證編碼的簡短,一般會(huì)簡寫過長的單詞,如:
- alg:algorithm,算法
- typ:type,類型
2.2 payload
payload主要存儲(chǔ)用戶和其他的一些數(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<>(); //獲取過去時(shí)間 Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND,10); String jwtToken = JWT.create() //header,map里面?zhèn)髦?,表示在除type、algorithm之外,添加自定義的內(nèi)容 .withHeader(map) //payload .withClaim("userId", "000001") .withClaim("userName", "CCC") .withClaim("admin", 1) //指定過期時(shí)間 .withExpiresAt(calendar.getTime()) //signature .sign(Algorithm.HMAC384("meethigher")); System.out.println(jwtToken); } }
校驗(yàn)jwt
public class VerifyJWT { public static void main(String[] args) { Map<String,Object> map=new HashMap<>(); //獲取過去時(shí)間 Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND,60); String jwtToken = JWT.create() //header,map里面?zhèn)髦?,表示在除type、algorithm之外,添加自定義的內(nèi)容 .withHeader(map) //payload .withClaim("userId", "000001") .withClaim("userName", "CCC") .withClaim("admin", 1) //指定過期時(shí)間 .withExpiresAt(calendar.getTime()) //signature .sign(Algorithm.HMAC384("meethigher")); System.out.println(jwtToken); //創(chuàng)建驗(yàn)證對象 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()); //注意,如果是個(gè)整型,用asString會(huì)返回一個(gè)null??梢渣c(diǎn)進(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)); } /** * 校驗(yàn)簽名是否正確,并返回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的實(shí)例 @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() //只顯示包含Api注解的,如果不加這個(gè),會(huì)有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實(shí)現(xiàn)JWT認(rèn)證的文章就介紹到這了,更多相關(guān)基于Springboot實(shí)現(xiàn)JWT認(rèn)證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot3.1.2 引入Swagger報(bào)錯(cuò)Type javax.servlet.http
這篇文章主要介紹了SpringBoot3.1.2 引入Swagger報(bào)錯(cuò)Type javax.servlet.http.HttpServletRequest not present解決辦法,文中通過代碼示例給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-03-03Java多線程工具CompletableFuture的使用教程
CompletableFuture實(shí)現(xiàn)了CompletionStage接口和Future接口,前者是對后者的一個(gè)擴(kuò)展,增加了異步回調(diào)、流式處理、多個(gè)Future組合處理的能力。本文就來詳細(xì)講講CompletableFuture的使用方式,需要的可以參考一下2022-08-08Spring Security permitAll()不允許匿名訪問的操作
這篇文章主要介紹了Spring Security permitAll()不允許匿名訪問的操作,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06spring security4 添加驗(yàn)證碼的示例代碼
本篇文章主要介紹了spring security4 添加驗(yàn)證碼的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-02-02基于Java的打包jar、war、ear包的作用與區(qū)別詳解
本篇文章,小編為大家介紹,基于Java的打包jar、war、ear包的作用與區(qū)別詳解。需要的朋友參考下2013-04-04java ThreadPoolExecutor線程池拒絕策略避坑
這篇文章主要為大家介紹了java ThreadPoolExecutor拒絕策略避坑踩坑示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07解決IDEA鼠標(biāo)點(diǎn)擊光標(biāo)變大問題
這篇文章主要介紹了解決IDEA鼠標(biāo)點(diǎn)擊光標(biāo)變大問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02基于IDEA查看maven依賴結(jié)構(gòu)流程解析
這篇文章主要介紹了基于IDEA查看maven依賴結(jié)構(gòu)流程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09最優(yōu)雅地整合 Spring & Spring MVC & MyBatis 搭建 Java 企業(yè)級應(yīng)用(附源碼)
這篇文章主要介紹了最優(yōu)雅地整合 Spring & Spring MVC & MyBatis 搭建 Java 企業(yè)級應(yīng)用(附源碼),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01