Oauth2(上)

一、简介

1.1 业务场景

公司原来使用的是自建的用户登陆系统,但是只有登陆功能,没有鉴权功能。

现公司有如下业务场景:

  1. 需要接入各大智能音箱,音箱需要通过标准的Oauth2授权码模式获取令牌从而拿到服务器资源;
  2. 后台管理界面需要操作权限 ;
  3. 后期要做开发者平台,需要授权码模式。

所以在以上业务场景下开始自建Oauth2框架,框架需要兼容公司原有的用户登陆系统。

1.3 Oauth2框架

Oauth2扩展了Security的授权机制。

二、相关概念

2.1 单点登陆

即一个token可以访问多个微服务。

2.2 授权方式

①授权码模式

第三方应用通过客户端进行登录,如果通过github账号进行登录,那么第三方应用会跳转到github的资源服务器地址,携带了client_id、redirect_uri、授权类型(code模式)和state(防止csrf攻击的token,可以不填)。随后资源服务器会重定向到第三方应用url并携带code和state参数,随后第三方应用携带code、client_id和client_secret再去请求授权服务器,先验证code是否有效,有效则发放认证token,携带该token可以取资源服务器上的资源。

授权码模式(authorization code)是功能最完整、流程最严密的授权模式,code保证了token的安全性,即使code被拦截,由于没有app_secret,也是无法通过code获得token的。

如当我们登陆CSDN的时候,可以使用第三方Github账号密码进行登陆并获取头像等信息。

首先需要注册CSDN的信息

  • 应用名称
  • 应用网站
  • 重定向标识 redirect_uri
  • 客户端标识 client_id
  • 客户端秘钥 client_secret

如github认证服务器中可以对客户端进行注册,需要填写应用名称、网站地址、应用描述和重定向地址。这样github就记录了该应用并产生一个client_id和client_secret。

获取令牌流程图如下:

优点

  • 不会造成我们的账号密码泄漏
  • Token不会暴露给前端浏览器

看下测试实例

# 指定授权方式为code模式,携带客户端id、重定向地址等信息访问。
GET https://oauth.marssenger.com/oauth/authorize?client_id=c1&response_type=code&scope=ROLE_ADMIN&redirect_uri=http://www.baidu.com
# 会跳转到登陆页面,输入账号密码。如果信息正常,会携带code跳转到重定向地址。
https://www.baidu.com/?code=YEQCZO
# 然后携带code访问授权服务器,就可以获取到令牌了。
https://oauth.marssenger.com/oauth/token?client_id=c1&client_secret=123456&grant_type=authorization_code&code=YEQCZO&redirect_uri=http://www.baidu.com
# 最终得到令牌如下
{
    "access_token": "ey......Jgw",
    "token_type": "bearer",
    "refresh_token": "ey......J-A",
    "expires_in": 86399,
    "scope": "ROLE_ADMIN",
    "cre": 1622694842,
    "jti": "fd970e49-082f-492e-9418-b21b45452f2d"
}

access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
cre:自定义添加的令牌创建日期
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

②简化模式

第三方应用通过客户端进行登录,通过github账号访问资源服务器,认证完成后重定向到redirect_uri并携带token,省略了通过授权码再去获取token的过程。

适用于公开的浏览器单页应用,令牌直接从授权服务器返回,不支持刷新令牌,且没有code安全保证,令牌容易因为被拦截窃听而泄露。

看下测试实例

# 指定授权方式为token模式,携带客户端id、重定向地址等信息访问。
GET https://oauth.marssenger.com/oauth/authorize?client_id=c1&response_type=token&scope=ROLE_ADMIN&redirect_uri=http://www.baidu.com
# 直接获取到了access_token,不支持刷新令牌
https://www.baidu.com/#access_token=ey......u0Q&token_type=bearer&expires_in=86399&cre=1622695736&jti=13a726b2-70d4-421e-8d5b-3a26233214cc

③密码模式

直接向第三方应用提供资源服务器的账号密码,第三方应用通过账号密码请求获取资源服务器上的资源。会向第三方应用暴露账号密码,除非特别信任该应用。

看下测试实例

# 指定授权方式为password,携带客户端id密码、用户账号密码等信息访问。
GET https://oauth.marssenger.com/oauth/token?client_id=c1&client_secret=123456&grant_type=password&username=admin&password=abc123&user_type=admin
# 获取令牌
{
    "access_token": "ey......_SA",
    "token_type": "bearer",
    "refresh_token": "ey......brw",
    "expires_in": 86399,
    "scope": "ROLE_ADMIN ROLE_APPLICATION",
    "cre": 1622691146,
    "jti": "c31a69bc-0eba-4e93-8f78-c0f8c04a2b11"
}

④客户端模式

不通过资源所有者,直接以第三方应用的秘钥和id获取资源服务器的token。

看下测试实例

# 指定授权方式为client_credentials,携带客户端id和密码进行访问。
GET https://oauth.marssenger.com/oauth/token?client_id=c1&client_secret=123456&grant_type=client_credentials
# 获取令牌
{
    "access_token": "ey......zMQ",
    "token_type": "bearer",
    "expires_in": 86399,
    "scope": "ROLE_ADMIN ROLE_APPLICATION",
    "cre": 1622697938,
    "jti": "8f962403-c7d8-4d4b-974f-7896a0a31389"
}

2.3 JWT令牌

Oauth2原生的token是一串随机的hash字符串,存在两个问题:

  • token验证需要远程调用认证服务器,效率低
  • token无法携带用户数据;

因此使用JWT来取代原生的token。

JWT全称为Json Web Token,使用一种特殊格式的token,token有特定含义,分为三部分:

  • 头部Header:包括令牌的类型(即JWT)及使用的哈希算法(如HMAC、SHA256或RSA)。
  • 载荷Payload:存放有效信息,如iss(签发者)、exp(过期时间)、sub(授权用户)和创建时间等,也可以自定义字段方便扩展。
  • 签名Signature:是对前两部分的数字签名,防止被篡改。

这三部分均用base64Url进行编码,并使用.进行分隔,一个典型的jwt格式的token类似xxxxx.yyyyy.zzzzz。认证服务器通过对称或非对称的加密方式利用payload生成signature,并在header中申明签名方式。这样jwt可以实现分布式的token验证功能,即资源服务器通过事先维护好的对称或者非对称密钥(非对称的话就是认证服务器提供的公钥),直接在本地验证token,这种去中心化的验证机制非常适合分布式架构。jwt相对于传统的token来说,解决以下两个痛点:

  • 通过验证签名,对于token的验证可以直接在资源服务器本地完成,不需要连接认证服务器;
  • 在payload中可以包含用户相关信息,这样就轻松实现了token和用户信息的绑定;
    如果认证服务器颁发的是jwt格式的token,那么资源服务器就可以直接自己验证token的有效性并绑定用户,这无疑大大提升了处理效率且减少了单点隐患。

总结:Header申明算法、Payload是用户信息、对Payload加密得到Signature,三部分用base64编码后通过"."连接组合为token;验证token时只需要根据header中的算法对Payload(默认是HMAC SHA256算法)进行验证。

JWT优点:

  • jwt基于json,非常方便使用;
  • 可以在令牌中自定义丰富的内容,易扩展;
  • 通过非对称加密算法和数字签名技术,JWT防止篡改,安全性高;
  • 资源服务使用JWT可不依赖认证服务器即可完成授权。

JWT缺点:

  • 在有效期内,token是无法作废的,用户的签退更多是一个客户端的签退,服务端token仍然有效,你只要使用这个token,仍然可以登陆系统。另外一个问题是续签问题,当然你也可以通过redis去记录token状态,并在用户访问后更新这个状态,但这就是硬生生把jwt的无状态搞成有状态了,而这些在传统的session+cookie机制中都是不需要去考虑的。

JWT安全加强

  • 避免网络劫持,HTTP协议使用header传递JWT容易泄露,使用HTTPS协议传输更安全。
  • 私钥存放在服务器端,保证服务器不被攻破。
  • JWT可以被暴力破解,所以需要保证秘钥复杂度,定期更换秘钥。

以上是理论,下面来看结合实际

# 通过密码模式请求已经搭建完成的授权服务器
POST https://oauth.marssenger.com/oauth/token?client_id=c1&client_secret=123456&grant_type=password&username=admin&password=abc123&user_type=admin

# 得到令牌如下,token因为太长省略了部分。
{
    "access_token": "ey......t_SA",
    "token_type": "bearer",
    "refresh_token": "ey......mbrw",
    "expires_in": 86399,
    "scope": "ROLE_ADMIN ROLE_APPLICATION",
    "cre": 1622691146,
    "jti": "c31a69bc-0eba-4e93-8f78-c0f8c04a2b11"
}

对其中的access_token进行解析:

# 令牌中的完整access_token如下
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiIzNyIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX0FQUExJQ0FUSU9OIl0sImNyZSI6MTYyMjY5MTE0NiwiZXhwIjoxNjIyNzc3NTQ2LCJhdXRob3JpdGllcyI6WyJiYWNrOnVzZXI6dXBkYXRlIiwiYmFjazpjb250ZW50OnVwZGF0ZSIsImJhY2s6Y29udGVudDpsaXN0IiwiYmFjazp1c2VyOmxpc3QiLCJiYWNrOnN5czphbGwiLCJST0xFX3VzZXIiLCJiYWNrOmNvbnRlbnQ6YWxsIiwiYmFjazpjb250ZW50OmFkZCIsImJhY2s6dXNlcjphbGwiLCJiYWNrOnVzZXI6YWRkIiwiYmFjazp1c2VyOmRlbGV0ZSIsImJhY2s6Y29udGVudDpkZWxldGUiXSwianRpIjoiYzMxYTY5YmMtMGViYS00ZTkzLThmNzgtYzBmOGMwNGEyYjExIiwiY2xpZW50X2lkIjoiYzEifQ.lZXI8rhN6XUgbHaXZa6zK2GAdI2nruT_LZpAtBMRIIuQddKu8827juVBqx498Orb3MNC7RzFV_cv365SlE_TaUJ09tW0jnd-8kdPaRIGt11SIg2Jik8EQ3l_t8_XOtZhq6TUjKfPZQfo0egXUO70QzyC9JPFGZQPAUYvwNCZMC0qBkYuI4paUWQoMh0yML25eVMIMf_fTPgxFFicEVzc78yO4PqUrXc-WGlZkRRx6EPyrIhtXVY0uHmBORKlnbPcVDVkcYnLXTUcVumtWRGUw4zsHjGLAWkiUC2ISvBUl5DVQStd9B5R_FzLWuWLNlskFaZ8npbKA9XuUH_CKxt_SA"

# access_token由两个"."分割为三部分,分别为Header,Payload和Signature,通过base64解密Header和Payload后得到:
Header:{"alg":"RS256","typ":"JWT"}
Payload:{
    "aud":[
        "res1"
    ],
    "user_name":"37",
    "scope":[
        "ROLE_ADMIN",
        "ROLE_APPLICATION"
    ],
    "cre":1622691146,
    "exp":1622777546,
    "authorities":[
        "back:user:update",
        "back:content:update",
        "back:content:list",
        "back:user:list",
        "back:sys:all",
        "ROLE_user",
        "back:content:all",
        "back:content:add",
        "back:user:all",
        "back:user:add",
        "back:user:delete",
        "back:content:delete"
    ],
    "jti":"c31a69bc-0eba-4e93-8f78-c0f8c04a2b11",
    "client_id":"c1"
}

# Signature为数字签名,即对内容的摘要通过私钥进行加密,然后在客户端通过公钥解密并与摘要进行对比,保证内容不会被篡改。Header中携带了使用的加密算法信息。

2.4 网关

有些架构方案中,认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。

但是个人觉得这样网关承担的责任太大,且每次业务逻辑改变后需要同时修改网关的代码或者将数据库刷新到网关内存中。所以为了方便起见,目前还是将权限信息和鉴权逻辑放到自己的业务中。优化工作后续再做,反正对于整套Oauth2搭建来说,将认证和鉴权工作放到gateway中只是小意思。

目前网关最大的作用就是路由请求了,同时可以设置黑名单进行过滤。

2.5 密钥配置

由于需要签名摘要,所以认证服务器需要配置密钥,这里使用RS256进行加密。配置密钥的方式有好多种。

  • 方式一:最简单的就是直接将公钥和私钥写在认证服务配置文件中,在项目启动时从配置文件读取。这样公钥可以直接写在资源服务中,也可以通过提供接口的方式让资源服务来请求获取公钥。

  • 方式二:通过生成SSL证书的方式,将证书放到资源路径下,然后认证服务运行时读取并解析证书,获取公钥和私钥。这种情况下公钥就必须通过提供接口的方式让资源服务来请求获取公钥。我们还是采取这种方式,因为我觉得更加优雅。

  • 方式三:通过jjwt框架生成密钥,每次重启都会更换随机密钥。可以开放公钥接口给其他资源服务。这种很方便,但是并不推荐,因为每次重启认证服务都需要重启资源服务,且会导致之前的token全部失效。

2.6 服务划分

将Oauth2服务划分为了两部分,一个是认证服务,一个是用户中心,就是将用户相关的部分拿出来新建一个用户服务。所有令牌相关的操作都在认证服务中完成,所有用户相关的操作都在用户中心完成。

认证服务需要访问用户的信息,可以通过Feign调用用户中心的接口获取资源;用户中心用于处理用户的相关操作,所以是一个资源服务,外部请求需要鉴权后才能进行操作。

三、部署

3.1 建表语句

-- used in tests that use HSQL

DROP TABLE IF EXISTS oauth_client_details;
CREATE TABLE oauth_client_details (
  client_id VARCHAR(256) NOT NULL COMMENT '客户端标识',
  resource_ids VARCHAR(256) NULL DEFAULT NULL COMMENT '接入资源列表',
  client_secret VARCHAR(256) NULL DEFAULT NULL COMMENT '客户端秘钥',
  scope VARCHAR(256) NULL DEFAULT NULL COMMENT '客户端权限',
  authorized_grant_types VARCHAR(256) NULL DEFAULT NULL COMMENT '授权模式',
  web_server_redirect_uri VARCHAR(256) NULL DEFAULT NULL COMMENT '重定向地址',
  authorities VARCHAR(256) NULL DEFAULT NULL COMMENT '指定用户的权限范围,如果授权的过程需要用户登陆,该字段不生效,implicit和client_credentials需要',
  access_token_validity int(11) NULL DEFAULT NULL COMMENT '令牌有效时间',
  refresh_token_validity int(11) NULL DEFAULT NULL COMMENT '更新令牌有效时间',
  additional_information VARCHAR(4096) COMMENT '可空',
  autoapprove VARCHAR(256) COMMENT '是否手动确认授权,默认false',
  PRIMARY KEY (client_id) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '接入客户端信息';

DROP TABLE IF EXISTS oauth_code;
CREATE TABLE oauth_code (
    create_time timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    code varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
    authentication blob NULL,
    INDEX code_index(code) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

create table oauth_client_token (

  token_i
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pxr007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值