JWT

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vwqoAt9E-1617088645091)(JWT.assets/image-20210321204029994.png)]

1、什么是JWT

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

https://jwt.io/introduction

JSON Web Token (JWT)是一种开放标准(RFC 7519),它定义了一种紧凑和自包含的方式,用于作为JSON对象在各方之间安全地传输信息。这个信息可以被验证和信任,因为它是数字签名的。JWTs可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

通俗的解释

JWT简称JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。

(JWT可以完成前端系统跟后端系统之间的信息传递)

Jwt的核心:一种是信息交换,一种用来做javaweb中的安全验证

2、JWT能做什么

1、授权

这是使用JWT最常见方案,一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用

2、信息交换

JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如:使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改。

3、为什么使用JWT

说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别。

传统的session认证

我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.

基于session认证所显露的问题

1、Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

2、扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

3、CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

4、在前后端分离系统中就更加痛苦了

​ 前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次,如果用session,每次携带sessionid到服务器,服务器还要查询用户信息。同时如果用户很多,这些信息存储在服务器内存中,给服务器增加负担。还有就是CSRF(跨站伪造请求攻击),session是基于cookie进行用户识别的,cookie如果被截获,用户就很容易受到跨站请求伪造的攻击。还有就是sessionid就是一个特征值,表达的信息不够丰富,不容易拓展,而且如果你后端应用是多节点部署,那么就需要实现session共享机制。不方便集群应用

基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *

那么我们现在回到JWT的主题上。
在这里插入图片描述

# 1、认证流程
- 首先,前端通过web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。

- 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编号拼接后签名,形成一个JWT,形成的JWT(Token)就是一个形同111.zzz.xxx的字符串
- token (head.payload.singurater)

- 后端将JWT字符串作为登录成功的返回结果返回到前端,前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可

- 前端在每次请求时将JWT放入HTTp Header中的Authorization位。(解决XSS和XSRF问题)

- 后端检测是否存在,如存在验证JWT有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方案是否是自己(可选)

- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应的结果。

# 2、JWT优势
- `简洁(Compact)`: 可以通过URL,POST参数或者在Http header发送,因为数据量少,传输速度也很快
- `自包含(Self-contained)`: 负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都有支持
- 不需要服务端保存会话信息,特别适用于分布式微服务

4、JWT的结构是什么

token (令牌) String====> header.payload.signature  “typ”:"JWT"
# 1、令牌组成
- 标头(Header)
- 有效载荷(Payload)
- 签名(Signature)
- 因此,JWT通常如下所示: xxxx.yyyyy.zzzzz  Header.Payload.Signature
# 2、Header
- 标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC、SHA256或RSA。它会使用 Base64编码组成 JWT 结构的第一部分。

- 注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子的,它并不是一种加密的过程
{
    "alg":"HS256",
    "typ":"JWT"
}
# 3、Payload
- 令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64 编码组成 JWT 结构的第二部分  不会放敏感信息
{
    "sub":"132465789",
    "name":"John Doe",
    "admin":true
}
# 4、Signature
- 前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过
- 如:
	HMACSHA256(base64UrlEncode(header)+ "." + base64UrlEncode(payload),secret);
	
# 签名的目的
- 最后一步前面的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改,如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

# 信息安全问题
- 在这里大家一定会问一个问题:Base64是一种编码,是一个可逆的过程,那么我的信息不就被暴露了吗?

- 是的,所以在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感的内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5IKC07CI-1617088645097)(JWT.assets/image-20210321224741947.png)]

# 5、放在一起
- 输出是三个由点分隔的Base64—URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。
- 简介(Compact)
    可以通过URL,POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快
- 自包含(Self-contained)
	负载中包含了所有用户所需要的非敏感数据,避免了多次查询数据库

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XtggT1BS-1617088645098)(JWT.assets/image-20210321224808324.png)]

5、使用JWT

1、导入依赖

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.14.0</version>
</dependency>

2、生成token

@Test
    void contextLoads() {

        HashMap<String, Object> map = new HashMap<>();

        Calendar instance = Calendar.getInstance();
        // 20秒后令牌token失效
        instance.add(Calendar.SECOND,20);

        String token = JWT.create()
                .withHeader(map) // header可以不写,因为默认值就是它
                .withClaim("userId", 21)  //payload
                .withClaim("username", "xiaoshuang")
                .withExpiresAt(instance.getTime()) // 指定令牌的过期时间
                .sign(Algorithm.HMAC256("XIAOSHUANG"));//签名

        System.out.println(token);
    }
# 生成结果
- eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTkxNDM1MDQsInVzZXJJZCI6MjEsInVzZXJuYW1lIjoieGlhb3NodWFuZyJ9.vXkV7zhtHuhQ5-vI0ODNPWgTGSIsWWgVc3eJgCtCltA

3、根据令牌和签名解析数据

@Test
public void test(){
    // 通过签名生成验证对象
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("XIAOSHUANG")).build();

    DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTkxNDM1MDQsInVzZXJJZCI6MjEsInVzZXJuYW1lIjoieGlhb3NodWFuZyJ9.vXkV7zhtHuhQ5-vI0ODNPWgTGSIsWWgVc3eJgCtCltA");
    System.out.println(verify.getClaim("userId"));
    System.out.println(verify.getClaim("username"));
    System.out.println("令牌过期时间:"+verify.getExpiresAt());

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n19x0ltw-1617088645099)(JWT.assets/image-20210323100538991.png)]

4、常见的异常信息

- SignatureVerificationException   签名不一致异常
- TokenExpiredException            令牌过期异常
- AlgorirhmMismatchExceotion       算法不匹配异常
- InvalidClaimException            失效的payload异常

5、封装JWT

package com.shuang.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;

public class JWTUtils {

    /**
     * 生成token  header.payload.singature
     */
    private static final String SING = "XIAOSHUANG";

    public static String getToken(Map<String, String> map) {

        Calendar instance = Calendar.getInstance();
        // 默认7天过期
        instance.add(Calendar.DATE, 7);

        //创建jwt builder
        JWTCreator.Builder builder = JWT.create();

        // payload
        map.forEach((k, v) -> {
            builder.withClaim(k, v);
        });
        String token = builder.withExpiresAt(instance.getTime())  //指定令牌过期时间
                .sign(Algorithm.HMAC256(SING));  // sign
        return token;
    }

    /**
     * 验证token  合法性
     */
    public static DecodedJWT verify(String token) {
        return JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
    }

    /**
     * 获取token信息方法
     */
    /*public static DecodedJWT getTokenInfo(String token){
        DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
        return verify;
    }*/
}

6、Springboot整合jJWT

1、搭建环境

1、导包
<dependencies>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.22</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.23</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.14.0</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </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>
2、entity-Dao-Service-Controller

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class User {
    private String id;
    private String name;
    private String password;

}

@Mapper
@Repository
public interface UserDAO {
    User login(User user);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--namespace 绑定一个对应的DAO/Mapper接口-->
<mapper namespace="com.shuang.dao.UserDAO">
    <select id="login" parameterType="User" resultType="User">
        select * from jwt.user where name = #{name} and password = #{password}
    </select>
</mapper>

public interface UserService {
    User login(User user);
}

@Service
@Transactional
public class UserServiceImpl implements UserService{

    @Autowired
    private UserDAO userDAO;
    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public User login(User user) {
        // 根据接收用户名密码查询数据库
        User userDB = userDAO.login(user);
        if (userDB!=null){
            return userDB;
        }
        throw  new RuntimeException("登录失败 -.-");
    }
}
package com.shuang.contorller;

import com.auth0.jwt.JWT;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.shuang.entity.User;
import com.shuang.service.UserService;
import com.shuang.utils.JWTUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

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


@RestController
@Slf4j
public class UserContorller {

    @Autowired
    private UserService userService;

    @GetMapping("/user/login")
    public Map<String,Object> login( User user){
        log.info("用户名:[{}]",user.getName());
        log.info("密码:[{}]",user.getPassword());
        Map<String,Object> map = new HashMap<>();
        try {
            User userDB = userService.login(user);
            Map<String, String> payload = new HashMap<>();
            payload.put("id",userDB.getId());
            payload.put("name",userDB.getName());
            // 生成jwt令牌
            String token = JWTUtils.getToken(payload);
            map.put("state",true);
            map.put("msg","认证成功!");
            map.put("token",token);  // 响应token
        } catch (Exception e) {
            map.put("state",false);
            map.put("msg",e.getMessage());
        }
        return map;
    }

    @PostMapping("/user/test")
    public Map<String,Object> test(String token){
        log.info("当前token为:[{}]",token);
        Map<String,Object> map = new HashMap<>();
        try {
            // 验证令牌
            DecodedJWT verify = JWTUtils.verify(token);
            map.put("state",true);
            map.put("msg","请求成功");
            return map;
        } catch (SignatureVerificationException e) {
            e.printStackTrace();
            map.put("msg","无效签名!");
        }catch (TokenExpiredException e){
            e.printStackTrace();
            map.put("msg","token过期");
        }catch (AlgorithmMismatchException e){
            e.printStackTrace();
            map.put("msg","算法不一致");
        }catch (Exception e){
            e.printStackTrace();
            map.put("msg","token无效!");
        }
        map.put("state",false);
        return map;
    }
}
3、封装JWT工具类
package com.shuang.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;


public class JWTUtils {

    /**
     * 生成token  header.payload.singature
     */
    private static final String SING = "XIAOSHUANG";


    public static String getToken(Map<String, String> map) {

        Calendar instance = Calendar.getInstance();
        // 默认7天过期
        instance.add(Calendar.DATE, 7);

        //创建jwt builder
        JWTCreator.Builder builder = JWT.create();

        // payload
        map.forEach((k, v) -> {
            builder.withClaim(k, v);
        });

        String token = builder.withExpiresAt(instance.getTime())  //指定令牌过期时间
                .sign(Algorithm.HMAC256(SING));  // sign
        return token;
    }

    /**
     * 验证token  合法性
     */
    public static DecodedJWT verify(String token) {
        return JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
    }

    /**
     * 获取token信息方法
     */
    /*public static DecodedJWT getTokenInfo(String token){
        DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token);
        return verify;
    }*/
}

2、测试

使用postman测试工具测试是否查询到数据库中的数据

验证token是否有效

7、解耦整合springboot拦截器

1、创建拦截器

package com.shuang.interceptors;

import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.shuang.utils.JWTUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

public class JWTInterceptors implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Map<String,Object> map = new HashMap<>();
        // 获取请求头中令牌
        String token = request.getHeader("token");
        try {
            // 验证令牌
           JWTUtils.verify(token);
            return true;  // 放行请求

        } catch (SignatureVerificationException e) {
            e.printStackTrace();
            map.put("msg","无效签名!");
        }catch (TokenExpiredException e){
            e.printStackTrace();
            map.put("msg","token过期");
        }catch (AlgorithmMismatchException e){
            e.printStackTrace();
            map.put("msg","算法不一致");
        }catch (Exception e){
            e.printStackTrace();
            map.put("msg","token无效!");
        }
        map.put("state",false);  // 设置状态
        // 将map以json的形式响应到前台  map --> json  (jackson)
        String json = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().println(json);
        return false;
    }
}

2、定制拦截器规则

package com.shuang.config;

import com.shuang.interceptors.JWTInterceptors;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JWTInterceptors())
                .addPathPatterns("/user/test")  // 其他接口token验证
                .excludePathPatterns("/user/login");  // 所有用户都放行
    }
}

3、修改业务逻辑代码

@PostMapping("/user/test")
public Map<String, Object> test(HttpServletRequest request) {
    Map<String, Object> map = new HashMap<>();
    // 验证令牌  交给拦截器去做
    // 只需要在这里处理自己的业务逻辑
    String token = request.getHeader("token");
    DecodedJWT verify = JWTUtils.verify(token);
    log.info("用户id:[{}]",verify.getClaim("id").asString());
    log.info("用户名字:[{}]",verify.getClaim("name").asString());
    map.put("state", true);
    map.put("msg", "请求成功");
  

4、请求头验证token

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4J9HwmEH-1617088645101)(JWT.assets/image-20210323154953469.png)]
如果觉得文章写得还可以的话,请帮我点赞并且多多关注鸭!!!

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐