ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微服务网关和Jwt令牌 入门学习!

微服务网关和Jwt令牌 入门学习!

微服务网关和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编码http://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

packagecom.wsm.jwt;importio.jsonwebtoken.Claims;importio.jsonwebtoken.JwtBuilder;importio.jsonwebtoken.Jwts;importio.jsonwebtoken.SignatureAlgorithm;importjava.util.Date;//JWT 测试类publicclassTestJWT{publicstaticvoidmain(String[]args){//打印: 创建的JWTStringjwt=createToken();System.out.println(jwt);System.out.println("---------------------------------------------------------------------------------------");//打印: JWT的解析数据System.out.println(getToken(jwt));}//创建 JWTpublicstaticStringcreateToken(){JwtBuilderjwt=Jwts.builder().setId("110")//设置唯一编号.setSubject("{'id':1,'name':'张三'}")//设置主题,可以是JSON数据(一般是要存储的用户信息!).setIssuedAt(newDate())//设置签发日期.signWith(SignatureAlgorithm.HS256,"xzzb");//设置签名 使用HS256算法,并设置SecretKey(字符串)Stringcompact=jwt.compact();//生成 JWT;returncompact;}//解析 JWTpublicstaticStringgetToken(Stringtoken){//调用 Jwts.parser(); 方法完成解析;Claimsxzzb=Jwts.parser().setSigningKey("xzzb")//明确Secret 解析私钥;.parseClaimsJws(token)//要解析的Token;.getBody();//JSON 格式;returnxzzb.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

importio.jsonwebtoken.Claims;importio.jsonwebtoken.JwtBuilder;importio.jsonwebtoken.Jwts;importio.jsonwebtoken.SignatureAlgorithm;importjavax.crypto.SecretKey;importjavax.crypto.spec.SecretKeySpec;importjava.util.Base64;importjava.util.Date;publicclassJwtUtil{//有效期为publicstaticfinalLongJWT_TTL=3600000L;// 60 * 60 *1000 一个小时//Jwt令牌信息publicstaticfinalStringJWT_KEY="xzzb";/** * 生成加密 secretKey * * @return */publicstaticSecretKeygeneralKey(){byte[]encodedKey=Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes());SecretKeykey=newSecretKeySpec(encodedKey,0,encodedKey.length,"AES");returnkey;}/** * * @param id jwt 唯一id * @param subject 设置主题,可以是JSON数据(一般是要存储的用户信息!) * @param ttlMillis 设置过期时间....毫秒; * @return */publicstaticStringcreateJWT(Stringid,Stringsubject,LongttlMillis){//指定算法SignatureAlgorithmsignatureAlgorithm=SignatureAlgorithm.HS256;//当前系统时间longnowMillis=System.currentTimeMillis();//令牌签发时间Datenow=newDate(nowMillis);//如果令牌有效期为null,则默认设置有效期1小时if(ttlMillis==null){ttlMillis=JwtUtil.JWT_TTL;}//令牌过期时间设置longexpMillis=nowMillis+ttlMillis;DateexpDate=newDate(expMillis);//生成秘钥SecretKeysecretKey=generalKey();//封装Jwt令牌信息JwtBuilderbuilder=Jwts.builder().setId(id)//唯一的ID.setSubject(subject)// 主题 可以是JSON数据.setIssuer("admin")// 签发者.setIssuedAt(now)// 签发时间.signWith(signatureAlgorithm,secretKey)// 签名算法以及密匙.setExpiration(expDate);// 设置过期时间returnbuilder.compact();}/** * 解析令牌数据 * * @param jwt * @return * @throws Exception */publicstaticClaimsparseJWT(Stringjwt)throwsException{SecretKeysecretKey=generalKey();returnJwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();}}

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

用户微服务模块: common_userService

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

用户sericeUserService.Java

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

controllerUserController.Java

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

网关微服模块: gateway_server

修改TokenFitter.Java

importcom.wsm.jwt.JwtUtil;importio.jsonwebtoken.Claims;importlombok.extern.slf4j.Slf4j;importorg.springframework.cloud.gateway.filter.GatewayFilterChain;importorg.springframework.cloud.gateway.filter.GlobalFilter;importorg.springframework.core.Ordered;importorg.springframework.http.HttpStatus;importorg.springframework.http.server.reactive.ServerHttpRequest;importorg.springframework.http.server.reactive.ServerHttpResponse;importorg.springframework.stereotype.Component;importorg.apache.commons.lang.StringUtils;importorg.springframework.web.server.ServerWebExchange;importreactor.core.publisher.Mono;//Gate way实现过滤器:@Component@Slf4jpublicclassTokenFitterimplementsGlobalFilter,Ordered{//自定义过滤器类 实现GlobalFilter Ordered接口并实现两个方法;@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){//获取request responseServerHttpRequestrequest=exchange.getRequest();ServerHttpResponseresponse=exchange.getResponse();//获取请求url 打印!Stringpath=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登录 网关同行!!");returnchain.filter(exchange);}//获取请求中的Token 从请求头中获取TokenStringtoken=request.getHeaders().getFirst("token");//不存在 则从请求中查找, 有没有Tokenif(StringUtils.isEmpty(token)){token=request.getQueryParams().getFirst("token");}//还不存在则,拦截器拦截!if(StringUtils.isEmpty(token)){response.setStatusCode(HttpStatus.UNAUTHORIZED);returnresponse.setComplete();}try{//验证Token 是否存在/合法...Claimsclaims=JwtUtil.parseJWT(token);//不合法出现异常,也直接请求拦截!!}catch(Exceptione){response.setStatusCode(HttpStatus.UNAUTHORIZED);returnresponse.setComplete();}//以上都成立, 放行!!returnchain.filter(exchange);}//过滤顺序级别@OverridepublicintgetOrder(){return0;}}

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

测试


Base64


为了确保http 数据传递安全, 对数据进行加密!传递的一种编码/解码技术

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

欧克, 终于写完了。。。 上面一系列的操作都来源一个项目:SpringCloudBJ

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进