Oauth2协议及其密码授权模式

将应用拆分成一个一个的服务,每一个服务都是提供特定的功能,一个服务只做一件事

Posted by Sunfy on 2021-11-25
Words 3.3k and Reading Time 12 Minutes
Viewed Times
Viewed Times
Visitors In Total

学习本课程的前提: 了解Oauth2协议及其密码授权模式,熟悉Spring Security Oauth2和JWT的使用,熟悉Spring Cloud Gateway网关使用。

1. 架构设计分析

1.1 多点登录

img

1.2 单点登录

img

1.3 微服务接入网关实现单点登录设计思路

网关整合 OAuth2.0 有两种思路,一种是授权服务器生成令牌, 所有请求统一在网关层验证,判断权限等操作;另一种是由各资源服务处理,网关只做请求转发。 比较常用的是第一种,把API网关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当前登录用户信息给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。

网关在认证授权体系里主要负责两件事:(1)作为OAuth2.0的资源服务器角色,实现接入方访问权限拦截。 (2)令牌解析并转发当前登录用户信息(明文token)给微服务 微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事: (1)用户授权拦截(看当前用户是否有权访问该资源) (2)将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)

img

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 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

适用场景:目前市面上主流的第三方验证都是采用这种模式

img

它的步骤如下:

(A)用户访问客户端,后者将前者导向授权服务器。

(B)用户选择是否给予客户端授权。

(C)假设用户给予授权,授权服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。

(D)客户端收到授权码,附上早先的”重定向URI”,向授权服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。

(E)授权服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

http://localhost:9999/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all

获取到code

img

img

密码模式

如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为”密码式”(password)。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而授权服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

适用场景:自家公司搭建的授权服务器

测试获取token

http://localhost:9999/oauth/token?username=test&password=test&grant_type=password&client_id=client&client_secret=123123&scope=all

img

测试校验token接口

img

因为授权服务器的security配置需要携带clientId和clientSecret,可以采用basic Auth的方式发请求

img

注意: 传参是token

img

2.6 配置资源服务器

测试携带token访问资源

img

或者请求头配置Authorization

img

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)。

img

头部(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了。

引入依赖

org.springframework.security spring-security-jwt 1.0.9.RELEASE

添加JWT配置

在授权服务器配置中指定令牌的存储策略为JWT

密码模式测试:

http://localhost:9999/oauth/token?username=test&password=test&grant_type=password&client_id=client&client_secret=123123&scope=all

img

将access_token复制到https://jwt.io/的Encoded中打开,可以看到会员认证信息

img

测试校验token

img

img

测试获取token_key

img

测试刷新token

img

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

img

将生成的jwt.jks文件cope到授权服务器的resource目录下

img

查看公钥信息

keytool -list -rfc —keystore jwt.jks | openssl x509 -inform pem -pubkey

img

第二步:授权服务中增加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

img

https://jwt.io/中校验token,可以获取到增强的用户信息,传入私钥和公钥可以校验通过。

img

2)测试校验token

img

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信息

img

测试: 会员微服务会员登录逻辑

img

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信息,所以会抛出异常

img

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,通过网关获取用户优惠券信息

img

错误的token,抛出异常

img

4)校验通过后,从token中获取的用户登录信息存储到请求头中

在AuthenticationFilter#filter中,将从token中获取的用户登陆信息存储到请求头中

//4. 校验通过后,从token中获取的用户登录信息存储到请求头中 //第四步 把从jwt中解析出来的 用户登陆信息存储到请求头中 ServerWebExchange webExchange = wrapHeader(exchange,claims);

解析用户登录信息存储到请求头中


Copyright 2021 sunfy.top ALL Rights Reserved

...

...

00:00
00:00