畅购商城文章系列
畅购商城:分布式文件系统FastDFS
畅购商城:商品的SPU和SKU概念
畅购商城:Lua、OpenResty、Canal实现广告缓存
畅购商城:微服务网关和JWT令牌(上)
畅购商城:微服务网关和JWT令牌(下)
畅购商城:Spring Security Oauth2 JWT(上)
畅购商城:Spring Security Oauth2 JWT(下)
畅购商城:购物车
畅购商城:订单
畅购商城:微信支付
畅购商城:秒杀(上)
畅购商城:秒杀(下)
畅购商城:分布式事务
畅购商城:集群高可用
目标
-
了解加密算法
-
了解JWT鉴权的介绍
-
掌握JET鉴权的 [权限认证] 的使用
使用JWT令牌来存储用户登录信息,在微服务网关中识别登录信息(用户的身份) -
掌握网关使用JWT进行校验
1. 加密算法(了解)
由于在学习JWT的时候会涉及使用很多加密算法, 简单了解就可以。
加密算法种类有:
1.1 可逆加密算法
加密后, 密文可以反向解密得到密码原文。
1.1.1 对称加密
【文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥】
解释:
在对称加密算法中,数据发信方将明文和加密密钥一起经过特殊的加密算法处理后,使其变成复杂的加密密文发送出去,收信方收到密文后,若想解读出原文,则需要使用加密时用的密钥以及相同加密算法的逆算法对密文进行解密,才能使其回复成可读明文。在对称加密算法中,使用的密钥只有一个,收发双方都使用这个密钥,这就需要解密方事先知道加密密钥。
优点: 对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。
缺点: 没有非对称加密安全.
用途: 一般用于保存用户手机号、身份证等敏感但能解密的信息。
常见的对称加密算法有: AES、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256
1.1.2 非对称加密
【两个密钥:公开密钥(publickey)和私有密钥,公有密钥加密,私有密钥解密】
解释: 同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端。
加密与解密:
- 私钥加密,持有私钥或公钥才可以解密
- 公钥加密,持有私钥才可解密
签名:
- 私钥签名, 持有公钥进行验证是否被篡改过。
优点: 非对称加密与对称加密相比,其安全性更好;
缺点: 非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
用途: 一般用于签名和认证。私钥服务器保存, 用来加密, 公钥客户拿着用于对于令牌或者签名的解密或者校验使用.
常见的非对称加密算法有: RSA、DSA(数字签名用)、ECC(移动设备用)、RS256(采用SHA-256的RSA签名)
1.2 不可逆加密算法
解释: 一旦加密就不能反向解密得到密码原文。
种类: Hash加密算法, 散列算法, 摘要算法等
用途:一般用于效验下载文件正确性,一般在网站上下载文件都能见到;存储用户敏感信息,如密码、 卡号等不可解密的信息。
常见的不可逆加密算法有: MD5、SHA、HMAC
1.3 Base64编码
- Base64是网络上最常见的用于传输8Bit字节代码的编码方式之一。
- Base64编码可用于在HTTP环境下传递较长的标识信息。
- 采用Base64编码解码具有不可读性,即所编码的数据不会被人用肉眼所直接看到。
注意:Base64只是一种编码方式,不算加密方法。
2. JWT 实现微服务鉴权
2.1 什么是微服务鉴权
我们之前已经搭建过了网关,使用网关在系统中比较适合进行权限校验。
那么我们可以采用JWT的方式来实现鉴权校验。
2.2 什么是JWT
JWT总结(重要):
JWT是用于微服务之间传递用户信息的一段加密字符串,该字符串是一个JSON格式,各个微服务可以根据该JSON字符串识别用户的身份信息,也就是该JSON字符串中会封装用户的身份信息。
Tip: JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
2.3 JWT的构成
一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。
2.3.1 头部(Header)
头部: 指定了令牌类型和令牌签名的算法
加密方式:Base64
头部用于描述关于该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 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码。
2.3.2 载荷(playload)
载荷(可以解密)总结:
包括三部分:1.标准中注册的声明(参与令牌校验)
2. 公共的声明(不参与令牌校验)
3. 私有声明(不参与令牌校验)
1+2+3 -> Base加密
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
(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
2.3.3 签证(signature)
签证总结:
Base64(头)+ Base64(载荷)+ 秘钥(盐)-> 加密:采用头中指定的算法进行加密 -> 密文 -> 签名
签名的作用:用于校验数据是否被篡改
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了。
2.4 JWT的介绍和使用
JWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
官方文档:https://github.com/jwtk/jjwt
2.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() );
}
}
2.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);
}
2.4.3 设置过期时间
有很多时候,我们并不希望签发的TOKEN是永久生效的,所以我们可以为TOKEN添加一个过期时间。
(1)TOKEN过期设置
.setExpiration(date)//用于设置过期时间 ,参数为Date类型数据
(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);
}
2.4.4 自定义claims
我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。
创建测试类,并设置测试方法:
创建token:
解析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);
}
2.5 鉴权处理
2.5.1 思路分析
微服务网关:获取令牌数据①参数②头③Cookie
1.用户通过访问微服务网关调用微服务,同时携带头文件信息
2.在微服务网关这里进行拦截,拦截后获取用户要访问的路径
3.识别用户访问的路径是否需要登录,如果需要,识别用户的身份是否能访问该路径[这里可以基于数据库设计一套权限]
4.如果需要权限访问,用户已经登录,则放行
5.如果需要权限访问,且用户未登录,则提示用户需要登录
6.用户通过网关访问用户微服务,进行登录验证
7.验证通过后,用户微服务会颁发一个令牌给网关,网关会将用户信息封装到头文件中,并响应用户
8.用户下次访问,携带头文件中的令牌信息即可识别是否登录
2.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,修改登录的方法:
/***
* 用户登录
*/
@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,"账号或者密码错误!");
}
2.5.3 网关过滤器拦截请求处理
拷贝JwtUtil到changgou-gateway-web中
2.5.3 自定义全局过滤器(实现用户权限鉴别)
流程:
#获取用户令牌信息
(1)头文件中
(2)参数获取令牌
(3)Cookie中
#如果没有令牌,则拦截
(1)设置没有权限的状态码
(2)响应空数据
#如果有令牌,则校验令牌是否有效
#无效则拦截
#有效放行
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;
}
}
2.5.5 配置过滤规则
修改网关系统的yml文件:
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
2.6 会话保持
用户每次请求的时候,我们都需要获取令牌数据,方法有多重,可以在每次提交的时候,将数据提交到头文件中,也可以将数据存储到Cookie中,每次从Cookie中校验数据,还可以每次将令牌数据以参数的方式提交到网关,这里面采用Cookie的方式比较容易实现。
2.6.1 登录封装Cookie
修改user微服务,每次登录的时候,添加令牌信息到Cookie中,修改changgou-service-user的com.changgou.user.controller.UserController
的 login
方法,代码如下:
2.6.2 过滤器获取令牌数据
每次在网关中通过过滤器获取Cookie中的令牌,然后对令牌数据进行解析,修改微服务网关changgou-gateway-web中的AuthorizeFilter,代码如下:
登录后测试,可以识别用户身份,不登录无法识别。
2.6.3 添加Header信息
我们还可以在Gateway的全局过滤器中添加请求头信息,例如可以讲令牌信息添加到请求头中,在微服务中获取头信息,如下代码:
修改微服务网关中的AuthorizeFilter过滤器,在令牌信息校验那块将令牌加入到请求头中,如下代码:
在changgou-service-user微服务的UserController的findAll方法中获取请求头测试,代码如下:
总结
了解加密算法、JWT鉴权的介绍,掌握JET鉴权的使用,使用JWT令牌来存储用户登录信息,在微服务网关中识别登录信息,掌握网关使用JWT进行校验。