JWT总结及在springboot中的使用

JWT总结

1. 什么是JWT

JWT,全称为JSON Web Token。JWT本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT Token,并且这个JWT Token带有签名信息,接受之后可以校验是否被篡改。具体的JWT认证流程如下:

    1. 用户认证:用户向服务器提供登录信息(如用户名和密码)。
    2. Token生成:服务器验证用户信息无误后,生成一个JWT,该Token包含三个部分:Header(包含类型和加密算法)、Payload(包含用户相关信息)、Signature(用于验证Token的签名)
    3. Token返回:服务器将生成的JWT返回给客户端。
    4. Token存储:客户端将JWT存储在本地(如浏览器的Cookie中)以备后续请求使用。
    5. Token验证:服务器接收到请求后,会取出Token,并用预先约定的密钥对其解密并进行验证。如果Token合法且未被篡改,则认为用户已认证通过。
    6. 信息获取:服务器随后解析JWT中的Payload部分来获取用户信息,并据此处理用户的请求。

这个流程展示了JWT如何作为身份验证和信息传递的工具,实现无状态的、可扩展的安全解决方案。由于JWT是自包含的,它减少了需要在服务端保存的数据量,并简化了跨域身份验证的过程。

2. JWT的构成

JSON Web Tokens (JWT) 是一种开放标准(RFC 7519),定义了一种紧凑、自包含的方式来安全地在各方之间传输信息。JWT主要由三个部分组成,即Header(头部)、Payload(载荷)和Signature(签名),这三部分之间通过.分隔

2.1. Header

描述了所使用的JWT类型(通常为JWT)以及签名算法(如HS256、RS256等)。

• typ: 表明这是一个JWT(固定为 "JWT")。

• alg: 指定用于签署JWT的算法,如 "HS256"(HMAC SHA-256)或 "RS256"(RSA SHA-256)等。

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

2.2. Payload

有效载荷部分是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择

  • iss(issuer):签发JWT的实体。
  • sub(subject):JWT所面向的用户或主题。
  • aud(audience):预期接收JWT的受众。
  • exp(expiration time):JWT过期时间,在此时间之后JWT应被视为无效。
  • nbf(not before):JWT生效时间之前,不应被接受处理的时间点。
  • iat(issued at):JWT的创建时间。
  • jti(JWT ID):JWT的唯一标识符,可用于防止重放攻击。

除了上述默认字段外,我们还可以自定义私有字段,因为我们可以把用户数据放到payload中

{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}

注意:Payload部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!

2.3. Signature

Signature部分用于确保JWT在传输过程中没有被篡改,它是通过对前两部分(Header和Payload编码后的字符串)使用Header中指定的加密算法以及一个共享或私有的密钥进行签名计算得到的。签名确保了只有知道该密钥的实体才能创建有效的JWT,并且任何人都可以验证JWT的完整性和来源的真实性。

生成签名的计算公式如下:

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

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwianRpIjoiMXBteXNldGd1amNvZGVuIiwiZXhwIjoxNTE2MjQyNjIyLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXX0.eW91cmUtYXV0aG9yaXphdGlvbi1zaWduYXR1cmU=

对于这段字符串,我们在JWT官网中,使用它的解码器进行解码,就可以得到Header、Payload、Signature这三部分。

3. 如何在springboot中使用JWT

下面我们现在做一个简单的案例来辅助我们的理解,

引入JWT的依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<!--如果JDK大于8需要引入此依赖-->
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>

创建JWT工具类

  • 创建一个JWT工具类,用于生成和验证JWT令牌。这个类通常会包含生成JWT、解析JWT以及提取其中载荷信息的方法。
package com.example.utils;


import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

public class JwtUtil {


    /**
     *
     * @param claims 设置的信息
     * @param ttlMillis Time To Live(生存时间)的缩写
     * @param secretKey jwt秘钥
     * @return
     */

    public static final String createJwt(Map<String, Object> claims,long ttlMillis, String secretKey){
        //指定使用的签名算法
        SignatureAlgorithm algorithm = SignatureAlgorithm.HS256;

        //生成JWT的时间
        long expiration =System.currentTimeMillis()+ttlMillis;
        Date exp = new Date(expiration);

        String json = Jwts.builder()
        // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
        .setClaims(claims)
        // 设置签名使用的签名算法和签名使用的秘钥  StandardCharsets(标准字符集)
        .signWith(algorithm, secretKey.getBytes(StandardCharsets.UTF_8))
        // 设置过期时间
        .setExpiration(exp)
        .compact();

        return json;
    }

    /**
     *
     * @param secretKey 注意:此秘钥一定要保存好在服务器,一定不要泄漏出去!
     * @param token     加密后的Token
     * @return
     */
    public static final Claims parseJwt(String secretKey, String token){

        Claims claims = Jwts.parser()
        //设置签名秘钥
        .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
        //设置需要解析的jwt
        .parseClaimsJws(token)
        .getBody();
        return claims;
    }
}

属性配置绑定

  • 使用@ConfigurationProperties注解读取在配置文件中的参数
package com.example.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@Data
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
    long adminTtl;
    String secretKey;
    String adminTokenName;
}
  • 在applicant.yml中的参数设置
jwt:
admin-ttl: 7200000
admin-token-name: token
secret-key: hyt

实现JWT拦截器

  • 创建一个自定义的JWT过滤器,它负责检查每个请求的header中的Token,并对其进行验证。验证成功之后获得参数,如下例子是获得用户登录的id,因每个线程都可以访问到自己的变量副本,而不会影响其他线程的变量值,所以我们设置到自己线程的变量值中
package com.example.interceptor;
import com.example.context.BaseContext;
import com.example.properties.JwtProperties;
import com.example.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截的是Controller的方法还是其他资源
        if(!(handler instanceof HandlerMethod)){
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        try {
            String token = request.getHeader(jwtProperties.getAdminTokenName());
            log.info("jwt校验:{}",token);
            Claims claims = JwtUtil.parseJwt(jwtProperties.getSecretKey(), token);
            Long userId = Long.valueOf(claims.get("userId").toString());
            log.info("当前员工id:{}",userId);
            BaseContext.setCurrentId(userId);
            return true;
        }catch (Exception ex){
            response.setStatus(401);
            return false;
        }

    }
}
  • ThreadLocal 是 Java 中的一个类,它用于在每个线程中存储一个独立的变量副本。这样,每个线程都可以访问到自己的变量副本,而不会影响其他线程的变量值。ThreadLocal 的主要方法是 set(),它用于设置当前线程的变量值。ThreadLocal 的 set() 方法接受一个参数,即要设置的变量值。当调用 set() 方法时,ThreadLocal 会将传入的值存储在当前线程的变量副本中。这样,当线程需要访问这个变量时,可以通过 get() 方法获取到自己线程的变量值。
package com.example.context;

public class BaseContext {
    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id){
        threadLocal.set(id);
    }

    public static Long getCurrentId(){
        return threadLocal.get();
    }

    public static void removeCurrentId(){
        threadLocal.remove();
    }
}

注册自定义拦截器

package com.example.config;

import com.example.interceptor.JwtTokenUserInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@Slf4j
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器");
        registry.addInterceptor(jwtTokenUserInterceptor)
        .addPathPatterns("/user/**")
        .excludePathPatterns("/user/login");
    }


}

登录接口

我们在定义一个登陆接口,登陆成功后返回jwt生成的token。

package com.example.controller;

import com.example.dto.UserLoginDTO;
import com.example.properties.JwtProperties;
import com.example.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Autowired
    private JwtProperties jwtProperties;

    @GetMapping("/login")
    public String login(@RequestBody UserLoginDTO userLoginDTO){
        //这里不再书写业务层和数据库层,而是进行模拟
        String nameDB = "hyt";
        String passwordDB = "123456";
        String name = userLoginDTO.getName();
        String password = userLoginDTO.getPassword();
        if(name.equals(nameDB) && password.equals(passwordDB)){
            Map<String, Object> claims = new HashMap<>();
            //这里id我随机生成,在真实业务时是通过数据过查询得到的
            Random random = new Random();
            claims.put("userId", random.nextInt(100));

            String token = JwtUtil.createJwt(
                claims, jwtProperties.getAdminTtl(),
                jwtProperties.getSecretKey());

            //在真实业务中是要按照前后联调一致格式返回
            return token;
        }
        return "密码错误";
    }

}
  • 用于封装前端传过来的参数
package com.example.dto;

import lombok.Data;

import java.io.Serializable;
@Data
public class UserLoginDTO implements Serializable {
    String name;
    String password;
}

测试

  • 登录成功返回token

  • 密码错误返回提示

  • 携带有效token访问别的方法

  • 当我们修改token值,在拦截器中我设置的是返回状态码401,HTTP状态码401表示"Unauthorized"(未经授权)

总结

JWT(JSON Web Tokens)作为一种轻量级且灵活的身份验证和授权机制,在现代web服务及移动应用中得到了广泛应用。它允许服务器端通过加密签名的方式向客户端发放安全的、自包含的令牌,这些令牌可以携带必要的用户身份信息和权限声明,而且由于其无需持久化存储的特性,非常适合于微服务架构下的无状态通信场景。

总结起来,采用JWT进行身份验证具有以下优点:

  • 安全性: 通过密钥签名保证令牌的安全性,防止篡改。
  • 高效性: 状态less设计减少了服务器端存储负担,提升了系统的可扩展性和响应速度。
  • 跨域友好: JWT可轻松应用于多个域名或子系统间的认证需求。
  • 自包含性: 令牌自身携带了足够的用户信息,减轻了服务器端查询数据库的频率。
  • 有效期可控: 可以在JWT中设置过期时间,从而控制用户的登录会话持续时间。

不过需要注意的是,JWT并非适用于所有场景,尤其是在敏感数据的处理上,因为一旦令牌被截获,黑客们在有效期内可以持续使用。因此,在实施JWT方案时,应充分考虑其适用范围,并配合合适的刷新策略、黑名单机制以及其他安全措施,确保系统的整体安全性和用户体验。

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值