Gateway + Security + OAuth 2.0 + JWT 实现统一的认证授权
1、前言
1.1、OAuth 2.0 介绍
1.2、OAuth 2.0 与 JWT 的关系
- OAuth 2.0 是一种认证授权的协议规范
- JWT 是基于 Token 的安全认证协议的实现
OAuth 2.0 的认证服务器签发的 Token 可以使用 JWT 来实现,JWT 轻量且安全。
1.3、基于 OAuth 2.0 认证授权的框架
OAuth 的官网提供了很多开发框架,分为服务器端和客户端,其中服务端和客户端都支持的 Java 框架有四个:Apache Oltu、Spring Security OAuth、Restlet Framework、Keycloak。值得一提的是,Keycloak 为现代应用和分布式服务提供了一套完整的认证授权管理开源解决方案,是一个独立的认证授权服务器;主要是基于 OAuth 2.0 协议实现,同时提供了多种语言库,可以很快速地根据业务需求将 Keycloak 集成到企业项目中去使用。
2、Gateway + Security + OAuth 2.0 + Knife4j
2.1、认证授权流程
- 用户携带账号密码通过网关服务请求认证服务
- 认证通过后,授权服务颁发身份令牌给客户端,并将身份令牌储存在 Redis/MySQL 中
- 用户携带身份令牌请求资源服务(微服务应用),必经网关服务
- 网关服务获取客户端带来的令牌和 Redis/MySQL 中的令牌进行比对校验
- 网关服务校验通过后,转发 HTTP 请求,资源服务(微服务应用)获取到身份令牌,进行身份校验和鉴权,通过后处理系统业务
- 资源服务(微服务应用)将响应数据返回给客户端
提示
Gateway 校验并解析外部传递过来的身份令牌后,可以获取到用户信息(身份 + 权限),并将用户信息写入到 HTTP Header 里,让后续的微服务可以方便地得到用户信息
2.2、应用架构设计
1 | ├── common 基础模块 |
认证服务负责认证和授权,网关服务只负责校验认证(Token)、请求转发和统一解析用户信息,业务模块负责校验认证(Token)和鉴权。由于 gateway、shop 模块没有使用 MySQL 存储,暂时无法实现注销 Token 的功能;若两者都引入 MySQL 存储,感觉应用有点重(依赖 ORM 框架,而且可能出现多数据源的场景),或者需要使用 Redis 存储来替代 MySQL 存储。终上所述,如果不考虑提供注销 Token 的功能,该方案还是可以接受的。
2.3、下载案例代码
3、Gateway + Security + OAuth 2.0 + JWT
3.1、应用架构设计
1 | ├── micro-oauth2-api 受保护的API服务,用户被网关服务鉴权通过后可以访问该服务,不整合Spring Security + Oauth2.0 |
认证服务负责认证和授权,网关负责校验认证(Token)和鉴权,其他 API 服务则只负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他 API 服务只是单纯地提供服务而没有任何安全相关逻辑。这种应用架构要求所有 HTTP 请求都必须经过网关服务,同时任何 API 服务都不能暴露在外网,否则会存在极大的安全隐患。
3.2、下载案例代码
4、延伸内容
4.1、Knife4j 整合 OAuth 2.0
4.2、网关是否适合进行认证与鉴权
第一派系:网关不适合进行业务操作,所以做个简单的去 Redis 比较 Token 校验是正确思路,剩下的交给后续的服务做
- 优点:不用在网关服务引入多余的 Spring Security、ORM 框架
- 缺点:第二派系的优点
第二派系:
- 网关用来认证与鉴权,登录蹦不蹦已经不是问题了,毕竟网关宕机,代表系统瘫痪了
- 优点:权限方面的代码会很好写,控制 URL 即可
- 缺点:第一派系的优点
第三派系:
- 网关用来认证与鉴权,但鉴权所需的数据(用户、角色、权限)从 Caffeine + Redis 缓存中加载,而认证授权服务启动时,负责将 MySQL 中权限相关的数据提前加载到 Redis
- 优点:第一与第二派系的优点
- 缺点:严重依赖 Redis,一般需要部署维护 Redis 集群,如果 Redis 集群宕机,可能会造成网关鉴权功能不可用或者系统瘫痪
4.3、基于 HTTP Header 传递用户信息的缺点
基于 Spring Cloud 的技术体系(RESTful 接口规范),Gateway 校验并解析外部传递过来的 Access Token 后,获取到用户信息(身份 + 权限),同时将用户信息写入到 HTTP Header 里,让后面的业务系统接收到 Gateway 转发过来的请求后,也能从 HTTP Header 里得到相应的用户信息。但这种方式对使用了 RPC 调用的场景不适用,因为在 RPC 调用里,无法从 HTTP Header 获取到任何数据,该问题的讨论可以关注这里。