當前位置:網站首頁>登錄令牌JWT — JSON WEB TOKEN

登錄令牌JWT — JSON WEB TOKEN

2022-01-28 02:25:54 java廠長

登錄令牌JWT — JSON WEB TOKEN

關於作者

  • 作者介紹

博客主頁:作者主頁

簡介:JAVA領域優質創作者、一名在校大三學生、在校期間參加各種省賽、國賽,斬獲一系列榮譽

關注我:關注我學習資料、文檔下載統統都有,每日定時更新文章,勵志做一名JAVA資深程序猿


JWT簡介

1、概述

傳統的Web應用中,使用session來存在用戶的信息,每次用戶認證通過以後,服務器需要創建一條記錄 保存用戶信息,通常是在內存中。

  • 隨著認證通過的用戶越來越多,服務器的在這裏的開銷就會越來越大
  • 由於Session是在內存中的,這就帶來一些擴展性的問題
  • 當我們想要擴展我們的應用,讓我們的數據被多個移動設備使用時,我們必須考慮跨資源共享問題
  • 需要客戶端(瀏覽器)中使用cookie存儲session的ID值,但是移動端設備沒有cookie

image-20220118120148079

2、什麼是JWT?

JWT是是目前最流行的跨域認證解决方案,本文介紹它的原理和用法。並且是一種用於雙方之間傳遞安全信息的簡潔的、URL安全的錶述性聲明規範。JWT作為一個開放的標准(RFC 7519),定義了一種簡潔的,自包含的方法用於通信雙方之間以Json對象的形式安全的傳遞信息。因為數字簽名的存在,這些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘鑰對進行簽名。

JWT就是token的一種具體實現方式,其全稱是JSON Web Token

官網地址:https://jwt.io/

先來說一下基本的流程:

  1. 跨域是一個請求url的協議、域名、端口三者之間任意一個與當前頁面url不同即為跨域,要想跨域肯定要實現不同的端到端的通信。
  2. 客戶端使用用戶名和密碼進行登錄
  3. 服務端收到請求,驗證客戶端的用戶名和密碼
  4. 驗證成功後,服務端會簽發一個類似於鑰匙一樣的私鑰token,再把這個token返回給客戶端
  5. 客戶端收到token後可以把它存儲到cookie、session、redis等
  6. 客戶端每次向服務端請求資源時需要攜帶服務端簽發的token,可以在cookie、header中攜帶
  7. 服務端接收到請求,然後驗證客戶端請求裏面是否攜帶著token,如果驗證成功,就向客戶端返回請求數據

與傳統的認證方式相比有哪些好處

  • json的通用性非常好,JWT支持多種語言實現,如JAVA,JavaScript,JS,PHP等很多語言都可以使用。
  • 因為有了payload部分,可以攜帶非敏感信息。
  • 方便信息傳遞,jwt的組成簡單,占用字節小。
  • 易於應用的擴展,不需要在服務端保存會話信息。

原理圖

jwt

1、前端也就是客戶端,通過錶單提交用戶名和密碼信息發送到後端(服務的)

2、後端(服務端)驗證該用戶的用戶名和密碼是否正確,驗證通過通過代碼規定生成相對應的token令牌,token令牌將包含用戶的數據i西信息作為Payload,與JWT Header分別進行Base64編碼拼接後簽名,生產類似於zzz.sss.rrr的字符串

3、後端(服務端)將生成的token作為判斷用戶登錄成功的依據

4、前端拿到後端發來的token令牌後存儲起來,等下一次用戶需要再次請求服務器時,該用戶將攜帶token(未過期的)請求服務器端以獲取數據

5、後端攔截該用戶的請求,判斷token是否過期,未過期則執行業務邏輯,返回用戶需要的數據

6、後端攔截該用戶的請求,判斷token是否過期,token令牌過期則返回錯誤的登錄信息,這是需要後端再次生成token令牌,此時又會回到步驟1。

注意,session和JWT的主要區別就是保存的比特置,session是保存在服務端的,而JWT是保存在客戶 端的,JWT就是一個固定格式的字符串

3、結構

JWT固定各種的字符串,由三部分組成:

  • Header,頭部
  • Payload,載荷
  • Signature,簽名

注意,把這三部分使用點(.)連接起來,就是一個JWT字符串

image-20220118121226620

1)頭部

header一般的由兩部分組成:token的類型(“JWT”)和算法名稱(比如:HMAC SHA256或者RSA等等)。 JWT裏驗證和簽名使用的算法列錶如下:

JWS 算法名稱
HS256 HMAC256
HS384 HMAC384
HS512 HMAC512
RS256 RSA256
RS384 RSA384
RS512 RSA512
ES256 ECDSA256
ES384 ECDSA384
ES512 ECDSA512

例如,

{
    
"typ": "JWT",
"alg": "HS256"
}

2)載荷

payload主要用來包含聲明(claims ),這個聲明一般是關於實體(通常是用戶)和其他數據的聲明。 聲明有三種類型:

  • registered
  • public
  • private

具體如下:

Registered claims : 這裏有一組預定義的聲明,它們不是强制的,但是推薦。

iss: jwt簽發者

sub: jwt所面向的用戶

aud: 接收jwt的一方

exp: jwt的過期時間,這個過期時間必須要大於簽發時間

nbf: 定義在什麼時間之前,該jwt都是不可用的

iat: jwt的簽發時間

jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊

Public claims : 可以隨意定義

  • 自定義數據:存放在token中存放的key-value值

Private claims : 用於在同意使用它們的各方之間共享信息,並且不是注册的或公開的聲明

例如:

{
    
"iss": "sxau",
"iat": 1446593502,
"exp": 1446594722,
"aud": "sxau.edu.com",
"sub": "[email protected]",
"username": "admin"
}

注意,不要在JWT的payload或header中放置敏感信息,除非它們是加密的

把頭部和載荷分別進行Base64編碼之後得到兩個字符串,然後再將這兩個編碼後的字符串用英文句號 . 連接在一起(頭部在前),形成新的字符串:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmI2OWNlZC02YWNlLTRmYzAtOTk5MS00Y.WUwMjIxODQ0OTciLCJleHAiOjE2MDYwNTQzNjl9

3)簽名

最後,將上面拼接完的字符串用HS256算法進行加密,在加密的時候,還需要提供一個密鑰(secret)。 加密後的內容也是一個字符串,這個字符串就是簽名

把這個簽名拼接在剛才的字符串後面就能得到完整的JWT字符串。 header部分和payload部分如果被篡改,由於篡改者不知道密鑰是什麼,也無法生成新的signature部分, 服務端也就無法通過。 在JWT中,消息體是透明的,使用簽名可以保證消息不被篡改。 例如,使用HMACSHA256加密算法,配合秘鑰,將前倆部進行加密,生成簽名

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

例如,將Header、Payload、Signature三部分使用點(.)連接起來

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmI2OWNlZC02YWNlLTRmYzAtOTk5MS00Y WUwMjIxODQ0OTciLCJleHAiOjE2MDYwNTQzNjl9.DNVhr36j66JpQBfcYoo64IRp84dKiQeaq7axHTBcP9 E

例如,使用官網提供的工具,可以對該JWT進行驗證和解析

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-p1WcHh3A-1642675440896)(https://gitee.com/z6135/cloudimage/raw/master/img/image-20220118124647526.png)]

注意,在代碼中,我們使用JWT封裝的工具類,也可以完成此操作

4、使用

在springboot中可以很容易的使用JWT,只需要引入相關依賴,封裝一個JWT的工具類,並且編寫 Controller的攔截器,對指定路徑進行攔截驗證token即可。

1)新建項目springboot-jwt

image-20220118125813591

2)pom文件中,引入操作jwt相關依賴

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zmz</groupId>
    <artifactId>springboot-jjwt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>springboot-jjwt</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

3)Controller層

package com.zmz.springbootjjwt.api;

import com.alibaba.fastjson.JSONObject;
import com.zmz.springbootjjwt.annotation.UserLoginToken;
import com.zmz.springbootjjwt.entity.User;
import com.zmz.springbootjjwt.service.TokenService;
import com.zmz.springbootjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;

/** * @author zhangshengrui * @date 2022-01-06 20:45 */
@Controller
@RequestMapping("api")
public class UserApi {
    
    @Autowired
    UserService userService;
    @Autowired
    TokenService tokenService;

    @GetMapping("/loginto")
    public String loginto(String username,String password ,ModelMap map){
    
        map.addAttribute("name","ceshi");
        System.out.println(username+" "+password);
        return "login";
    }
    //登錄
    @ResponseBody
    @PostMapping("/login")
    public Object login(@RequestBody User user){
    
        System.out.println(user);
        JSONObject jsonObject=new JSONObject();
        User userForBase=userService.findByUsername(user);
        if(userForBase==null){
    
            jsonObject.put("message","登錄失敗,用戶不存在");
            return jsonObject;
        }else {
    
            if (!userForBase.getPassword().equals(user.getPassword())){
    
                jsonObject.put("message","登錄失敗,密碼錯誤");
                return jsonObject;
            }else {
    
                String token = tokenService.getToken(userForBase);
                jsonObject.put("token", token);
                jsonObject.put("user", userForBase);
                return jsonObject;
            }
        }
    }

    @ResponseBody
    @UserLoginToken
    @GetMapping("/getMessage")
    public String getMessage(){
    
        return "你已通過驗證";
    }
}
  1. 攔截器
package com.zmz.springbootjjwt.interceptor;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.zmz.springbootjjwt.annotation.PassToken;
import com.zmz.springbootjjwt.annotation.UserLoginToken;
import com.zmz.springbootjjwt.entity.User;
import com.zmz.springbootjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;


/** * @author zhangshengrui * @date 2022-01-06 20:41 */
public class AuthenticationInterceptor implements HandlerInterceptor {
    
    @Autowired
    UserService userService;
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
    
        String token = httpServletRequest.getHeader("token");// 從 http 請求頭中取出 token
        // 如果不是映射到方法直接通過
        if(!(object instanceof HandlerMethod)){
    
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        Method method=handlerMethod.getMethod();
        //檢查是否有passtoken注釋,有則跳過認證
        if (method.isAnnotationPresent(PassToken.class)) {
    
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
    
                return true;
            }
        }
        //檢查有沒有需要用戶權限的注解
        if (method.isAnnotationPresent(UserLoginToken.class)) {
    
            UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
            if (userLoginToken.required()) {
    
                // 執行認證
                if (token == null) {
    
                    throw new RuntimeException("無token,請重新登錄");
                }
                // 獲取 token 中的 user id
                String userId;
                try {
    
                    userId = JWT.decode(token).getAudience().get(0);
                } catch (JWTDecodeException j) {
    
                    throw new RuntimeException("401");
                }
                User user = userService.findUserById(userId);
                if (user == null) {
    
                    throw new RuntimeException("用戶不存在,請重新登錄");
                }
                // 驗證 token
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
                try {
    
                    jwtVerifier.verify(token);
                } catch (JWTVerificationException e) {
    
                    throw new RuntimeException("401");
                }
                return true;
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    

    }
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    

    }
}

5)全局异常處理GloablExceptionHandler.java

package com.zmz.springbootjjwt.interceptor;

import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/** * @author zhangshengrui * @date 2022-01-06 22:37 */
@ControllerAdvice
public class GloablExceptionHandler {
    
    @ResponseBody
    @ExceptionHandler(Exception.class)
    public Object handleException(Exception e) {
    
        String msg = e.getMessage();
        if (msg == null || msg.equals("")) {
    
            msg = "服務器出錯";
        }
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("message", msg);
        return jsonObject;
    }
}

6)javaconfig配置類

package com.zmz.springbootjjwt.config;

import com.zmz.springbootjjwt.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/** * @author zhangshengrui * @date 2022-01-06 22:33 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
        registry.addInterceptor(authenticationInterceptor())
                .addPathPatterns("/**")// 攔截所有請求,通過判斷是否有 @LoginRequired 注解 决定是否需要登錄
                .excludePathPatterns("/loginto","/static/**");
    }
    @Bean
    public AuthenticationInterceptor authenticationInterceptor() {
    
        return new AuthenticationInterceptor();
    }
}

7)啟動項目,直接訪問http://localhost:8888/api/login

image-20220118131642909

8)登錄成功拿到Token之後訪問http://localhost:8888/api/getMessage

image-20220118132230411

不攜帶token

image-20220118132253366

注意:這裏的key一定不能錯,因為在攔截器中是取關鍵字token的值String token = httpServletRequest.getHeader("token");加上token之後就可以順利通過驗證和進行接口訪問了

源碼地址:[廠長Github源碼地址](https://github.com/z6135/springboot-jwt)

版權聲明
本文為[java廠長]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/01/202201280225540999.html

隨機推薦