一篇让你精通JWT,妥妥的避坑指南~

news2024/11/22 1:24:53

视频教程传送门:JWT 两小时极简入门:JWT实战应用与防坑指南~_哔哩哔哩_bilibiliJWT 两小时极简入门:JWT实战应用与防坑指南~共计12条视频,包括:01.课程介绍与前置知识点、02.JWT概念、03.JWT组成等,UP主更多精彩视频,请关注UP账号。https://www.bilibili.com/video/BV1gk4y177DS/?vd_source=aced8a6167c51fbe68b3d65b157734eb

一、前置知识点

本课程对JWT讲解涉及到以下相关知识点,需要提前准备:

  • Java Web

  • Spring/SpringMVC/SpringBoot

  • Spring Security

二、JWT介绍

2.1 概念

官网:JSON Web Tokens - jwt.io

整理一下:

JSON Web Token,简称 JWT,读音是 [dʒɒt]( jot 的发音),是一个基于 RFC 7519 的开放数据标准,它定义了一种宽松且紧凑的数据组合方式。其作用是:JWT是一种加密后数据载体,可在各应用之间进行数据传输

JWT中一般涵盖了用户身份信息,每次访问时,server校验信息合法性即可。

2.2 JWT组成

一个 JWT 通常有 HEADER (头),PAYLOAD (有效载荷)和 SIGNATURE (签名)三个部分组成,三者之间使用“.”链接,格式如下:

header.payload.signature

 

一个简单的JWT案例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   //header
.eyJ1c2VyX2luZm8iOlt7ImlkIjoiMSJ9LHsibmFtZSI6ImRhZmVpIn0seyJhZ2UiOiIxOCJ9XSwiaWF0IjoxNjgxNTcxMjU3LCJleHAiOjE2ODI3ODM5OTksImF1ZCI6InhpYW9mZWkiLCJpc3MiOiJkYWZlaSIsInN1YiI6ImFsbHVzZXIifQ  //payload
.v1TxJ0mngnVx4t9O3uibAHPSLUyMM7sUM06w8ODYjuE //signature

 注意三者之间有一个点号(“.”)相连

2.2.1 Header组成

JWT的头部承载两部分信息:

  • 声明类型,默认是JWT

  • 声明加密的算法 常用的算法:HMAC 、RSA、ECDSA等

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

alg:表示签名的算法,默认是 HMAC SHA256(写成 HS256);

typ: 表示令牌(token)的类型,JWT 令牌统一写为 JWT

使用Base64加密,构成了JWT第一部分-header:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2.2.2 Payload组成

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的有效信息。

标准载荷:有很多,建议使用,但不强制,对JWT信息作补充。

标准载荷介绍
iss (issuer)签发人(谁签发的)
exp (expiration time)过期时间,必须要大于签发时间
sub (subject)主题(用来做什么)
aud (audience)受众(给谁用的)比如:http://www.xxx.com
nbf (Not Before)生效时间
iat (Issued At)签发时间
jti (JWT ID)编号,JWT 的唯一身份标识

自定义载荷:可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。

{
    "user_info": [
      {
        "id": "1"
      },
      {
        "name": "dafei"
      },
      {
        "age": "18"
      }
    ],
    "iat": 1681571257,
    "exp": 1682783999,
    "aud": "xiaofei",
    "iss": "dafei",
    "sub": "alluser"
}

 使用Base64加密,构成了JWT第二部分-payload:

eyJ1c2VyX2luZm8iOlt7ImlkIjoiMSJ9LHsibmFtZSI6ImRhZmVpIn0seyJhZ2UiOiIxOCJ9XSwiaWF0IjoxNjgxNTcxMjU3LCJleHAiOjE2ODI3ODM5OTksImF1ZCI6InhpYW9mZWkiLCJpc3MiOiJkYWZlaSIsInN1YiI6ImFsbHVzZXIifQ

2.2.3 signature组成

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

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

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

因为有这个密钥的存在,所以即便调用方偷偷的修改了前两部分的内容,在验证环节就会出现签名不一致的情况,所以保证了安全性。

使用Base64加密,构成了JWT第三部分-signature:

l6JdYARw4IHmjliSbh9NP6ji1L15qVneWTJU5noQ-k8

2.3 在线生成/解析JWT

2.3.1 编码工具

地址:JWT Token在线编码生成 - ToolTT在线工具箱

2.3.2 解码工具

地址:JWT在线解码 - 开发工具箱

三、JWT特点

3.1 无状态

JWT 不需要在服务端存储任何状态,客户端可以携带 JWT 来访问服务端,从而使服务端变得无状态。这样,服务端就可以更轻松地实现扩展和负载均衡。

3.2 可自定义

JWT 的载荷部分可以自定义,可以存储任何 JSON 格式的数据。这意味着我们可以使用 JWT 来实现一些自定义的功能,例如存储用户喜好、配置信息等等。

3.3 扩展性强

JWT 有一套标准规范,因此很容易在不同平台和语言之间共享和解析。此外,开发人员可以根据需要自定义声明(claims)来实现更加灵活的功能。

3.4 调试性好

由于 JWT 的内容是以 Base64 编码后的字符串形式存在的,因此非常容易进行调试和分析。

3.5 安全性取决于密钥管理

JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。因此,在使用 JWT 时,一定要注意密钥的管理,包括生成、存储、更新、分发等等。

3.6 无法撤销

由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。如果用户在使用 JWT 认证期间被注销或禁用,那么服务端就无法阻止该用户继续使用之前签发的 JWT。因此,开发人员需要设计额外的机制来撤销 JWT,例如使用黑名单或者设置短期有效期等等。

3.7 需要缓存到客户端

由于 JWT 包含了用户信息和授权信息,一般需要客户端缓存,这意味着 JWT 有被窃取的风险。

3.8 载荷大小有限制

由于 JWT 需要传输到客户端,因此载荷大小也有限制。一般不建议载荷超过 1KB,会影响性能。

四、JWT优缺点

4.1 优点

  • 无状态:JWT 本身不需要存储在服务器上,因此可以实现无状态的身份验证和授权。

  • 可扩展性:JWT 的载荷可以自定义,因此可以根据需求添加任意信息。

  • 可靠性:JWT 使用数字签名来保证安全性,因此具有可靠性。

  • 跨平台性:JWT 支持多种编程语言和操作系统,因此具有跨平台性。

  • 高效性:由于 JWT 不需要查询数据库,因此具有高效性。

4.2 缺点

  • 安全性取决于密钥管理:JWT 的安全性取决于密钥的管理,如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。

  • 无法撤销令牌:由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。

  • 需要传输到客户端:由于 JWT 包含了用户信息和授权信息,因此 JWT 需要传输到客户端,这意味着 JWT 有被攻击者窃取的风险。

  • 载荷大小有限制:由于 JWT 需要传输到客户端,因此载荷大小也有限制。

五、JWT应用场景

5.1 一次性验证

用户注册成功后发一份激活邮件或者其他业务需要邮箱激活操作,都是可以使用jwt。

原因:

JWT时效性:让该链接具有时效性(比如约定2小时内激活),

JWT不可篡改性:防止篡改以激活其他账户

5.2 RESTful API 的无状态认证

使用 JWT 来做 RESTful api 的身份凭证:当用户身份校验成功,客户端每次接口访问都带上JWT,服务端校验JWT合法性(是否篡改/是否过期等)

5.3 信息交换

JWT是在各方(项目间/服务间)之间安全传输信息的好方式。 因为JWT可以签名:例如使用公钥/私钥对,所以可以确定发件人是他们自称的人。 此外,由于使用标头和有效载荷计算签名,因此您还可以验证内容是否未被篡改。

5.4 JWT令牌登录

JWT 令牌登录也是一种应用场景,但也是JWT被诟病最多的地方,因为JWT令牌存在各种不安全。

1>JWT令牌存储与客户端,容易泄露并被伪造身份搞破坏。

2>JWT 被签发,就无法撤销,当破坏在进行时,后端无法马上禁止。

上面问题可通过监控异常JWT访问,设置黑名单 + 强制下线等方式尽量避免损失。

六、JWT实战案例

6.1 案例1:邮件激活

6.1.1 需求与分析

需求:当用户注册成功,给指定邮箱发送一个激活链接,当用户点击激活链接后,激活账号。

分析:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.2.RELEASE</version>
    <relativePath/>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <scope>compile</scope>
    </dependency>

    <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>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId> 
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

1>设计一个注册接口/regist,请求成功后模拟下发激活链接(链接本质是激活接口参数为:jwt)

2>设计激活接口/active,接收参数为jwt

6.1.2 代码实现

步骤1:创建项目:mail-active-demo

步骤2:导入相关依赖

具体选哪个JWT工具包,可以看官网推荐:JSON Web Token Libraries - jwt.io

此处选择:jjwt

 

步骤3:代码编写

用户注册实体类

package com.langfeiyes.mail.entity;

public class User {
    private Long id;
    private String username;
    private String password;
    private int state;


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }
}

JWT常量类:

package com.langfeiyes.mail.util;

/**
 * 常量类
 */
public class JwtConstant {

    // 基本url
    public static final String BASE_DOMAIN_URL = "http://localhost:8080/";

    //jwt密码
    public static final String JWT_SECRET = "langfeiyesabcdefghijklmnopqrstuvwxyz11111111111";
    //jwt失效时间,单位秒
    public static final Long JWT_EXPIRATION = 24 * 60 * 60 * 1000L;
    //jwt 创建时间
    public static final String JWT_CREATE_TIME = "jwt_create_time";

    //jwt 用户信息-key
    public static final String USER_INFO_KEY = "user_info_key";
    //jwt 用户信息-id
    public static final String USER_INFO_ID = "user_info_id";
    //jwt 用户信息-username
    public static final String USER_INFO_USERNAME = "user_info_username";

}

JWT工具类:

package com.langfeiyes.mail.util;

import com.langfeiyes.mail.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken工具类
 */
public class JwtTokenUtil {


    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    public static String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
                //.setHeader(new HashMap<>())
                //.setAudience("Audience")
                //.setIssuer("Issuer")
                //.setSubject("Subject")
                //.setNotBefore(new Date())
                //.setIssuedAt(new Date())
                //.setId("jwt id")
                .setClaims(claims)//把荷载存储到里面
                .setExpiration(generateExpirationDate())//设置失效时间
                .signWith(Keys.hmacShaKeyFor(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))) //签名
                .compact();
        return token;
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    public static Claims parseToken(String token){
        Claims claims=null;
        try{
            claims = Jwts.parserBuilder()
                    .setSigningKey(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 生成token失效时间
     */
    private static Date generateExpirationDate() {
        //失效时间是当前系统的时间+我们在配置文件里定义的时间
        return new Date(System.currentTimeMillis()+JwtConstant.JWT_EXPIRATION);
    }

    /**
     * 根据token获取用户名
     */
    public static String getUserName(String token){
        Claims claims = parseToken(token);
        return getValue(claims, JwtConstant.USER_INFO_USERNAME);
    }

    /**
     * 验证token是否有效
     */
    public static boolean validateToken(String token){
        //claims 为null 意味着要门jwt被修改
        Claims claims = parseToken(token);
        return claims != null &&!isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     * @param token
     * @return
     */
    public static boolean isTokenExpired(String token) {
        //先获取之前设置的token的失效时间
        Date expireDate=getExpiredDate(token);
        return expireDate.before(new Date()); //判断下,当前时间是都已经在expireDate之后
    }

    /**
     * 根据token获取失效时间
     * 也是先从token中获取荷载
     * 然后从荷载中拿到到设置的失效时间
     * @param token
     * @return
     */
    private static Date getExpiredDate(String token) {
        Claims claims=parseToken(token);
        return claims.getExpiration();
    }


    /**
     * 刷新我们的token:重新构建jwt
     */
    public static String refreshToken(String token){
        Claims claims=parseToken(token);
        claims.put(JwtConstant.JWT_CREATE_TIME,new Date());
        return createToken(claims);
    }

    /**
     * 根据身份信息获取键值
     *
     * @param claims 身份信息
     * @param key 键
     * @return 值
     */
    public static String getValue(Claims claims, String key){
        return claims.get(key) != null ? claims.get(key).toString():null;
    }
}

接口:

package com.langfeiyes.mail.controller;

import com.langfeiyes.mail.util.JwtConstant;
import com.langfeiyes.mail.entity.User;
import com.langfeiyes.mail.util.JwtTokenUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

@RestController
public class UserController {
    //模拟需要缓存的用户库
    //key: url, value: 要激活用户
    private static Map<String, User> map = new HashMap<>();

    @GetMapping("/regist")
    public String regist(User user){
        //假装成功
        System.out.println("注册成功");
        user.setId(new Random().nextLong());
        //创建jwt
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtConstant.USER_INFO_ID, user.getId());
        claims.put(JwtConstant.USER_INFO_USERNAME, user.getUsername());
        claims.put(JwtConstant.JWT_CREATE_TIME, new Date());
        String jwt = JwtTokenUtil.createToken(claims);

        //缓存jwt
        map.put(jwt, user);
        return JwtConstant.BASE_DOMAIN_URL + "/active?jwt=" + jwt;
    }


    @GetMapping("/active")
    public String active(String jwt){
        User user = map.get(jwt);
        if(user != null && JwtTokenUtil.validateToken(jwt)){
            map.remove(jwt);
            return "执行激活逻辑...";
        }else{
            return "参数不合法...";
        }
    }
}

步骤4:启动项目

package com.langfeiyes.mail;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

步骤5:测试

浏览器发起2个请求:

注册

http://localhost:8080/regist?username=dafei&password=666

激活

http://localhost:8080//active?jwt=eyJhbGciOiJIUzUxMiJ9.eyJjcmVhdGVfdGltZSI6MTY4MTYxNzA2MDg3NSwiaWQiOjEsInVzZXJuYW1lIjoiZGFmZWkiLCJleHAiOjE2ODE3MDM0NjB9.vQcsXUaEictz3QgjUBKwAV1qlou9yFCSMo4H6OaArz1ReEFzXt6klziHqonvsEfkv9aYdDc6G-vKVO9Zh1kcXw

6.2 案例2:JWT令牌登录

6.2.1 需求与分析

需求:设计/login 与 /list 2个接口实现登录与列表逻辑,注意访问/list接口必须进行登录校验

要求:使用Spring security + JWT

分析

1>设计2个接口,/login登录成功创建JWT响应到客户端

2>设计登录检查拦截器,当访问/list接口时进行登录拦截

代码设计

 

6.2.2 代码实现

步骤1:创建项目:security-jwt-demo

步骤2:导入相关依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.2.RELEASE</version>
    <relativePath/>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <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>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

 

步骤3:编写代码

实体类:User--登录主体

package com.langfeiyes.jwt.entity;

public class User {
    private Long id;
    private String username;
    private String password;
    private int state;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }
}

常量类:JwtConstant--配置jwt密码,过期时间

package com.langfeiyes.jwt.util;

/**
 * 常量类
 */
public class JwtConstant {

    // 基本url
    public static final String BASE_DOMAIN_URL = "http://localhost:8080/";

    //jwt密码
    public static final String JWT_SECRET = "langfeiyesabcdefghijklmnopqrstuvwxyz11111111111";
    //jwt失效时间,单位秒
    public static final Long JWT_EXPIRATION = 24 * 60 * 60 * 1000L;
    //jwt 创建时间
    public static final String JWT_CREATE_TIME = "jwt_create_time";

    //jwt 用户信息-key
    public static final String USER_INFO_KEY = "user_info_key";
    //jwt 用户信息-id
    public static final String USER_INFO_ID = "user_info_id";
    //jwt 用户信息-username
    public static final String USER_INFO_USERNAME = "user_info_username";

}

工具类-JwtTokenUtil--JWT方法操作

package com.langfeiyes.jwt.util;

import com.langfeiyes.jwt.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken工具类
 */
public class JwtTokenUtil {


    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    public static String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
                //.setHeader(new HashMap<>())
                //.setAudience("Audience")
                //.setIssuer("Issuer")
                //.setSubject("Subject")
                //.setNotBefore(new Date())
                //.setIssuedAt(new Date())
                //.setId("jwt id")
                .setClaims(claims)//把荷载存储到里面
                .setExpiration(generateExpirationDate())//设置失效时间
                .signWith(Keys.hmacShaKeyFor(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))) //签名
                .compact();
        return token;
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    public static Claims parseToken(String token){
        Claims claims=null;
        try{
            claims = Jwts.parserBuilder()
                    .setSigningKey(Decoders.BASE64.decode(JwtConstant.JWT_SECRET))
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 生成token失效时间
     */
    private static Date generateExpirationDate() {
        //失效时间是当前系统的时间+我们在配置文件里定义的时间
        return new Date(System.currentTimeMillis()+JwtConstant.JWT_EXPIRATION);
    }

    /**
     * 根据token获取用户名
     */
    public static String getUserName(String token){
        Claims claims = parseToken(token);
        return getValue(claims, JwtConstant.USER_INFO_USERNAME);
    }

    /**
     * 验证token是否有效
     */
    public static boolean validateToken(String token){
        //claims 为null 意味着要门jwt被修改
        Claims claims = parseToken(token);
        return claims != null &&!isTokenExpired(token);
    }

    /**
     * 判断token是否已经失效
     * @param token
     * @return
     */
    public static boolean isTokenExpired(String token) {
        //先获取之前设置的token的失效时间
        Date expireDate=getExpiredDate(token);
        return expireDate.before(new Date()); //判断下,当前时间是都已经在expireDate之后
    }

    /**
     * 根据token获取失效时间
     * 也是先从token中获取荷载
     * 然后从荷载中拿到到设置的失效时间
     * @param token
     * @return
     */
    private static Date getExpiredDate(String token) {
        Claims claims=parseToken(token);
        return claims.getExpiration();
    }


    /**
     * 刷新我们的token:重新构建jwt
     */
    public static String refreshToken(String token){
        Claims claims=parseToken(token);
        claims.put(JwtConstant.JWT_CREATE_TIME,new Date());
        return createToken(claims);
    }

    /**
     * 根据身份信息获取键值
     *
     * @param claims 身份信息
     * @param key 键
     * @return 值
     */
    public static String getValue(Claims claims, String key){
        return claims.get(key) != null ? claims.get(key).toString():null;
    }
}

接口响应类-R

package com.langfeiyes.jwt.util;

public class R {
    private int code;
    private String msg;
    private Object data;

    public R() {
    }

    public R(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static R ok(){
        return new R(200, "操作成功", null);
    }
    public static R ok(Object data){
        return new R(200, "操作成功", data);
    }
    public static R fail(){
        return new R(500, "操作失败", null);
    }
    public static R fail(Object data){
        return new R(500, "操作失败", data);
    }
    public static R fail(String msg){
        return new R(500, msg, null);
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

    public Object getData() {
        return data;
    }
}

登录过滤器:JwtLoginFilter--做jwt登录

package com.langfeiyes.jwt.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.langfeiyes.jwt.util.JwtConstant;
import com.langfeiyes.jwt.util.JwtTokenUtil;
import com.langfeiyes.jwt.util.R;
import org.springframework.security.core.Authentication;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
    private AuthenticationManager authenticationManager;
    public JwtLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;

        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "GET"));
    }



    /**
     * 拦截登录。获取表单的用户名与密码
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //使用 请求体 传递登录参数,更加安全
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
    }

    /**
     * 登录成功后调用的方法
     * 返回token
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {

        //security 登录成功封装实体验证
        UserDetails userDetails = (UserDetails) authResult.getPrincipal();
        //根据用户名生成token
        //创建jwt
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtConstant.USER_INFO_USERNAME, userDetails.getUsername());
        claims.put(JwtConstant.JWT_CREATE_TIME, new Date());
        String jwt = JwtTokenUtil.createToken(claims);

        String token = JwtTokenUtil.createToken(claims);
        response.setHeader("token", token);
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().print(new ObjectMapper().writeValueAsString(R.ok("登录成功")));
    }

    /**
     * 登录失败后调用的方法
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().print(new ObjectMapper().writeValueAsString(R.fail(failed.getMessage())));
    }
}

登录拦截过滤器-JwtAuthFilter--登录检查--权限检查

package com.langfeiyes.jwt.filter;

import com.langfeiyes.jwt.util.JwtTokenUtil;
import io.jsonwebtoken.Jwt;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;

public class JwtAuthFilter extends BasicAuthenticationFilter {
    public JwtAuthFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }
    /**
     * 对HTTP请求头做处理
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        //授权
        UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
        //授权失败
        if (authRequest == null) {
            chain.doFilter(request, response);
            return;
        }
        //如果有授权,放到权限上下文(容器)中
        SecurityContextHolder.getContext().setAuthentication(authRequest);
        chain.doFilter(request, response);
    }

    /**
     * 认证token是否合法,若合法,返回认证,否则返回null
     */
    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        //获取token
        String token = request.getHeader("token");

        if (!StringUtils.isEmpty(token) && JwtTokenUtil.validateToken(token)) {
            //从token中获取username
            String username = JwtTokenUtil.getUserName(token);

            return new UsernamePasswordAuthenticationToken(username, token, Arrays.asList(new SimpleGrantedAuthority("admin")));
        }
        return null;
    }
}

security整体配置类-JwtWebSecurityConfig

package com.langfeiyes.jwt.config;

import com.langfeiyes.jwt.filter.JwtAuthFilter;
import com.langfeiyes.jwt.filter.JwtLoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class JwtWebSecurityConfig extends WebSecurityConfigurerAdapter {



    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("dafei")
                .password("666")
                .roles("admin")
                .and()
                .passwordEncoder(NoOpPasswordEncoder.getInstance());

    }



    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .exceptionHandling()

                .and().authorizeRequests()
                .antMatchers("/login").permitAll()			//所有请求都可以访问
                .antMatchers("/list").authenticated()
                .and().logout()
                .and()
                .addFilter(new JwtLoginFilter(authenticationManager()))	//登录时的过滤器
                .addFilter(new JwtAuthFilter(authenticationManager()))	//验证JWT的过滤器
                .httpBasic();
    }

}

访问接口-UserController

package com.langfeiyes.jwt.controller;

import com.langfeiyes.jwt.util.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @GetMapping("/list")
    public R list(){
        return R.ok("list....");
    }
}

步骤4:启动项目

package com.langfeiyes.jwt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

步骤5:测试

GET http://localhost:8080/login?username=dafei&password=666

<> 2023-04-16T230507.200.json
    
###
GET http://localhost:8080/list
Accept: */*
token: eyJhbGciOiJIUzUxMiJ9.eyJjcmVhdGVfdGltZSI6MTY4MTY1NzUwNzE1NCwidXNlcm5hbWUiOiJkYWZlaSIsImV4cCI6MTY4MTc0MzkwN30.q8X7IisUgF8if299exu1jU-0hgZOFzgUABt9SynqQ2HdyVJJqfAZpywVmyvRLQ8-n5hLf-JtF2mjAbQBlfQZwg

<> 2023-04-16T231623.200.json

七、JWT避坑指南

7.1 Redis校验实现令牌泄露保护

因为JWT是无状态的,当JWT令牌颁发后,在有效时间内,是无法进行销毁,所以就存在很大隐患:令牌泄露

避坑:颁发JWT令牌时,在Redis中也缓存一份,当判定某个JWT泄露了,立即移除Redis中的JWT。当接口发起请求时,强制用户重新进行身份验证,直至验证成功。

7.2 临检JWT限制敏感操作

基于JWT无状态性,同时泄露可能很大,一些涉及到敏感数据变动,执行临检操作。

避坑:在涉及到诸如新增,修改,删除,上传,下载等敏感性操作时,强制检查用户身份,如手机验证码,扫描二维码等手段,确认操作者是用户本人。

7.3 异常JWT监控:超频识别与限制

当JWT令牌被盗取,一般会出现高频次的系统访问。针对这种情况,监控用户端在单位时间内的请求次数,当单位时间内的请求次数超出预定阈值值,则判定该用户JWT令牌异常。

避坑:当判断JWT令牌异常,直接进行限制(比如:IP限流,JWT黑名单等)。

7.4 地域检查杜绝JWT泄露可能

一般用户活动范围是固定,意味着JWT客户端访问IP相对固定,JWT泄露之后,可能会先异地登录的情况。

避坑:对JWT进行异地访问检查,有效时间内,IP频繁变动可判断为JWT泄露。

7.5 客户端区分检查防止JWT泄露

对于APP产品来说,一般客户端是固定的,基本为移动设备(APP,平板),可以结合设备机器码进行绑定。

避坑:将JWT与机器码绑定,存储与服务端,当客户端发起请求时,通过检查客户端的机器码与服务端机器码是否匹配判断JWT是否泄露。

7.6 JWT令牌保护:限时,限数,限频

JWT令牌泄露是无法避免,但是我们可以进行泄露识别,做好泄露后补救保证系统安全。

避坑:对客户端进行合理限制,比如限制每个客户端的 JWT 令牌数量、访问频率、JWT令牌时效等,以降低 JWT 令牌泄露的风险。

八、常见的面试题

问:什么是JWT?解释一下它的结构。

JWT是一种开放标准,用于在网络上安全地传输信息。它由三部分组成:头部、载荷和签名。头部包含令牌的元数据,载荷包含实际的信息(例如用户ID、角色等),签名用于验证令牌是否被篡改。

问:JWT的优点是什么?它与传统的session-based身份验证相比有什么优缺点?

JWT的优点包括无状态、可扩展、跨语言、易于实现和良好的安全性。相比之下,传统的session-based身份验证需要在服务端维护会话状态,使得服务端的负载更高,并且不适用于分布式系统。

问:在JWT的结构中,分别有哪些部分?每个部分的作用是什么?

JWT的结构由三部分组成:头部、载荷和签名。头部包含令牌类型和算法,载荷包含实际的信息,签名由头部、载荷和密钥生成。

问:JWT如何工作?从开始到验证过程的完整流程是怎样的?

JWT的工作流程分为三个步骤:生成令牌、发送令牌、验证令牌。在生成令牌时,服务端使用密钥对头部和载荷进行签名。在发送令牌时,将令牌发送给客户端。在验证令牌时,客户端从令牌中解析出头部和载荷,并使用相同的密钥验证签名。

问:什么是JWT的签名?为什么需要对JWT进行签名?如何验证JWT的签名?

JWT的签名是由头部、载荷和密钥生成的,用于验证令牌是否被篡改。签名使用HMAC算法或RSA算法生成。在验证JWT的签名时,客户端使用相同的密钥和算法生成签名,并将生成的签名与令牌中的签名进行比较。

问:什么是JWT的令牌刷新?为什么需要这个功能?

令牌刷新是一种机制,用于解决JWT过期后需要重新登录的问题。在令牌刷新中,服务端生成新的JWT,并将其发送给客户端。客户端使用新的JWT替换旧的JWT,从而延长令牌的有效期。

问:JWT是否加密?如果是,加密的部分是哪些?如果不是,那么它如何保证数据安全性?

JWT本身并不加密,但可以在载荷中包含敏感信息。为了保护这些信息,可以使用JWE(JSON Web Encryption)对载荷进行加密。如果不加密,则需要在生成JWT时确保不在载荷中包含敏感信息。

问:在JWT中,如何处理Token过期的问题?有哪些方法可以处理?

JWT过期后,客户端需要重新获取新的JWT。可以通过在JWT中包含过期时间或使用refresh token等机制来解决过期问题。

问:JWT和OAuth2有什么关系?它们之间有什么区别?

JWT和OAuth2都是用于身份验证和授权的开放标准。JWT是一种身份验证机制,而OAuth2是一种授权机制。JWT用于在不同的系统中安全地传输信息,OAuth2用于授权第三方应用程序访问受保护的资源。

问:JWT在什么场景下使用较为合适?它的局限性是什么?

JWT在单体应用或微服务架构中的使用比较合适。它的局限性包括无法撤销、令牌较大、无法处理并发等问题。在需要针对每次请求进行访问控制或需要撤销令牌的情况下,JWT可能不是最佳选择。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/519351.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一个例子让你彻底弄懂分布式系统的CAP理论

1 推荐的文章 下面这篇知乎文章是我见过的最简单易懂的一篇&#xff0c;把CAP定义以及为什么AP和CP只能二选一以及场景特定下选AP还是CP作为系统目标等讲解明明白白 谈谈分布式系统的CAP 2 个人对上面这篇文章的的一些补充 可用性可以人为设置一个阈值&#xff0c;比如用户体…

openPOWERLINK源码(最新)在stm32单片机上的移植指南

最近着了powerlink的道&#xff0c;连续几晚十二点前没睡过觉。不得不说兴趣这东西劲太大了&#xff0c;让人睡不着。喜欢上研究POWERLINK&#xff0c;最新版的源码结构挺清晰的&#xff0c;移植并测试了嵌入式linux作为从站和电脑主站之间的通信&#xff0c;挺有趣的。接下来想…

路由器配置方法(固定地址)

前言 由于学校给分配了IP地址&#xff0c;因此我们的路由器接入的时候不能选择自动接入方式&#xff0c;而要选择固定地址方式。 step 1 我们首先先将路由器接上网线&#xff0c;这里注意一定要接wan口 因为路由器分为两个口&#xff0c;wan口是入口&#xff0c;lan口是出口…

第十二届蓝桥杯青少组国赛Python真题,包含答案

第十二届蓝桥杯青少组国赛Python真题 一、选择题 第 1 题 单选题 设sHi LanQiao&#xff0c;运行以下哪个选项代码可以输出“LanQiao”子串 () 答案&#xff1a;A 第 2 题 单选题 已知a-2021.0529&#xff0c;运行以下哪个选项代码可以输出“2021.05”() 答案&#xff1a;…

2023.05.12 C高级 day4

有m1.txt m2.txt m3.txt m4.txt&#xff0c;分别创建出对应的目录&#xff0c;m1 m2 m3 m4 并把文件移动到对应的目录下 #!/bin/bash for i in 1 2 3 4 dotouch m$i.txtmkdir m$imv m$i.txt ./m$i/m$i.txt done 运行结果 2. 使用break关键字打印九九乘法表&#xff0c;提示&am…

【2023/05/12】Z3

Hello&#xff01;大家好&#xff0c;我是霜淮子&#xff0c;2023倒计时第7天。 Share Listen,my heart,to the whispers of the world with which it makes love to you. 译文&#xff1a; 静静的听&#xff0c;我的心呀&#xff0c;听那世界的低语&#xff0c;这是它对你求…

黑客必备工具:Hydra的完整安装和使用指南

安装Hydra 1.安装必要的依赖库 在终端中执行以下命令&#xff0c;安装Hydra所需的依赖库&#xff1a; sudo apt-get install build-essential checkinstall libssl-dev libssh-dev libidn11-dev libpcre3-dev libgtk2.0-dev libmysqlclient-dev libpq-dev libsvn-dev firebi…

经典HTML前端面试题总结

经典HTML前端面试题总结 1. 1简述一下你对 HTML 语义化的理解&#xff1f;.1.2 标签上 title 与 alt 属性的区别是什么&#xff1f;1.3 iframe的优缺点&#xff1f;1.4 href 与 src&#xff1f;1.5 HTML、XHTML、XML有什么区别1.6 知道img的srcset的作用是什么&#xff1f;1.7 …

代码随想录算法训练营第五十九天

代码随想录算法训练营第五十九天| 503.下一个更大元素II&#xff0c;42. 接雨水 503.下一个更大元素II42. 接雨水复杂单调栈整合单调栈 503.下一个更大元素II 题目链接&#xff1a;下一个更大元素II 因为可以循环&#xff0c;直接拼一个nums在nums后面就行了。 class Solutio…

[OGeek2019]babyrop

小白垃圾笔记不建议阅读。。。。 这道题额………………做了好几天。。 更离谱的是还把ubuntu16给玩坏了。 师傅说kali可以打通&#xff0c;气得我连夜下卡里 后来发现不是版本的问题&#xff0c;是我的脚本的问题。libc写的不对。 先分析这道题。 32位程序。没有canary&…

串口与wifi模块

经过以下学习&#xff0c;我们掌握&#xff1a; AT指令与wifi模块的测试方法&#xff1a;通过CH340直接测试&#xff0c;研究各种AT指令下wifi模块的响应信息形式。编程&#xff0c;使用串口中断接收wifi模块对AT指令的响应信息以及透传数据&#xff0c;通过判断提高指令执行的…

C语言函数大全-- w 开头的函数(1)

C语言函数大全 本篇介绍C语言函数大全-- w 开头的函数 1. wcscat 1.1 函数说明 函数声明函数功能wchar_t * wcscat(wchar_t *dest, const wchar_t *src);用于将一个宽字符字符串追加到另一个宽字符字符串的末尾 参数&#xff1a; dest &#xff1a; 目标字符串src &#xf…

29.Mybatis—多表操作与注解开发

目录 一、Mybatis学习。 &#xff08;1&#xff09;MyBatis的多表操作。 &#xff08;1.1&#xff09;一对一查询。 &#xff08;1.2&#xff09;一对多查询。 &#xff08;1.3&#xff09;多对多查询。 &#xff08;1.4&#xff09;三种查询知识小结。 一、Mybatis学习。…

算法修炼之练气篇——练气十二层

博主&#xff1a;命运之光 专栏&#xff1a;算法修炼之练气篇 前言&#xff1a;每天练习五道题&#xff0c;炼气篇大概会练习200道题左右&#xff0c;题目有C语言网上的题&#xff0c;也有洛谷上面的题&#xff0c;题目简单适合新手入门。&#xff08;代码都是命运之光自己写的…

从零开始学习Vue3:掌握前端开发的核心技能——更新中

这里写目录标题 一、了解ts1、ts是js的超集 二、掌握ts基本写法1、数据类型2、对象使用3、类4、多态5、类修饰符6、存储器7、抽象类 一、了解ts 1、ts是js的超集 即你可以在ts中使用原生js语法&#xff1a;JavaScript 更多功能 TypeScript** js是弱类型语言&#xff0c;而t…

ESP32设备驱动-LIS3DSH加速度传感器驱动

LIS3DSH加速度传感器驱动 文章目录 LIS3DSH加速度传感器驱动1、LIS3DH介绍2、硬件准备3、软件准备4、驱动实现1、LIS3DH介绍 LIS3DSH 是一款超低功耗高性能三轴线性加速度计,属于“纳米”系列,具有嵌入式状态机,可通过编程实现自主应用。 LIS3DSH 具有 2g/4g/6g/8g/16g 的动…

八、Consul注册中心

目录 1、到官网下载Consul https://www.consul.io/ 2、解压下载好的压缩包 3、配置系统环境变量 4、WINR启动cmd窗口&#xff0c;输入consul启动命令 5、启动完成后访问Consul页面 http://localhost:8500&#xff08;8500为Consul默认端口号&#xff09; 6、server-mem…

〖Python网络爬虫实战㉔〗- Ajax数据爬取之Ajax 分析案例

订阅&#xff1a;新手可以订阅我的其他专栏。免费阶段订阅量1000 python项目实战 Python编程基础教程系列&#xff08;零基础小白搬砖逆袭) 说明&#xff1a;本专栏持续更新中&#xff0c;目前专栏免费订阅&#xff0c;在转为付费专栏前订阅本专栏的&#xff0c;可以免费订阅付…

Git安装及使用

Git简介 Git是什么 Git是目前世界上最先进的分布式版本控制系统(Version Control System)。 Git的功能 Git安装 Git官网&#xff1a;https://git-scm.com/ 打开安装程序后&#xff0c;一直点击下一步&#xff0c;直到以下位置&#xff1a; 这里选择第一项&#xff0c;即…

Spring基础且核心的两大概念——IoC 与 DI

什么是Spring&#xff1f; Spring 全称 Spring Framework&#xff0c;它是一个目前市场上最流行、结构最庞大的开源框架&#xff0c;之所以如此&#xff0c;是因为其有独特且全面的应用场景&#xff0c;这样好的生态才使企业一直对青睐。 注意这句话&#xff1a;Spring 是包含了…