学习本课程的前提: 了解Oauth2协议及其密码授权模式,熟悉Spring Security Oauth2和JWT的使用,熟悉Spring Cloud Gateway网关使用。
1. 架构设计分析
1.1 多点登录
1.2 单点登录
1.3 微服务接入网关实现单点登录设计思路
网关整合 OAuth2.0 有两种思路,一种是授权服务器生成令牌, 所有请求统一在网关层验证,判断权限等操作;另一种是由各资源服务处理,网关只做请求转发。 比较常用的是第一种,把API网关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。
网关在认证授权体系里主要负责两件事:
2. 搭建微服务授权中心
授权中心的认证依赖:
- 第三方客户端的信息
- 微服务的信息
- 登录用户的信息
创建微服务tulingmall-auth
2.1 引入依赖
2.2 添加yml配置
2.3 配置授权服务器
基于DB模式配置授权服务器存储第三方客户端的信息
在oauth_client_details中添加第三方客户端信息(client_id client_secret scope等等)
基于内存模式配置授权服务器存储第三方客户端的信息
2.4 配置SpringSecurity
获取会员信息,此处通过feign从tulingmall-member获取会员信息,需要配置feign,核心代码:
修改授权服务配置,支持密码模式
2.5 测试模拟用户登录
授权码模式
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
适用场景:目前市面上主流的第三方验证都是采用这种模式
它的步骤如下:
(A)用户访问客户端,后者将前者导向授权服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,授权服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的”重定向URI”,向授权服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)授权服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
获取到code
密码模式
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为”密码式”(password)。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而授权服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
适用场景:自家公司搭建的授权服务器
测试获取token
测试校验token接口
因为授权服务器的security配置需要携带clientId和clientSecret,可以采用basic Auth的方式发请求
注意: 传参是token
2.6 配置资源服务器
测试携带token访问资源
或者请求头配置Authorization
2.7 Spring Security Oauth2整合JWT
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。 官网:https://jwt.io/
JWT令牌的优点:
- jwt基于json,非常方便解析。
- 可以在令牌中自定义丰富的内容,易扩展。
- 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
- 资源服务使用JWT可不依赖认证服务即可完成授权。
缺点:
JWT令牌较长,占存储空间比较大。
JWT组成
一个JWT实际上就是一个字符串,它由三部分组成,头部(header)、载荷(payload)与签名(signature)。
头部(header)
头部用于描述关于该JWT的最基本的信息:类型(即JWT)以及签名所用的算法(如HMACSHA256或RSA)等。
这也可以被表示成一个JSON对象:
{ “alg”: “HS256”, “typ”: “JWT” }
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷(payload)
第二部分是载荷,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
- 标准中注册的声明(建议但不强制使用)
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- 公共的声明 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
- 私有的声明 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{ “sub”: “1234567890”, “name”: “John Doe”, “iat”: 1516239022 }
然后将其进行base64加密,得到Jwt的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
签名(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret(盐,一定要保密)
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:
var encodedString = base64UrlEncode(header) + ‘.’ + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, ‘fox’); // khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzME
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzME
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
引入依赖
添加JWT配置
在授权服务器配置中指定令牌的存储策略为JWT
密码模式测试:
将access_token复制到https://jwt.io/的Encoded中打开,可以看到会员认证信息
测试校验token
测试获取token_key
测试刷新token
2.8 优化:实现JWT非对称加密(公钥私钥)
第一步:生成jks 证书文件
我们使用jdk自动的工具生成
命令格式
keytool
-genkeypair 生成密钥对
-alias jwt(别名)
-keypass 123456(别名密码)
-keyalg RSA(生证书的算法名称,RSA是一种非对称加密算法)
-keysize 1024(密钥长度,证书大小)
-validity 365(证书有效期,天单位)
-keystore D:/jwt/jwt.jks(指定生成证书的位置和证书名称)
-storepass 123456(获取keystore信息的密码)
-storetype (指定密钥仓库类型)
使用 “keytool -help” 获取所有可用命令
keytool -genkeypair -alias jwt -keyalg RSA -keysize 2048 -keystore D:/jwt/jwt.jks
将生成的jwt.jks文件cope到授权服务器的resource目录下
查看公钥信息
keytool -list -rfc —keystore jwt.jks | openssl x509 -inform pem -pubkey
第二步:授权服务中增加jwt的属性配置类
yml中添加jwt配置
tuling: jwt: keyPairName: jwt.jks keyPairAlias: jwt keyPairSecret: 123123 keyPairStoreSecret: 123123
第三步:修改JwtTokenStoreConfig的配置,支持非对称加密
第四步:扩展JWT中的存储内容
有时候我们需要扩展JWT中存储的内容,根据自己业务添加字段到Jwt中。 继承TokenEnhancer实现一个JWT内容增强器
在JwtTokenStoreConfig中配置TulingTokenEnhancer
在授权服务器配置中配置JWT的内容增强器
1)通过密码模式测试获取token
https://jwt.io/中校验token,可以获取到增强的用户信息,传入私钥和公钥可以校验通过。
2)测试校验token
3. 接入网关服务
在网关服务tulingmall-gateway中配置tulingmall-auth
1)yml中添加对tulingmall-auth的路由
2)编写GateWay的全局过滤器进行权限的校验拦截
认证过滤器AuthenticationFilter#filter中需要实现的逻辑
//1.过滤不需要认证的url,比如/oauth/** //2. 获取token // 从请求头中解析 Authorization value: bearer xxxxxxx // 或者从请求参数中解析 access_token //3. 校验token // 拿到token后,通过公钥(需要从授权服务获取公钥)校验 // 校验失败或超时抛出异常 //4. 校验通过后,从token中获取的用户登录信息存储到请求头中
1)过滤不需要认证的url ,可以通过yml设置不需要认证的url。
网关中引入授权中心配置
- id: tulingmall-auth uri: lb://tulingmall-auth predicates: - Path=/oauth/**
测试: 密码模式 client_id为会员微服务,能够获取到token信息
测试: 会员微服务会员登录逻辑
2) 解析请求,获取token
从请求头中解析 Authorization value: bearer xxxxxxx 或者 从请求参数中解析 access_token
引入依赖
在AuthenticationFilter#filter中实现获取token的逻辑
//2. 获取token // 从请求头中解析 Authorization value: bearer xxxxxxx // 或者从请求参数中解析 access_token //第一步:解析出我们Authorization的请求头 value为: “bearer XXXXXXXXXXXXXX” String authHeader = exchange.getRequest().getHeaders().getFirst(“Authorization”); //第二步:判断Authorization的请求头是否为空 if(StringUtils.isEmpty(authHeader)) { log.warn(“需要认证的url,请求头为空”); throw new GateWayException(ResultCode.AUTHORIZATION_HEADER_IS_EMPTY); }
测试: 通过网关获取用户优惠券信息,因为请求头中不带token信息,所以会抛出异常
3)校验token
拿到token后,通过公钥(需要从授权服务获取公钥)校验,校验失败或超时抛出异常
引入依赖
在AuthenticationFilter#filter中实现校验token的逻辑
//3. 校验token // 拿到token后,通过公钥(需要从授权服务获取公钥)校验 // 校验失败或超时抛出异常 //第三步 校验我们的jwt 若jwt不对或者超时都会抛出异常 Claims claims = JwtUtils.validateJwtToken(authHeader,publicKey);
校验token逻辑
工具类
需要从tulingmall-auth获取公钥,实现公钥获取逻辑
注意: 此处不能直接通过@LoadBalancer配置RestTemplate去获取公钥,思考为什么?
源码参考:
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration
org.springframework.beans.factory.support.DefaultListableBeanFactory#preInstantiateSingletons
测试: 正确的token,通过网关获取用户优惠券信息
错误的token,抛出异常
4)校验通过后,从token中获取的用户登录信息存储到请求头中
在AuthenticationFilter#filter中,将从token中获取的用户登陆信息存储到请求头中
//4. 校验通过后,从token中获取的用户登录信息存储到请求头中 //第四步 把从jwt中解析出来的 用户登陆信息存储到请求头中 ServerWebExchange webExchange = wrapHeader(exchange,claims);
解析用户登录信息存储到请求头中
...
...
Copyright 2021 sunfy.top ALL Rights Reserved