JWT详解

4 JWT讲解

4.1 需求分析

我们之前已经搭建过了网关,使用网关在网关系统中比较适合进行权限校验。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Mn7ughA-1645080859625)(images\1562059385713.png)]

那么我们可以采用JWT的方式来实现鉴权校验。

4.2 什么是JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

4.3 JWT的构成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

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

在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64EncoderBASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码

载荷(playload)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

(1)标准中注册的声明(建议但不强制使用)

iss: jwt签发者
sub: 描述说明
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

(2)公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

(3)私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

这个指的就是自定义的claim。比如下面面结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。

定义一个payload:

{"sub":"1234567890","name":"John Doe","admin":true}

然后将其进行base64加密,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)

payload (base64后的)

secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

4.4 JJWT的介绍和使用

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

官方文档:

https://github.com/jwtk/jjwt

4.4.1 创建TOKEN

(1)依赖引入

在changgou-parent项目中的pom.xml中添加依赖:

<!--鉴权-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

(2)创建测试

在changgou-common的/test/java下创建测试类,并设置测试方法

public class JwtTest {

    /****
     * 创建Jwt令牌
     */
    @Test
    public void testCreateJwt(){
        JwtBuilder builder= Jwts.builder()
                .setId("888")             //设置唯一编号
                .setSubject("小白")       //设置主题  可以是JSON数据
                .setIssuedAt(new Date())  //设置签发日期
                .signWith(SignatureAlgorithm.HS256,"itcast");//设置签名 使用HS256算法,并设置SecretKey(字符串)
        //构建 并返回一个字符串
        System.out.println( builder.compact() );
    }
}

运行打印结果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4

再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。

4.4.2 TOKEN解析

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

/***
 * 解析Jwt令牌数据
 */
@Test
public void testParseJwt(){
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4";
    Claims claims = Jwts.parser().
            setSigningKey("itcast").
            parseClaimsJws(compactJwt).
            getBody();
    System.out.println(claims);
}

运行打印效果:

{jti=888, sub=小白, iat=1562062287}

试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.

4.4.3 设置过期时间

有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。

4.4.3.1 token过期设置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m74mukUm-1645080859625)(images\1562062896413.png)]

解释:

.setExpiration(date)//用于设置过期时间 ,参数为Date类型数据

运行,打印效果如下:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q
4.4.3.2 解析TOKEN
/***
 * 解析Jwt令牌数据
 */
@Test
public void testParseJwt(){
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q";
    Claims claims = Jwts.parser().
            setSigningKey("itcast").
            parseClaimsJws(compactJwt).
            getBody();
    System.out.println(claims);
}

打印效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DxfdSBY5-1645080859626)(images\1562063075099.png)]

当前时间超过过期时间,则会报错。

4.4.4 自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。

创建测试类,并设置测试方法:

创建token:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-irKqTzoW-1645080859626)(images\1562063346685.png)]

运行打印效果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjMyOTIsImFkZHJlc3MiOiLmt7HlnLPpu5Hpqazorq3nu4PokKXnqIvluo_lkZjkuK3lv4MiLCJuYW1lIjoi546L5LqUIiwiYWdlIjoyN30.ZSbHt5qrxz0F1Ma9rVHHAIy4jMCBGIHoNaaPQXxV_dk

解析TOKEN:

/***
 * 解析Jwt令牌数据
 */
@Test
public void testParseJwt(){
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjMyOTIsImFkZHJlc3MiOiLmt7HlnLPpu5Hpqazorq3nu4PokKXnqIvluo_lkZjkuK3lv4MiLCJuYW1lIjoi546L5LqUIiwiYWdlIjoyN30.ZSbHt5qrxz0F1Ma9rVHHAIy4jMCBGIHoNaaPQXxV_dk";
    Claims claims = Jwts.parser().
            setSigningKey("itcast").
            parseClaimsJws(compactJwt).
            getBody();
    System.out.println(claims);
}

运行效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YiKW4E8o-1645080859627)(images\1562063412350.png)]

4.5 鉴权处理

4.5.1 思路分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ORRklDoD-1645080859627)(images\1562069596308.png)]

1.用户通过访问微服务网关调用微服务,同时携带头文件信息
2.在微服务网关这里进行拦截,拦截后获取用户要访问的路径
3.识别用户访问的路径是否需要登录,如果需要,识别用户的身份是否能访问该路径[这里可以基于数据库设计一套权限]
4.如果需要权限访问,用户已经登录,则放行
5.如果需要权限访问,且用户未登录,则提示用户需要登录
6.用户通过网关访问用户微服务,进行登录验证
7.验证通过后,用户微服务会颁发一个令牌给网关,网关会将用户信息封装到头文件中,并响应用户
8.用户下次访问,携带头文件中的令牌信息即可识别是否登录
4.5.2用户登录签发TOKEN

(1)生成令牌工具类

在changgou-common中创建类entity.JwtUtil,主要辅助生成Jwt令牌信息,代码如下:

public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000  一个小时

    //Jwt令牌信息
    public static final String JWT_KEY = "itcast";

    public static String createJWT(String id, String subject, Long ttlMillis) {
        //指定算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        //当前系统时间
        long nowMillis = System.currentTimeMillis();
        //令牌签发时间
        Date now = new Date(nowMillis);

        //如果令牌有效期为null,则默认设置有效期1小时
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }

        //令牌过期时间设置
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);

        //生成秘钥
        SecretKey secretKey = generalKey();

        //封装Jwt令牌信息
        JwtBuilder builder = Jwts.builder()
                .setId(id)                    //唯一的ID
                .setSubject(subject)          // 主题  可以是JSON数据
                .setIssuer("admin")          // 签发者
                .setIssuedAt(now)             // 签发时间
                .signWith(signatureAlgorithm, secretKey) // 签名算法以及密匙
                .setExpiration(expDate);      // 设置过期时间
        return builder.compact();
    }

    /**
     * 生成加密 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes());
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }


    /**
     * 解析令牌数据
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}

(2) 用户登录成功 则 签发TOKEN,修改登录的方法:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lc92Q1SP-1645080859628)(images\1562070521132.png)]

代码如下:

/***
 * 用户登录
 */
@RequestMapping(value = "/login")
public Result login(String username,String password){
    //查询用户信息
    User user = userService.findById(username);

    if(user!=null && BCrypt.checkpw(password,user.getPassword())){
        //设置令牌信息
        Map<String,Object> info = new HashMap<String,Object>();
        info.put("role","USER");
        info.put("success","SUCCESS");
        info.put("username",username);
        //生成令牌
        String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(info),null);
        return new Result(true,StatusCode.OK,"登录成功!",jwt);
    }
    return  new Result(false,StatusCode.LOGINERROR,"账号或者密码错误!");
}
4.5.3 网关过滤器拦截请求处理

拷贝JwtUtil到changgou-gateway-web中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CEdB6Fwt-1645080859628)(images\1562116408008.png)]

4.5.4 自定义全局过滤器

创建 过滤器类,如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BiFp58w3-1645080859628)(images\1562116467343.png)]

AuthorizeFilter代码如下:

@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {

    //令牌头名字
    private static final String AUTHORIZE_TOKEN = "Authorization";

    /***
     * 全局过滤器
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取Request、Response对象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //获取请求的URI
        String path = request.getURI().getPath();

        //如果是登录、goods等开放的微服务[这里的goods部分开放],则直接放行,这里不做完整演示,完整演示需要设计一套权限系统
        if (path.startsWith("/api/user/login") || path.startsWith("/api/brand/search/")) {
            //放行
            Mono<Void> filter = chain.filter(exchange);
            return filter;
        }

        //获取头文件中的令牌信息
        String tokent = request.getHeaders().getFirst(AUTHORIZE_TOKEN);

        //如果头文件中没有,则从请求参数中获取
        if (StringUtils.isEmpty(tokent)) {
            tokent = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
        }

        //如果为空,则输出错误代码
        if (StringUtils.isEmpty(tokent)) {
            //设置方法不允许被访问,405错误代码
           response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
           return response.setComplete();
        }

        //解析令牌数据
        try {
            Claims claims = JwtUtil.parseJWT(tokent);
        } catch (Exception e) {
            e.printStackTrace();
            //解析失败,响应401错误
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //放行
        return chain.filter(exchange);
    }


    /***
     * 过滤器执行顺序
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}
4.5.5 配置过滤规则

修改网关系统的yml文件:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FiHy2gTw-1645080859629)(images\1562117570398.png)]

上述代码如下:

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]': # 匹配所有请求
              allowedOrigins: "*" #跨域处理 允许所有的域
              allowedMethods: # 支持的方法
                - GET
                - POST
                - PUT
                - DELETE
      routes:
            - id: changgou_goods_route
              uri: lb://goods
              predicates:
              - Path=/api/album/**,/api/brand/**,/api/cache/**,/api/categoryBrand/**,/api/category/**,/api/para/**,/api/pref/**,/api/sku/**,/api/spec/**,/api/spu/**,/api/stockBack/**,/api/template/**
              filters:
              - StripPrefix=1
              - name: RequestRateLimiter #请求数限流 名字不能随便写 ,使用默认的facatory
                args:
                  key-resolver: "#{@ipKeyResolver}"
                  redis-rate-limiter.replenishRate: 1
                  redis-rate-limiter.burstCapacity: 1
            #用户微服务
            - id: changgou_user_route
              uri: lb://user
              predicates:
              - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
              filters:
              - StripPrefix=1

  application:
    name: gateway-web
  #Redis配置
  redis:
    host: 192.168.211.132
    port: 6379

server:
  port: 8001
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
management:
  endpoint:
    gateway:
      enabled: true
    web:
      exposure:
        include: true

测试访问http://localhost:8001/api/user/login?username=changgou&password=changgou,效果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SrkEDGrf-1645080859629)(images\1562117166529.png)]

测试访问http://localhost:8001/api/user,效果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NfMGMO45-1645080859630)(images\1562117230885.png)]

参考官方手册:

https://cloud.spring.io/spring-cloud-gateway/spring-cloud-gateway.html#_stripprefix_gatewayfilter_factory

4.6 会话保持

用户每次请求的时候,我们都需要获取令牌数据,方法有多重,可以在每次提交的时候,将数据提交到头文件中,也可以将数据存储到Cookie中,每次从Cookie中校验数据,还可以每次将令牌数据以参数的方式提交到网关,这里面采用Cookie的方式比较容易实现。

4.6.1 登录封装Cookie

修改user微服务,每次登录的时候,添加令牌信息到Cookie中,修改changgou-service-user的com.changgou.user.controller.UserControllerlogin方法,代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NeT4un0f-1645080859630)(images\1562120506682.png)]

4.6.2 过滤器获取令牌数据

每次在网关中通过过滤器获取Cookie中的令牌,然后对令牌数据进行解析,修改微服务网关changgou-gateway-web中的AuthorizeFilter,代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vGVHPmmq-1645080859631)(images\1562120763383.png)]

登录后测试,可以识别用户身份,不登录无法识别。如下访问http://localhost:8001/api/user会携带令牌数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GbyY2Uwx-1645080859631)(images\1562121093670.png)]

4.6.3 添加Header信息

我们还可以在Gateway的全局过滤器中添加请求头信息,例如可以讲令牌信息添加到请求头中,在微服务中获取头信息,如下代码:

修改微服务网关中的AuthorizeFilter过滤器,在令牌信息校验那块将令牌加入到请求头中,如下代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vu8QS9ga-1645080859631)(images\1562243515236.png)]

在changgou-service-user微服务的UserController的findAll方法中获取请求头测试,代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PLhJnc5p-1645080859632)(images\1562243608017.png)]

后台输出令牌数据如下:

![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5CyAnlqa-1645080859632)(images\1562243685686.png)]](https://img-blog.csdnimg.cn/701be60c5b80465dba84001492cf6ae2.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAwrfljZfmn6_kuIDmoqbCt-OAgQ==,size_20,color_FFFFFF,t_70,g_se,x_16

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JWT(JSON Web Token)是一种用于身份验证的开放标准(RFC 7519)。它通过数字签名验证消息的完整性,因此,JWT可以安全地传输信息。JWT通常用于Web应用程序中,以验证用户的身份。 生成JWT token的步骤如下: 1. 首先,需要选择一个加密算法,如HMAC SHA256或RSA。 2. 构建JWT token的header部分。header部分是一个JSON对象,它描述了JWT的类型和加密算法。例如: ``` { "alg": "HS256", "typ": "JWT" } ``` 其中,"alg"表示使用的加密算法,"typ"表示JWT的类型。 3. 构建JWT token的payload部分。payload部分也是一个JSON对象,它包含要传输的信息,例如用户ID、用户名、过期时间等。例如: ``` { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } ``` 其中,"sub"表示主题(通常是用户ID),"name"表示用户名,"iat"表示JWT的发布时间。 4. 使用密钥对header部分和payload部分进行签名。签名可以确保消息的完整性和真实性。签名的方法取决于所选择的加密算法。例如,如果使用HMAC SHA256,签名可以如下计算: ``` HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) ``` 其中,"secret"是一个密钥,用于计算签名。 5. 将header部分、payload部分和签名组合在一起,用"点"连接起来,形成JWT token。例如: ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIy. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ``` 其中,第一个部分是base64UrlEncode后的header部分,第二个部分是base64UrlEncode后的payload部分,第三个部分是签名。 因为JWT token包含签名,所以接收方可以验证它的完整性和真实性。如果签名验证失败,说明该消息可能已被篡改或伪造。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值