菜鸟的JWT学习总结

说明

更新时间:2020/8/19 11:09,更新了JWT相关内容

本文主要对JWT的学习总结,本文会持续更新,不断地扩充

本文仅为记录学习轨迹,如有侵权,联系删除

一、什么是JWT

JWT全称是JSON Web Token,是目前最流行的跨域认证解决方案,常用于web项目的token校验,用户校验,权限校验等,也可以用于信息的加密传输。

基于session的认证方式
传统的认证方式也就是我们最常用的基于session的认证方式,用户向服务器发送用户名和密码,服务器经过验证后,将数据保存在session里面,并且向用户返回一个sessionid,存入客户端的cookie中,之后的每次访问都会将sessionid通过cookie传给服务器,用来比对服务器存放的session,以此达到用户的身份认证。
在这里插入图片描述

基于JWT的认证方式
这种方式简单来说就是,客户端向服务器发送用户名和密码,服务器经过验证之后,生成一个Token令牌,该令牌主要由3部分组成,里面包含用户信息和签名等数据,之后客户端每次发送请求就需要将该Token发送给服务器端,通过校验签名的方式,实现用户数据的认证。
在这里插入图片描述

两种方式对比

  1. session是保存在服务端的,随着用户认证的数量增多,服务器的压力会越来越大,而jwt是保存在客户端的,不会对服务器造成影响。
  2. 应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而jwt不需要。

二、JWT的组成

主要组成又3部分,Header(头部)、Payload(负载)和Signature(签名)由这3部分组成,类似于Header.Payload.Signature这样的形式,里面就包含了需要的用户信息(建议非敏感信息),一个实际的例子如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

(1)Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

{
  "alg": "HS256",//alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256)
  "typ": "JWT"typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
}
注意:

Header 主要由令牌和所使用的签名算法组成,它会使用Base64编码将Header进行编码后形成一串字符串,组成JWT的头部。

(2)Payload

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

除此之外,还可以用来存放自定义的用户信息,比如用户名,用户id等自己需要的信息。不i过一般不建议存放像密码这类敏感信息,容易造成安全问题。

userId:"123",
userName:"灰太狼",
age:"12"

它最后也会经过Base64编码形成一串字符串,组成JWT的负载。

(3)Signature

Signature 部分是对前两部分的签名,防止数据篡改。前面两个部分都是使用Base64进行编码的,前端可以解开知道里面的信息,Signature 需要使用编码后的header和payload以及我们提供的一个密钥,使用header中指定的签名算法进行签名,用来保证JWT没有被篡改过,注意,里面用到的密钥是绝对保密的,只能服务器端这边自己知道,防止信息泄露。

签名的目的:当客户端携带token(JWT)向服务器发送请求时,里面携带由上面说的3部分数据,服务器接收到数据后,先进行签名的校验,即将客户端发送的token的前两个部分(header和payload)用header中指定的签名算法加上密钥进行签名,然后将生成的签名和客户端发送的token第三部分(签名)进行比对,如果一致则签名校验通过,信息没有被修改。

三、JJWT

JJWT全称是Java JWT,适用于Java和Android的JSON Web令牌,JJWT旨在成为最容易使用和理解的库,用于在JVM和Android上创建和验证JSON Web令牌(JWT)。JJWT是纯Java实现,完全基于JWT, JWS,JWE, JWK和JWA RFC规范以及Apache 2.0许可条款下的开源。

相关地址
JWT官网: https://jwt.io/
JJWT github: https://github.com/jwtk/jjwt#features-unsupported

在平时的使用中可以通过JJWT来进行JWT的相关操作。

基本使用
引入依赖

        <!--JJWT-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.7</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.7</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.7</version>
            <scope>runtime</scope>
        </dependency>

这里在网上找了一个工具类JwtUtil

package com.zsc.utils;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.codec.binary.Base64;
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;

@Slf4j
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
@Component
@Data
@AllArgsConstructor
public class JwtUtil {
    /**
     * 秘钥
     * - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
     * - 长度必须大于128位
     */
    @Value("${jwt.secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")
    private String secret;
    /**
     * 有效期,单位秒
     * - 默认2周
     */
    @Value("${jwt.expire-time-in-second:1209600}")
    private Long expirationTimeInSecond;

    /**
     * 从token中获取claim
     *
     * @param token token
     * @return claim
     */
    public Claims getClaimsFromToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(this.secret.getBytes())
                    .parseClaimsJws(token)
                    .getBody();
        } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
            log.error("token解析错误", e);
            throw new IllegalArgumentException("Token invalided.");
        }
    }

    /**
     * 获取token的过期时间
     *
     * @param token token
     * @return 过期时间
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimsFromToken(token)
                .getExpiration();
    }

    /**
     * 判断token是否过期
     *
     * @param token token
     * @return 已过期返回true,未过期返回false
     */
    private Boolean isTokenExpired(String token) {
        Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * 计算token的过期时间
     *
     * @return 过期时间
     */
    private Date getExpirationTime() {
        return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);
    }

    /**
     * 为指定用户生成token
     *
     * @param claims 用户信息
     * @return token
     */
    public String generateToken(Map<String, Object> claims) {
        Date createdTime = new Date();
        Date expirationTime = this.getExpirationTime();


        byte[] keyBytes = secret.getBytes();
        SecretKey key = Keys.hmacShaKeyFor(keyBytes);

        System.out.println("key="+key.toString());
        return Jwts.builder()
                .setClaims(claims)//payload
                .setIssuedAt(createdTime)
                .setExpiration(expirationTime)//过期时间
                // 你也可以改用你喜欢的算法
                // 支持的算法详见:https://github.com/jwtk/jjwt#features
                .signWith(key, SignatureAlgorithm.HS256)//header
                .compact();
    }

    /**
     * 判断token是否非法
     *
     * @param token token
     * @return 未过期返回true,否则返回false
     */
    public Boolean validateToken(String token) {
        return !isTokenExpired(token);
    }

}

对于该工具类可以用配置文件的方式配置密钥和过期时间,如果不配置则用默认的密钥和过期时间,配置如下

jwt:
  secret: aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrs1232134214
  # 有效期,单位秒
  expire-time-in-second: 60

使用的时候将该类直接注入即可

    @Autowired
    private JwtUtil jwtUtil;
	......

测试案例

package com.zsc.utils;

import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.awt.desktop.ScreenSleepEvent;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName : JwtUtilTest
 * @Description : JWT工具类测试
 * @Author : CJH
 * @Date: 2020-08-18 17:06
 */
@Slf4j
@SpringBootTest
public class JwtUtilTest {

    @Autowired
    private JwtUtil jwtUtil;


    //生成Token
    public String generateTokenTest(){
        Map<String,Object> map = new HashMap<>();
        map.put("userId",123);
        map.put("userName","灰太狼");
        map.put("admin",true);

        String token = jwtUtil.generateToken(map);


        return token;
    }

    //获取Token
    @Test
    public void getClaimsFromTokenTest(){
        String token = generateTokenTest();
        log.info("token=[{}]",token);

        try {
            Thread.sleep(30000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Claims claims = jwtUtil.getClaimsFromToken(token);




        Object userId = claims.get("userId");
        Object userName = claims.get("userName");
        Object admin = claims.get("admin");

        log.info("userId = [{}],userName = [{}],admin = [{}]",userId,userName,admin);
    }

    //获取token过期时间
    @Test
    public void getExpirationDateFromTokenTest(){
        String token = generateTokenTest();
        Date expirationDateFromToken = jwtUtil.getExpirationDateFromToken(token);
        log.info("token过期时间=[{}]",expirationDateFromToken);
    }

    //判断token是否非法
    @Test
    public void isTokenExpiredTest(){
        String token = generateTokenTest();
        Boolean aBoolean = jwtUtil.validateToken(token);
        log.info("token是否非法=[{}]",aBoolean);
    }
}

四、登录接口实战

为了规范化,这里将登录接口实战集成进我的api_demo2项目里面,集成的方式跟上面的JJWT里面的一样,先添加依赖,引入工具类,配置号密钥和过期时间。

依赖

        <!--JJWT-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.10.7</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.10.7</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.10.7</version>
            <scope>runtime</scope>
        </dependency>

配置

jwt:
  secret: aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrs1232134214
  # 有效期,单位秒
  expire-time-in-second: 60

引入工具类

package com.zsc.utils;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

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

@Slf4j
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
@Component
@Data
@AllArgsConstructor
public class JwtUtil {
    /**
     * 秘钥
     * - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
     * - 长度必须大于128位
     */
    @Value("${jwt.secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")
    private String secret;
    /**
     * 有效期,单位秒
     * - 默认2周
     */
    @Value("${jwt.expire-time-in-second:1209600}")
    private Long expirationTimeInSecond;

    /**
     * 从token中获取claim
     *
     * @param token token
     * @return claim
     */
    public Claims getClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(this.secret.getBytes())
                .parseClaimsJws(token)
                .getBody();
//        try {
//            return Jwts.parser()
//                    .setSigningKey(this.secret.getBytes())
//                    .parseClaimsJws(token)
//                    .getBody();
//        } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
//            log.error("token解析错误", e);
//            throw new IllegalArgumentException("Token invalided.");
//        }
    }

    /**
     * 获取token的过期时间
     *
     * @param token token
     * @return 过期时间
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimsFromToken(token)
                .getExpiration();
    }

    /**
     * 判断token是否过期
     *
     * @param token token
     * @return 已过期返回true,未过期返回false
     */
    private Boolean isTokenExpired(String token) {
        Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    /**
     * 计算token的过期时间
     *
     * @return 过期时间
     */
    private Date getExpirationTime() {
        return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);
    }

    /**
     * 为指定用户生成token
     *
     * @param claims 用户信息
     * @return token
     */
    public String generateToken(Map<String, Object> claims) {
        Date createdTime = new Date();
        Date expirationTime = this.getExpirationTime();


        byte[] keyBytes = secret.getBytes();
        SecretKey key = Keys.hmacShaKeyFor(keyBytes);

        System.out.println("key=" + key.toString());
        return Jwts.builder()
                .setClaims(claims)//payload
                .setIssuedAt(createdTime)
                .setExpiration(expirationTime)//过期时间
                // 你也可以改用你喜欢的算法
                // 支持的算法详见:https://github.com/jwtk/jjwt#features
                .signWith(key, SignatureAlgorithm.HS256)//header
                .compact();
    }

    /**
     * 判断token是否非法
     *
     * @param token token
     * @return 未过期返回true,否则返回false
     */
    public Boolean validateToken(String token) {
        return !isTokenExpired(token);
    }

}

创建JWT的拦截器JwtInterceptor

package com.zsc.interceptor;

import com.zsc.enums.ResultCode;
import com.zsc.exception.BusinessException;
import com.zsc.utils.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @ClassName : JwtInterceptor
 * @Description : Jwt拦截器
 * @Author : CJH
 * @Date: 2020-08-18 22:01
 */
@Slf4j
@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        /**
         * 注意,token一般存放在请求头中,所以发起请求时,请求头必须携带有token,字段名为token
         */
        String token = request.getHeader("token");

        try{
            Boolean tokenResult= jwtUtil.validateToken(token);
            if (tokenResult){
                return true;
            }else {
                log.error("token无效");
                throw new BusinessException(ResultCode.TOKEN_INVALID);
            }

        }catch (ExpiredJwtException e){
            log.error("token异常:[{}]",e.getMessage());
            throw new BusinessException(ResultCode.TOKEN_EXPIRED);

        }catch (MalformedJwtException e){
            log.error("token异常:[{}]",e.getMessage());
            throw new BusinessException(ResultCode.TOKEN_FORMAT_ERROR);

        }catch (UnsupportedJwtException e){
            log.error("token异常:[{}]",e.getMessage());
            throw new BusinessException(3011,e.getMessage());

        }catch (IllegalArgumentException e){
            log.error("token异常:[{}]",e.getMessage());
            throw new BusinessException(ResultCode.TOKEN_IS_EMPTY);
        }
    }
}

在自定义的web配置类中添加拦截器

package com.zsc.config;

import com.zsc.interceptor.JwtInterceptor;
import com.zsc.interceptor.RateLimitInterceptor;
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.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

/**
 * @ClassName : WebConfig
 * @Description : web配置类
 * @Author : CJH
 * @Date: 2020-08-17 21:35
 */
@Configuration
@EnableWebMvc
@Slf4j
public class WebConfig implements WebMvcConfigurer {

    /**
     * 全局限流拦截器
     */
    @Resource
    private RateLimitInterceptor rateLimitInterceptor;

    /**
     * jwt拦截器
     */
    @Autowired
    private JwtInterceptor jwtInterceptor;

    /**
     * 向web中添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //限流拦截器
        registry.addInterceptor(rateLimitInterceptor)
                .addPathPatterns("/api/**");

        //JWT拦截器
        registry.addInterceptor(jwtInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/users/**");
    }

    /**
     * 静态资源配置
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        /**
         * 资源映射到本地目录
         */
        registry.addResourceHandler("/uploads/**")
                .addResourceLocations("file:F:\\Git\\GitRepository\\java\\java_springboot\\api_demo2\\uploads\\");
    }
}

登录接口


    @PostMapping("/login")
    public ResponseResult login(@RequestBody UserQueryDto userQueryDto){
        //构成查询参数
        PageQuery<UserQueryDto> userDtoPageQuery = new PageQuery<>();
        userDtoPageQuery.setPageIndex(1);
        userDtoPageQuery.setPageSize(10);
        userDtoPageQuery.setQuery(userQueryDto);

        //查询
        PageResult<List<UserDto>> result = userServer.query(userDtoPageQuery);

        if(!result.getData().isEmpty()){
            UserDto userDto = result.getData().get(0);

            Map<String,Object> map = new HashMap<>();
            map.put("name",userDto.getName());
            map.put("age",userDto.getAge());
            map.put("email",userDto.getEmail());
            map.put("phone",userDto.getPhone());

            UserTokenVo userTokenVo = new UserTokenVo(jwtUtil.generateToken(map));

            return ResponseResult.success(userTokenVo);
        }else{
            return ResponseResult.failure(ResultCode.USER_OR_PASSWORD_ERROR);
        }

    }

到此集成完成,开始测试,为了测试方便,这里新建了一个测试的控制器

package com.zsc.controller;

import com.zsc.common.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ClassName : TestController
 * @Description : 用于测试的控制器
 * @Author : CJH
 * @Date: 2020-08-18 22:43
 */
@RestController
@Slf4j
@Validated//开启校验支持
@RequestMapping("/api/test")
public class TestController {

    //测试token接口1
    @GetMapping("/test01")
    public ResponseResult<String> test01(String str){
       return ResponseResult.success(str);
    }
}

由于拦截器配置了拦截所有除了"/api/usrs/**"的所有路径,所以,在请求test01接口的时候需要带上token,不然会请求失败,当然上面也做了相应的错误处理
在这里插入图片描述
测试登录接口,返回对应的tonken
在这里插入图片描述

再次请求test01,带上登录返回的token
在这里插入图片描述
到此JWT集成完成,之后开发如果某些接口需要token保护的可以将其路径添加到拦截器里面,以达到认证的目的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值