【SpringBoot深入浅出系列】SpringBoot之集成JWT实现token验证

举报
奔跑吧邓邓子 发表于 2022/04/22 23:49:10 2022/04/22
【摘要】 一、JWT 是什么?JWT(JSON Web Token)是一种开放标准 (RFC 7519),它定义了一种紧凑且独立的方式,用于将信息作为 JSON 对象在各方之间安全地传输。该信息可以进行验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。通俗地说,JWT 的本质就是一个字符串,它是将用户信息保存到一个...

一、JWT 是什么?

JWT(JSON Web Token)是一种开放标准 (RFC 7519),它定义了一种紧凑且独立的方式,用于将信息作为 JSON 对象在各方之间安全地传输。该信息可以进行验证和信任,因为它是经过数字签名的。JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
通俗地说,JWT 的本质就是一个字符串,它是将用户信息保存到一个 Json 字符串中,然后进行编码后得到一个 JWT token,并且这个 JWT token 带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为 Json 对象传输。

二、为什么使用 JWT?

基于 token 的认证方式相比传统的 session 认证方式更节约服务器资源,并且对移动端和分布式更加友好。具有如下优点:

1.支持跨域访问

cookie 是无法跨域的,而 token 由于没有用到 cookie(前提是将 token 放到请求头中),所以跨域后不会存在信息丢失问题。

2.无状态

token 机制在服务端不需要存储 session 信息,因为 token 自身包含了所有登录用户的信息,所以可以减轻服务端压力。

3.更适用CDN

可以通过内容分发网络请求服务端的所有资料。

4.更适用于移动端

当客户端是非浏览器平台时,cookie 是不被支持的,此时采用 token 认证方式会简单很多。

5.无需考虑CSRF

由于不再依赖 cookie,所以采用 token 认证方式不会发生 CSRF,所以也就无需考虑 CSRF 的防御。

三、何时使用 JWT?

1.授权

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

2.信息交换

JWT 是在各方之间安全传输信息的好方法。由于 JWT 可以签名(例如,使用公钥/私钥对),因此您可以确定发送方就是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被篡改。

四、JWT 结构

JWT 由三个部分组成,它们之间由点(.)分隔,分别是:
标头(header)
有效载荷(payload)
签名(Signature)
JWT 结构如下所示:
xxxxx.yyyyy.zzzzz

1.标头(header)

标头通常由两部分组成:令牌的类型和正在使用的签名算法(如 HMAC SHA256 或 RSA)。例如:
{
“alg”: “HS256”,
“typ”: “JWT”
}
使用 Base64 URL 算法将该 JSON 对象转换为字符串保存,形成 JWT 的第一部分。

2.有效载荷(payload)

JWT 的第二部分是有效负载,其中包含声明。声明是关于实体(通常是用户)和其他数据的语句。有三种类型的声明:注册声明、公共声明和私人声明。

注册声明:这些是一组预定义的声明,这些声明不是必需的,但建议提供一组有用的、可互操作的声明。其中一些是:iss(发行人),exp(到期时间),sub(主题),aud(受众)等。

请注意,声明名称的长度只有三个字符,因为 JWT 应该是紧凑的。

公共声明:这些可以由使用JWT的人随意定义。

私人声明:这些是为在同意使用它们的各方之间共享信息而创建的自定义声明,既不是注册声明也不是公开声明。

以下是有效负载的示例:

{
“sub”: “1234567890”,
“name”: “John Doe”,
“admin”: true
}
然后对有效负载进行 Base64 URL 编码,形成 JWT 的第二部分。

请注意,对于已签名的令牌,此信息虽然受到保护以防止篡改,但任何人都可以读取。不要将机密信息放在 JWT 的有效负载或标头元素中,除非它已加密。

3.签名(Signature)

要创建签名部分,必须获取编码的标头、编码的有效负载、密钥,通过指定的算法生成哈希,以确保数据不会被篡改。

例如,如果要使用 HMAC SHA256 算法,将按以下方式创建签名:

HMACSHA256(
base64UrlEncode(header) + “.” +
base64UrlEncode(payload),
secret)
签名用于验证消息在此过程中未发生更改。并且,对于使用私钥签名的令牌,它还可以验证 JWT 的发送者是否是它所说的发件人。

将所有内容放在一起,输出是三个 Base64-URL 字符串,由点分隔,可以在 HTML 和 HTTP 环境中轻松传递,同时与基于 XML 的标准(如 SAML)相比更紧凑。

五、创建项目集成 JWT 实现 token 验证

1.项目说明

新建 Spring Initializr 项目 jwt,项目下新建controller、entity、utils 类。项目实现根据用户信息生成 token 及验证 token。
项目目录结构:
在这里插入图片描述

2.创建 Spring Initializr 项目 jwt

在这里插入图片描述

(1).添加依赖

添加依赖,如果已按截图操作,pom.xml 还需引入 JWT 和 lombok 依赖:

<?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.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.chaoyue</groupId>
    <artifactId>jwt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>jwt</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </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>

(2).添加配置

application.yml 文件中添加如下配置:

server:
  port: 8080

jwt:
  secret: qnAqsQa7600vrTBcr1WB8P8dg4cbgS5i8LZGjWnpREL # 密钥
  expiration: 30 # token 有效期(S)

PS:密钥可通过在线工具库生成:https://www.gjk.cn/randstr

(3).新建实体类 User

为减少不必要的代码,引入 lombok 依赖。实体类代码如下:

package com.chaoyue.jwt.entity;

import lombok.Data;

@Data
public class User {
//    @TableId(type = IdType.AUTO)
    private Long id; // id
    private String username; // 用户名
    private String password; // 密码
}

(4).新建 JWT 工具类 JwtUtils

package com.chaoyue.jwt.utils;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtUtils {

    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";

    /**
     * 密钥
     */
    @Value("${jwt.secret}")
    private String secret;

    /**
     * token有效期 (S)
     */
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 根据用户信息生成 token
     * @param userInfo
     * @return
     */
    @SneakyThrows
    public String generateToken(Object userInfo) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME, new ObjectMapper().writeValueAsString(userInfo));
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 从 token 中获取用户信息
     * @param token
     * @param valueType
     * @param <T>
     * @return
     */
    @SneakyThrows
    public <T> T getUserInfoFromToken(String token, Class<T> valueType) {
        Claims claims = getClaimsFromToken(token);
        return new ObjectMapper().readValue(claims.getSubject(), valueType);
    }

    /**
     * 判断 token 是否有效
     * @param token
     * @return
     */
    public boolean isTokenExpired(String token) {
        Date expiredDate = getExpiredDateFromToken(token);
        return expiredDate.after(new Date());
    }

    /**
     * 刷新 token
     * @param token
     * @return
     */
    public String refreshToken(String token) {
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED, new Date());
        return generateToken(claims);
    }

    /**
     * 生成 token
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims) {
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(generateKeyByDecoders())
                .compact();
    }

    /**
     * 从 token 中获取
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(generateKeyByDecoders())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }


    /**
     * 生成 token 过期时间
     * @return
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 生成自定义 Key
     * @return
     */
    private SecretKey generateKeyByDecoders() {
        return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
    }

    /**
     * 从 token 中获取过期时间
     * @param token
     * @return
     */
    private Date getExpiredDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

}

(5).新建控制类 LoginController

package com.chaoyue.jwt.controller;

import com.chaoyue.jwt.entity.User;
import com.chaoyue.jwt.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class LoginController {

    @Autowired
    private JwtUtils jwtUtils;

    @GetMapping("login")
    public String login(User user) {

        // 生成token
        User userInfo = new User();
        userInfo.setId(1L);
        userInfo.setUsername("admin");
        userInfo.setPassword("123456");
        return jwtUtils.generateToken(userInfo);

    }

}

3.启动服务并测试

启动服务后,浏览器输入:http://localhost:8080/user/login,返回:
在这里插入图片描述
PS:JWT 官网:https://jwt.io/

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。