微服务网关和Jwt令牌

1,315 阅读12分钟

「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战

微服务网关和Jwt令牌

常见面试题:

为什么需要网关:

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

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

以上这些问题可以借助网关解决。

网关的优点:

在这里插入图片描述

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

用户权限——网关管理:

假设一个项目存在很多的角色: 不同的角色有不同的权限功能就可以通过网关来进行管理:

  • 不同的角色给与不同的网关, 网关里管理能够操作的模块... 在这里插入图片描述

总结:

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

JWT讲解

什么是JWT

  • JSON Web Token(JWT)是一个非常轻巧的规范。(网络应用环境间传递声明而执行的一种基于JSON的开放标准)
  • 这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

说起JWT,我们应该来谈一谈

token的鉴权机制 传统的session认证的区别

传统的session认证

  • 我们知道,http协议本身是一种无状态的协议 这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行
  • 因为根据http协议,我们并不能知道是哪个用户发出的请求
  • 所以为了让我们的应用能识别是哪个用户发出的请求, 我们只能在服务器存储(Session)一份用户登录的信息 这份信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用; 这样我们的应用就能识别请求来自哪个用户了

Session 问题:

  • Session的数据是保存在 服务器中的... 而随着认证用户的增多,服务端的开销会明显增大。

而且:

  • 当我们使用了 微服务架构... 一个功能就是一个模块,一个模块就是一个独立的服务器.
  • Session就不能实现在多个,模块之间共享数据了!!

token的鉴权机制

token的鉴权机制 类似于http协议也是无状态的

它不需要在服务端去保留用户的认证信息或者会话信息。

这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

  • 用户使用用户名密码来请求服务器
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

你第一次登录成功,服务器给你生成一个令牌/身份证(Token), 下次在来请求带着令牌来如果没有或错误,不允许登录!并根据令牌得知你是那个用户!

  • 这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持 CORS(跨来源资源共享)策略 ,一般我们在服务端这么做就可以了

因为 Jwt 可以搭配 Secret密钥, 来进行解密, 不同微服模块,就可以通过解码 来获得对应的用户信息!(存储信息!)

JWT的构成

一个JWT实际上就是一个字符串逗号分隔,它由三部分组成: 头部 载荷 签证

头部(Header)

头部用于描述关于该JWT的最基本的信息

  • 例如其类型以及签名所用的算法等。一般被表示成一个JSON对象。

一般默认为: 一般不需要用户自定义,程序默认

 {"typ":"JWT","alg":"HS256"}     //头部指明了签名算法是HS256算法;

之后会进行BASE64编码base64.xpcha.com/,编码后的字符串如下:

 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9        #这就是通常JWT的头部.存放着签名的算法信息!

载荷(payload)

载荷就是存放有效信息的地方。 (用户名/密码/…. 重要信息的地方,进行加密!) 一般由开发者编写!

一般包括以下部分:或存储一些重要的信息:用户信息..

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

比如下面这个 Payload 使用了 sub 还有两个开发者自定义的字段!这里就像一个存储重要信息的仓库.

 {"sub":"1234567890","name":"John Doe","admin":true}    
 //sub   通常表示用户id 
 //name  用户名
 //admin 是否管理员

base64加密,得到Jwt的第二部分 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

签证(signature)

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

  • 第一部分:hader头部的加密,eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
  • 第二部分:palyload的加密 ,eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
  • 第三部分:Secret ,这个相当于是一个密码,这个密码秘密地存储在服务端。
  • 先是用 Base64 编码的 header.payload (header+payload) ,再用加密算法加密一下,加密的时候要放进去一个 Secret // (header+payload) 加密信息合并 var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload); // HMACSHA256加密算法加密(haeder中指定的加密算法), 并加密过程附带一个 Secret密钥 HMACSHA256(encodedString, 'secret'); 处理完成以后看起来像这样: TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ 这就是 signature;

此时已经有了三段 密文了, 最后在将三段密文合并在一起, 三者之间通过 . 点来间隔区分...就形成了 JWT

 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:

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

Jwt的验证流程

① 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串 ② 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串 ③ 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。 ④ 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密。

JWT Demo测试:

在这里插入图片描述 上面的模块其实,无伤大雅...最关键的就是:pom依赖TestJWT.Java

引入 jjwt 依赖

pom.xml

 <dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.0</version>
 </dependency>

创建测试Demo

TestJWT.Java

 package com.wsm.jwt;
 import io.jsonwebtoken.Claims;
 import io.jsonwebtoken.JwtBuilder;
 import io.jsonwebtoken.Jwts;
 import io.jsonwebtoken.SignatureAlgorithm;
 import java.util.Date;
 ​
 //JWT 测试类
 public class TestJWT {
 ​
     public static void main(String[] args) {
         //打印: 创建的JWT
         String jwt = createToken();
         System.out.println(jwt);
 ​
         System.out.println("---------------------------------------------------------------------------------------");
 ​
         //打印: JWT的解析数据
         System.out.println(getToken(jwt));
     }
 ​
     //创建 JWT
     public static String createToken() {
 ​
         JwtBuilder jwt = Jwts.builder()
                 .setId("110")                                    //设置唯一编号
                 .setSubject("{'id':1,'name':'张三'}")            //设置主题,可以是JSON数据(一般是要存储的用户信息!)
                 .setIssuedAt(new Date())                        //设置签发日期
                 .signWith(SignatureAlgorithm.HS256, "xzzb");  //设置签名 使用HS256算法,并设置SecretKey(字符串)
 ​
         String compact = jwt.compact();                          //生成 JWT;
         return compact;
     }
 ​
     //解析 JWT
     public static String getToken(String token) {
         //调用 Jwts.parser(); 方法完成解析;
         Claims xzzb = Jwts.parser()
                 .setSigningKey("xzzb")  //明确Secret 解析私钥;
                 .parseClaimsJws(token)  //要解析的Token;
                 .getBody();             //JSON 格式;
 ​
         return xzzb.toString();         //返回!
     }
 }

测试:

运行 main 在这里插入图片描述

类中存在两个方法: createToken() getToken() 创建/解决JWT

  • 多次运行发现 每次运行的Token 都不一样! 因为生成Jwt 里面加了时间
  • 但, 发现了我们可以, 根据 Secret密钥JWT Token 又一次获得数据....

扩:

在这里插入图片描述

JWT 可以定义自定义claims

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

通过: JwtBuilder jwtBuilder = Jwts.builder(); jwtBuilder.addClaims(Map); //可以存储自定义Map 类型数据存储!编码!

JWT + 网关模拟:用户登录

基于上面。JWT Demo 我们可以实现:自定义jwt 的 Token, 并对其进行 解码

思路分析

在这里插入图片描述

  • 1.用户通过访问微服务网关调用微服务,同时携带头文件信息: (Token)
  • 2.在微服务网关这里进行拦截,拦截后获取用户要访问的路径
  • 3.识别用户访问的路径是否需要登录?有些操作不用登录也可以访问, 京东的查看商品... 这种请求一般直接放行! 如果需要,识别用户的身份是否能访问该路径[这里可以基于数据库设计一套权限] 如果需要权限访问,用户已经登录,则放行。 如果需要权限访问,且用户未登录,则提示用户需要登录。
  • 5.用户通过网关访问用户微服务,进行登录验证
  • 6.验证通过后,用户微服务会颁发一个令牌给网关,网关会将用户信息封装到头文件中,并响应用户
  • 7.用户下次访问,携带头文件中的令牌信息即可识别是否登录!

生成令牌工具类

为了方便操作,这里提供了一个便于快速生成 JWT的工具类:JwtUtil.Java

一般定义在公共的 api模块中, 注意需要引入 pom.xml依赖哦! 服务模块引用 api 模块!

JwtUtil.Java

 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;
 ​
 public class JwtUtil {
 ​
     //有效期为
     public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000  一个小时
 ​
     //Jwt令牌信息
     public static final String JWT_KEY = "xzzb";
 ​
     /**
      * 生成加密 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 id        jwt 唯一id
      * @param subject   设置主题,可以是JSON数据(一般是要存储的用户信息!)
      * @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);
         //如果令牌有效期为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();
     }
 ​
     /**
      * 解析令牌数据
      *
      * @param jwt
      * @return
      * @throws Exception
      */
     public static Claims parseJWT(String jwt) throws Exception {
         SecretKey secretKey = generalKey();
         return Jwts.parser()
                 .setSigningKey(secretKey)
                 .parseClaimsJws(jwt)
                 .getBody();
     }
 }

其中包含三个方法: 生成密钥Secret 生成jwt 解析jwt

用户微服务模块: common_userService

这里就是主要完成登录的操作了! 这次没有用户类就直接使用 map了... 的小案例了; 如果是接着上面项目做的建议,把Token限流给关了!

用户serice UserService.Java

     /**
      * @param name 用户名
      * @param pwd 密码
      * @return
      */
     public HashMap<String, Object> userLogin(String name, String pwd) {
         //登录
         if (name.equals("admin") && pwd.equals("123")) {
             HashMap<String, Object> user = new HashMap<>();
             user.put("id", 1);
             user.put("username", name);
             user.put("passwd", pwd);
             return user;
         }
         return null;
     }

controller UserController.Java

     //执行登录方法!
     @PostMapping("/login")
     public HashMap<String, Object> login(String name, String pwd) {
         HashMap<String, Object> user = userService.userLogin(name, pwd);
         if (user != null) {
 ​
             String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(user), null);
             HashMap<String, Object> result = new HashMap<>();
             result.put("code", "1001");
             result.put("token", jwt);
             return result;
         }
         return null;
     }

网关微服模块: gateway_server

修改 TokenFitter.Java

 import com.wsm.jwt.JwtUtil;
 import io.jsonwebtoken.Claims;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.cloud.gateway.filter.GatewayFilterChain;
 import org.springframework.cloud.gateway.filter.GlobalFilter;
 import org.springframework.core.Ordered;
 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.apache.commons.lang.StringUtils;
 import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Mono;
 ​
 //Gate way实现过滤器:
 @Component
 @Slf4j
 public class TokenFitter implements GlobalFilter, Ordered {     //自定义过滤器类 实现GlobalFilter Ordered接口并实现两个方法;
 ​
 ​
     @Override
     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
         //获取request response
         ServerHttpRequest request = exchange.getRequest();
         ServerHttpResponse response = exchange.getResponse();
         //获取请求url 打印!
         String path = request.getURI().getPath();
         System.out.println(path);
 ​
         //判断请求... 对于请求 /user/login  /api/address/get  这请求网关可以直接放行...
         //对于一些操作本身就是不登录也可也访问...   登录请求当然直接放行,本身就是登录才有Token 不登录那里来Token 放行!
         if(path.startsWith("/user/login") || path.startsWith("/api/address/get")){
             System.out.println("Token登录 网关同行!!");
             return chain.filter(exchange);
         }
         //获取请求中的Token       从请求头中获取Token
         String token = request.getHeaders().getFirst("token");
         //不存在 则从请求中查找, 有没有Token
         if(StringUtils.isEmpty(token)){
             token=request.getQueryParams().getFirst("token");
         }
         //还不存在则,拦截器拦截!
         if(StringUtils.isEmpty(token)){
             response.setStatusCode(HttpStatus.UNAUTHORIZED);
             return response.setComplete();
         }
 ​
 ​
         try {
             //验证Token 是否存在/合法...
             Claims claims = JwtUtil.parseJWT(token);        //不合法出现异常,也直接请求拦截!!
         } catch (Exception e) {
             response.setStatusCode(HttpStatus.UNAUTHORIZED);
             return response.setComplete();
         }
         //以上都成立, 放行!!
         return chain.filter(exchange);
     }
 ​
     //过滤顺序级别
     @Override
     public int getOrder() {
         return 0;
     }
 }

结合,网关的过滤器进行 微服网关! Token的操作执行....

测试

在这里插入图片描述

Base64

在这里插入图片描述 为了确保http 数据传递安全, 对数据进行加密!传递的一种 编码/解码技术

BASE64 存在解码所有并不安全! 所以后面又存在一个 加盐的操作! 二次加密/多次加密!(连续多次加密!)

欧克, 终于写完了。。。 上面一系列的操作都来源一个项目:SpringCloudBJ 在这里插入图片描述