Chiriri's blog Chiriri's blog
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)

Iekr

苦逼后端开发
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)
  • JavaSE

  • JavaEE

  • Linux

  • MySQL

  • NoSQL

  • Python

  • Python模块

  • 机器学习

  • 设计模式

  • 传智健康

  • 畅购商城

    • Day01 项目搭建
    • Day02 FastDFS
    • Day03 微服务鉴权
      • 微服务网关Gateway
        • 微服务网关微服务搭建
        • 微服务网关跨域
        • 微服务网关过滤器
      • 网关限流
        • 令牌桶算法
        • 代码实现
      • BCrypt密码加密
        • 测试用例
        • 新增管理员密码加密
        • !image-20211115211324853管理员登陆密码验证
      • 加密算法
        • 可逆加密算法
        • 对称加密
        • 非对称加密
        • 不可逆加密算法
        • Base64编码
      • JWT 实现微服务鉴权
        • JJWT签发和验证token
        • 创建token
        • 解析token
        • 设置过期时间
        • 自定义claims
      • 微服务鉴权
        • 微服务签发token
        • 网关过滤器验证token
    • Day04 新增和修改商品
    • Day05 广告缓存
    • Day06 监听数据库更新广告缓存
    • Day07 ES搜索
    • Day08 Thymeleaf
    • Day09 Oauth2
    • Day10 购物车渲染
    • Day11 订单结算
    • Day12 分布式事务解决方案
    • Day13 微信支付
    • Day14 订单处理
    • Day15 秒杀前端
    • Day16 秒杀后端
  • 博客项目

  • JVM

  • JUC

  • Golang

  • Kubernetes

  • 硅谷课堂

  • C

  • 源码

  • 神领物流

  • RocketMQ

  • 短链平台

  • 后端
  • 畅购商城
Iekr
2021-11-15
目录

Day03 微服务鉴权

# Day03 微服务鉴权

# 微服务网关 Gateway

不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性
  • 存在跨域请求,在一定场景下处理相对复杂
  • 认证复杂,每个服务都需要独立认证
  • 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施

1557824391432

优点如下:

  • 安全 ,只有网关系统对外进行暴露,微服务可以隐藏在内网,通过防火墙保护。
  • 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
  • 易于统一认证授权。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
  • 减少了客户端与各个微服务之间的交互次数

总结:微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权,安全控制,日志统一处理,易于监控的相关功能。

实现微服务网关的技术有很多,

  • nginx Nginx (engine x) 是一个高性能的 HTTP (opens new window) 和反向代理 (opens new window) web 服务器,同时也提供了 IMAP/POP3/SMTP 服务
  • zuul ,Zuul 是 Netflix 出品的一个基于 JVM 路由和服务端的负载均衡器。
  • spring-cloud-gateway, 是 spring 出品的 基于 spring 的网关项目,集成断路器,路径重写,性能比 Zuul 好。

gateway 官网:

https://spring.io/projects/spring-cloud-gateway

# 微服务网关微服务搭建

1557825607685

在 changgou_gateway 创建 changgou_gateway_system 工程 pom 文件

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
1
2
3
4
5
6
7
8
9
10
11
12

创建包 com.changgou.system 启动类

package com.changgou.system;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

application.yml

spring:
  application:
    name: sysgateway
  cloud:
    gateway:
      routes:
        - id: goods # 唯一标识
          uri: lb://goods #lb为动态路由协议 通过eureka找到此服务具体路径
          predicates:
            - Path=/goods/**  # 访问路径
          filters:
            - StripPrefix= 1 # 1为忽略第一斜杠后的字符
        - id: system
          uri: lb://system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix= 1
server:
  port: 9101
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:6868/eureka
  instance:
    prefer-ip-address: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

启动启动类

访问

http://192.168.42.41:9101/goods/brand/category/%E6%89%8B%E6%9C%BA

和

http://192.168.42.41:9001/brand/category/%E6%89%8B%E6%9C%BA

得到的数据一致 证明网关生效 gateway 已经转发指定路径

# 微服务网关跨域

修改 application.yml 在 spring.cloud.gateway 节点添加配置

      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
            - GET
            - POST
            - PUT
            - DELETE
1
2
3
4
5
6
7
8
9

配置文件整体

spring:
  application:
    name: sysgateway
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
        - id: goods # 唯一标识
          uri: lb://goods #lb为动态路由协议 通过eureka找到此服务具体路径
          predicates:
            - Path=/goods/**  # 访问路径
          filters:
            - StripPrefix= 1 # 1为忽略第一斜杠后的字符
        - id: system
          uri: lb://system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix= 1
server:
  port: 9101
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:6868/eureka
  instance:
    prefer-ip-address: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# 微服务网关过滤器

我们可以通过网关过滤器,实现一些逻辑的处理,比如 ip 黑白名单拦截、特定地址的拦截等。下面的代码中做了两个过滤器,并且设定的先后顺序,只演示过滤器与运行效果。

在 com.changgou.system.fileter 创建 IpFilter 类 实现 GlobalFilter, Ordered 接口 实现方法

package com.changgou.system.fileter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.InetSocketAddress;

/**
 * 获取客户端的访问ip
 */

@Component
public class IpFilter  implements GlobalFilter, Ordered {
    //具体业务逻辑
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取客户端的访问ip
        System.out.println("经过第一个过滤器");
        ServerHttpRequest request = exchange.getRequest();
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        System.out.println("ip:"+remoteAddress.getHostName());

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

    //过滤器的执行优先级 返回值越小 执行优先级越高
    @Override
    public int getOrder() {
        return 1;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

在 com.changgou.system.fileter 创建 UrlFilter 类 实现 GlobalFilter, Ordered 接口 实现方法

package com.changgou.system.fileter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class UrlFilter implements GlobalFilter, Ordered {

    //业务代码
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("第二个过滤器");
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        System.out.println("path:" + path);
        return chain.filter(exchange);
    }

    //过滤器优先级
    @Override
    public int getOrder() {
        return 2;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 网关限流

网关可以做很多的事情,比如,限流,当我们的系统被频繁的请求的时候,就有可能将系统压垮,所以为了解决这个问题,需要在每一个微服务中做限流操作,但是如果有了网关,那么就可以在网关系统做限流,因为所有的请求都需要先通过网关系统才能路由到微服务中。

1557909861570

# 令牌桶算法

令牌桶算法是比较常见的限流算法之一,大概描述如下:

  1. 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
  2. 根据限流大小,设置按照一定的速率往桶里添加令牌;
  3. 桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
  4. 请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
  5. 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流

1557910299016

这个算法的实现,有很多技术,Guava (读音:瓜哇) 是其中之一,redis 客户端也有其实现。

# 代码实现

需求:每个 ip 地址 1 秒内只能发送 1 次请求,多出来的请求返回 429 错误。

我们基于 redis 实现

  • 引入依赖
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>
1
2
3
4
5
6
  • application 添加 redis 连接地址
spring :
	redis:
    	host: 192.168.130.128
1
2
3
  • 在 filters 下添加
- name: RequestRateLimiter #请求数限流 名字不能随便写
              args:
                key-resolver: "#{@ipKeyResolver}"  #为bean绑定的方法名
                redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
                redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
1
2
3
4
5

完整配置

spring:
  application:
    name: sysgateway
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
        - id: goods # 唯一标识
          uri: lb://goods #lb为动态路由协议 通过eureka找到此服务具体路径
          predicates:
            - Path=/goods/**  # 访问路径
          filters:
            - StripPrefix= 1 # 1为忽略第一斜杠后的字符
            - name: RequestRateLimiter #请求数限流 名字不能随便写
              args:
                key-resolver: "#{@ipKeyResolver}"  #为bean绑定的方法名
                redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
                redis-rate-limiter.burstCapacity: 1 #令牌桶总容量
        - id: system
          uri: lb://system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix= 1
  redis:
    host: 192.168.130.128
server:
  port: 9101
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:6868/eureka
  instance:
    prefer-ip-address: true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
  • 在启动类下 添加 @bean 返回 KeyResolver 对象
package com.changgou.system;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);

    }

    @Bean
    public KeyResolver ipKeyResolver() {
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
                //just对指定内容进行 限制 此处对访问者地址作限制
                return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
            }
        };
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  • burstCapacity:令牌桶总容量。
  • replenishRate:令牌桶每秒填充平均速率。
  • key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据 #{@beanName} 从 Spring 容器中获取 Bean 对象。

通过在 replenishRate 和中设置相同的值来实现稳定的速率 burstCapacity 。设置 burstCapacity 高于时,可以允许临时突发 replenishRate 。在这种情况下,需要在突发之间允许速率限制器一段时间(根据 replenishRate ),因为 2 次连续突发将导致请求被丢弃( HTTP 429 - Too Many Requests )

key-resolver: "#{@userKeyResolver}" 用于通过 SPEL 表达式来指定使用哪一个 KeyResolver.

如上配置:

表示 一秒内,允许 一个请求通过,令牌桶的填充速率也是一秒钟添加一个令牌。

最大突发状况 也只允许 一秒内有一次请求,可以根据业务来调整 。

# BCrypt 密码加密

在用户模块,对于用户密码的保护,通常都会进行加密。我们通常对密码进行加密,然后存放在数据库中,在用户进行登录的时候,将其输入的密码进行加密然后与数据库中存放的密文进行比较,以验证用户密码是否正确。 目前,MD5 和 BCrypt 比较流行。相对来说,BCrypt 比 MD5 更安全。因为其内部引入的加盐机制

BCrypt 官网 http://www.mindrot.org/projects/jBCrypt/

# 测试用例

测试盐和加密 测试类

package com.changgou.test;

import org.springframework.security.crypto.bcrypt.BCrypt;

public class TestBcrypt {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            //获取盐
            String gensalt = BCrypt.gensalt();
            System.out.println(gensalt);
            //基于当前的盐对密码进行加密
            String saltPassword = BCrypt.hashpw("123456", gensalt);//需要一个密码和盐
            System.out.println(saltPassword);
            //解密
            boolean checkpw = BCrypt.checkpw("123456", saltPassword);//需要一个明文密码 加上加密后的值解密 返回一个布尔值
            System.out.println(checkpw);

        }


    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 新增管理员密码加密

修改 changgou_service_system 项目 com.changgou.system.service.impl.AdminServiceImpl 对增加操作进行加密

/**
 * 增加
 * @param admin
 */
@Override
public void add(Admin admin){
    //获取盐
    String gensalt = BCrypt.gensalt();
    //对用户的密码进行加密
    String hashpw = BCrypt.hashpw(admin.getPassword(), gensalt);
    admin.setPassword(hashpw);
    adminMapper.insert(admin);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# image-20211115211324853 管理员登陆密码验证

AdminService 新增方法定义

    /**
     * 登录验证密码
     * @param admin
     * @return
     */
    boolean login(Admin admin);
1
2
3
4
5
6

AdminServiceImpl 实现此方法

@Override
public boolean login(Admin admin) {
    //根据登录名获取管理员信息
    Admin admin1 = new Admin();
    admin1.setLoginName(admin.getLoginName());
    admin1.setStatus("1");
    //根据用户名查询
    Admin adminResult = adminMapper.selectOne(admin1);

    if (adminResult == null){
        return false;
    }else{
        //对密码进行校验
        return BCrypt.checkpw(admin.getPassword(),adminResult.getPassword());
    }
    //返回结果
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

AdminController 新增方法

@PostMapping("/login")
public Result login(@RequestBody Admin admin) {
    boolean result = adminService.login(admin);
    if (result) {
        //密码是正确的
        return new Result(true, StatusCode.OK, "登录成功");
    } else {
        return new Result(false, StatusCode.ERROR, "登录失败");
    }
}
1
2
3
4
5
6
7
8
9
10

# 加密算法

由于在学习 JWT 的时候会涉及使用很多加密算法

# 可逆加密算法

加密后,密文可以反向解密得到密码原文

# 对称加密

【文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥】

解释: 在对称加密算法中,数据发信方将明文和加密密钥一起经过特殊的加密算法处理后,使其变成复杂的加密密文发送出去,收信方收到密文后,若想解读出原文,则需要使用加密时用的密钥以及相同加密算法的逆算法对密文进行解密,才能使其回复成可读明文。在对称加密算法中,使用的密钥只有一个,收发双方都使用这个密钥,这就需要解密方事先知道加密密钥。

优点: 对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。

缺点: 没有非对称加密安全.

用途: 一般用于保存用户手机号、身份证等敏感但能解密的信息。

常见的对称加密算法有: AES、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256

# 非对称加密

【两个密钥:公开密钥(publickey)和私有密钥,公有密钥加密,私有密钥解密】

解释: 同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端.

加密与解密:

  • 私钥加密,持有私钥或公钥才可以解密
  • 公钥加密,持有私钥才可解密

签名:

  • 私钥签名,持有公钥进行验证是否被篡改过.

优点: 非对称加密与对称加密相比,其安全性更好;

缺点: 非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。 用途: 一般用于签名和认证。私钥服务器保存,用来加密,公钥客户拿着用于对于令牌或者签名的解密或者校验使用.

常见的非对称加密算法有: RSA、DSA(数字签名用)、ECC(移动设备用)、RS256 (采用SHA-256 的 RSA 签名)

# 不可逆加密算法

解释: 一旦加密就不能反向解密得到密码原文.

种类: Hash 加密算法,散列算法,摘要算法等

** 用途:** 一般用于效验下载文件正确性,一般在网站上下载文件都能见到;存储用户敏感信息,如密码、 卡号等不可解密的信息。

常见的不可逆加密算法有: MD5、SHA、HMAC

# Base64 编码

Base64 是网络上最常见的用于传输 8Bit 字节代码的编码方式之一。Base64 编码可用于在 HTTP 环境下传递较长的标识信息。采用 Base64 编码解码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。注意:Base64 只是一种编码方式,不算加密方法。

在线编码工具:

http://www.jsons.cn/img2base64/

# JWT 实现微服务鉴权

1557903370904

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

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

头部(Header)

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

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

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

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
1

载荷(playload)

载荷就是存放有效信息的地方。

定义一个 payload:

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

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

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
1

签证(signature)

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

header (base64 后的)

payload (base64 后的)

secret

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

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
1

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
1

# JJWT 签发和验证 token

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

官方文档:https://github.com/jwtk/jjwt

导入依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
1
2
3
4
5

# 创建 token

测试样例

JwtBuilder builder= Jwts.builder()
    .setId("888")   //设置唯一编号
    .setSubject("小白")//设置主题  可以是JSON数据
    .setIssuedAt(new Date())//设置签发日期
    .signWith(SignatureAlgorithm.HS256,"itcast");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串 
System.out.println( builder.compact() );
1
2
3
4
5
6
7

结果

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y
1

# 解析 token

String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y";

Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();

System.out.println(claims);
1
2
3
4
5

结果

{jti=888, sub=小白, iat=1557904181}
1

# 设置过期时间

  1. 创建 token 并设置过期时间
//当前时间
long currentTimeMillis = System.currentTimeMillis();
Date date = new Date(currentTimeMillis);
JwtBuilder builder= Jwts.builder()
    .setId("888")   //设置唯一编号
    .setSubject("小白")//设置主题  可以是JSON数据
    .setIssuedAt(new Date())//设置签发日期
    .setExpiration(date) //设置jwt过期时间
    .signWith(SignatureAlgorithm.HS256,"itcast");//设置签名 使用HS256算法,并设置SecretKey(字符串)
//构建 并返回一个字符串
System.out.println( builder.compact() );
1
2
3
4
5
6
7
8
9
10
11
  1. 解析 token
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI";

Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();

System.out.println(claims);
1
2
3
4
5

如果我们的 token 已经过期 我们尝试解析 token 则会报错

# 自定义 claims

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

创建 token

@Test
public void createJWT(){
    //当前时间
    long currentTimeMillis = System.currentTimeMillis();
    currentTimeMillis+=1000000L;
    Date date = new Date(currentTimeMillis);
    JwtBuilder builder= Jwts.builder()
        .setId("888")   //设置唯一编号
        .setSubject("小白")//设置主题  可以是JSON数据
        .setIssuedAt(new Date())//设置签发日期
        .setExpiration(date)//设置过期时间
        .claim("roles","admin")//设置角色
        .claim("company","itheima") //可以添加多个claim
        .signWith(SignatureAlgorithm.HS256,"itcast");//设置签名 使用HS256算法,并设置SecretKey(字符串)
    //构建 并返回一个字符串
    System.out.println( builder.compact() );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

解析 TOKEN

//解析
@Test
public void parseJWT(){
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI";
    Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();
    System.out.println(claims);
}
1
2
3
4
5
6
7

结果

{jti=888,sub=小白,iat=1568617941,roles=admin,company=itheima}
1

# 微服务鉴权

1557906391792

  1. 用户进入网关开始登陆,网关过滤器进行判断,如果是登录,则路由到后台管理微服务进行登录
  2. 用户登录成功,后台管理微服务签发 JWT TOKEN 信息返回给用户
  3. 用户再次进入网关开始访问,网关过滤器接收用户携带的 TOKEN
  4. 网关过滤器解析 TOKEN ,判断是否有权限,如果有,则放行,如果没有则返回未认证错误

# 微服务签发 token

在 changgou_service_system 添加依赖

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.0</version>
</dependency>
1
2
3
4
5

在 changgou_service_system 中创建类: JwtUtil

package com.changgou.system.util;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "itcast";

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        SecretKey secretKey = generalKey();

        JwtBuilder builder = Jwts.builder()
                .setId(id)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("admin")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

修改 AdminController 的 login 方法,用户登录成功 则 签发 TOKEN


    /**
     * 登陆
     *
     * @param admin
     * @return
     */
    @PostMapping("/login")
    public Result login(@RequestBody Admin admin) {
        boolean result = adminService.login(admin);
        if (result) {
            //密码是正确的
            //生成jwt令牌 返回结果
            Map<String, String> info = new HashMap<>();
            info.put("username", admin.getLoginName());
            //基于工具类生成jwt
            String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), admin.getLoginName(), null);
            info.put("token", jwt);
            return new Result(true, StatusCode.OK, "登录成功", info);

        } else {
            return new Result(false, StatusCode.ERROR, "登录失败,请检查用户账号或密码");
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 网关过滤器验证 token

在 changgou_gateway_system 网关系统添加依赖

<!--鉴权-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
1
2
3
4
5
6

创建 JWTUtil 类

package com.changgou.system.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "itcast";

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {

        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        SecretKey secretKey = generalKey();

        JwtBuilder builder = Jwts.builder()
                .setId(id)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("admin")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

创建过滤器,用于 token 验证

package com.changgou.system.fileter;

import com.changgou.system.util.JwtUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //获取请求对象
        ServerHttpRequest request = exchange.getRequest();
        //获取响应对象
        ServerHttpResponse response = exchange.getResponse();
        //判断此次请求是否为登陆 如是则直接放行
        if (request.getURI().getPath().contains("/admin/login")) {
            //放行
            return chain.filter(exchange);
        }
        //获取当前的所有请求头信息
        HttpHeaders headers = request.getHeaders();
        //获取jwt令牌信息
        String jwtToken = headers.getFirst("token");
        //判断当前令牌是否存在
        if (StringUtils.isEmpty(jwtToken)) {
            //如不存在 则向客户端提示错误信息 当前用户认证失败
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        //如果令牌存在 解析jwt令牌 判断令牌是否合法 不合法则返回错误信息
        try {
            //解析令牌
            JwtUtil.parseJWT(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            //令牌解析失败
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        //令牌合法 放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

测试

image-20211116203825836

编辑 (opens new window)
上次更新: 2023/12/06, 01:31:48
Day02 FastDFS
Day04 新增和修改商品

← Day02 FastDFS Day04 新增和修改商品→

最近更新
01
k8s
06-06
02
进程与线程
03-04
03
计算机操作系统概述
02-26
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Iekr | Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式