获取用户信息
[一]背景
用户信息是任何信息系统关键的核心信息,系统中很多业务功能都会涉及使用或关联用户信息。
在传统的、非前后端分离的系统中,用户登录系统之后,用户的相关信息就会存储在 Session 中(或者扩展的存储中,例如:Redis 或 Cache 等),通过用户的唯一标识就可以随时随地的、便捷的获取到用户的信息。
在前后端分离的系统中或者微服务架构的系统中,由于架构方式的不同以及架构的复杂性,就很难像传统单体系统方式,直接从 Session 或者某类存储中读取到用户信息。即使使用了像 Redis 之类的支持分布式的集中存储来实现统一读取用户信息,也势必导致代码的冗余性和耦合度提升。
为了方便代码开发,本系统结合系统所使用组件自身的特点,提供了几种获取用户信息的途径和方法。
注意
传统的、非前后端分离的系统,核心元素是 Session,而 Session 是存储在后端的内存中。前后端分离的系统或者微服务架构,核心元素是 Token,而 Token 是存储在客户端中。这就导致两种架构的"玩法"非常不同,所以如果您选择了前后端分离架构或者微服务架构,首先就要改变代码实现的思想与方式,切勿将传统系统开发思路和方式,在前后端分离或者微服务架构系统下"生搬硬套"。
另外,可能有些系统就是使用 Redis 存储用户信息或者 Token,用的时候直接写代码去 Redis 读。可能你觉得这种方式也很“方便”,本不像本文所述的这么“危言耸听”。这是因为这种方式不符合本系统的设计理念和设计美学,也是本系统与很多同类产品不同原因。
[二]后端
在开发后端代码时,可以使用以下两种方式,获取到用户信息
[1]SecurityContextHolder(不推荐)
可以通过 SecurityContextHolder
来读取当前已登录用户信息。这种方式是由 Spring Security
框架自身提供的机制。通过 SecurityContextHolder
类的命名可以知道,Spring Security 将当前登录用户信息放入到某个线程之中,在需要读取用户信息时,可以通过方法 SecurityContextHolder.getContext().getAuthentication()
来读取信息。
本系统也提供了便捷的调用方式:SecurityUtils
。调用 SecurityUtils.getAuthentication()
方法就可以直接获取用户信息。
这种方式在传统单体非常便捷,也非常适用。但是在微服务架构中却不推荐使用。主要原因是:
- 一个服务就相当于一个单体系统,不同服务中
SecurityContextHolder
对应的线程是不同的,很难在分布式架构下保证其一致性。 - OAuth2 支持多种认证方式,在 Spring 生态体系下(
Spring Authorization Server
+Spring Security
),不同的认证方式下SecurityContextHolder.getContext().getAuthentication()
获取的 “用户信息” 不同。例如:OAuth2 客户端模式下,SecurityContextHolder.getContext().getAuthentication()
返回的是客户端的认证信息。
[2]BearerTokenResolver(推荐)
服务间想要获取到用户信息,最直接的想法就是跨服务调用系统提供的用户接口。因为,实际开发中需要用到用户信息的地方会非常多,每个服务都去调用用户信息接口,就会让代码变得非常臃肿。
为了让代码使用更加便捷和简洁,同时降低代码耦合性。Dante Cloud 在系统提供了 BearerTokenResolver
接口。
BearerTokenResolver
会利用 Spring Authorization Server
自身提供的机制,从请求携带的 Token 中解析用户信息,同时支持 Jwt Token 和 Opaque Token。而且,Dante Cloud 已经将 BearerTokenResolver
注入到系统中,任何服务都可以直接使用,无需额外处理。
使用时,需要在你的代码中,注入 BearerTokenResolver
Bean,然后请求中携带的 Token 传递给其中相应的方法就可以进行获取到用户信息。
例如,新建一个 MyService
,在这个 Service 代码中注入BearerTokenResolver
Bean,如下例所示:
@Service
public class MyService {
private final BearerTokenResolver bearerTokenResolver;
public MyService(BearerTokenResolver bearerTokenResolver) {
this.bearerTokenResolver = bearerTokenResolver;
}
}
注意
BearerTokenResolver
是针对 Servlet(阻塞式)环境的。如果您开发的是 Reactive(响应式)环境服务,那么请注入 ReactiveBearerTokenResolver
[三]前端
前端主要通过从 Token 中解析和调用后端接口,两种途径获取用户信息。
[1]从Token中解析
Jwt Token
JWT(JSON Web Token)是一种自包含的、无状态的令牌,以结构化和可读的格式携带信息。JWT 由三部分组成:header
,payload
和 signature
,每部分都以 Base64URL
编码。
在使用 Token 的系统架构下,比较常见的方式,就是对 Jwt Token 进行扩展,将“必要”的用户信息添加至 Jwt Token 中,前端可以直接从 Jwt Token 中获取用户的相关信息。
将用户信息放入 Jwt Token 中的方式,优势是可以减少前后端的交互请求,但是JWT 中的声明对任何拥有令牌的人都是可见的,出于安全性的考虑防止信息泄露,Jwt Token 中不适合放入大量用户相关的信息,特别是较为敏感的用户信息。
Dante Cloud 也支持这种方式,在 Jwt Token 中扩展了,openid
、email
、roles
、avatar
和 employeeId
等五项必要信息。本系统仅扩展这几项信息,为了保证数据的安全性,即使放入 Jwt Token 中,也是放入的加密过的内容。前端在使用时,需要再次解密才能使用。
本系统中前后端数据加密传输逻辑,可以参见:【数字信封】
注意
Dante Cloud 支持 Jwt Token 和 Opaque Token 两种模式,仅在 Jwt Token 下才支持从 Token 中读取用户必要信息
ID Token
OAuth2 最初设计的出发点并不是“人”的交互(涉及“用户”的系统),而是为了“系统”间的交互设计的,所以本身并不存在“用户”相关的内容。OIDC 的出现正是为了弥补 OAuth2 对用户支持的不足才出现的。
在 OIDC 中,ID 令牌是一种 JWT,包含用户信息并用于验证用户身份。通常与访问令牌同时签发,ID 令牌允许客户端验证用户身份。客户端可以验证 ID 令牌以确保用户身份并提取用户信息用于个性化或授权目的。ID 令牌仅限一次性使用,不应用于 API 资源授权。
Dante Cloud 当前默认在签发 Opaque Token 的同时也会签发 ID Token,可以通过解析 ID Token 获取到用户信息
注意
当前 ID Token 也仅是扩展了,openid
、email
、roles
、avatar
和 employeeId
等五项必要信息。其它信息需要自己修改代码进行扩展。
[2]调用后端接口
既然已经支持了从 Token 中解析用户信息,那么在什么情况下需要调用后端接口来获取用户信息?
Opaque Token
Opaque Token 不透明令牌是一种访问令牌,顾名思义,对客户端或任何外部方来说都是不透明的或不可见的。这意味着令牌本身不携带关于用户或授予权限的任何可读信息。
当你收到一个不透明令牌时,它通常看起来是一个看似随机的字符串,尝试解码它不会产生任何有意义的数据。由于令牌的实际内容只有签发它的授权服务器知道,为了验证不透明令牌,客户端必须将其发送回服务器,服务器然后验证其真实性并确定相关的权限。这种方法确保了敏感信息保持隐藏,提供了额外的安全层,但它也需要额外的服务器通信来验证令牌。
因为,客户端无法从 Opaque Token 读取到任何用户信息,这种模式下只能在拿到 Opaque Token 以后,请求后端的 /userinfo
接口来获取用户信息。
提示
Dante Cloud 为了提升系统安全性,默认使用的就是 Opaque Token,所以正常情况就需要通过 /userinfo
接口来获取用户信息。为了方便起见,Dante Cloud 还同时开启了 OIDC 支持,在签发访问令牌时也会同时签发 ID Token。这样就无需通过/userinfo
接口来获取用户信息。
[3]实际使用
在 Dante Cloud 前端工程中,已经处理好了各种方式的组合以及兼容问题。
开发前端代码时,直接调用 useAuthenticationStore
中的相关属性值即可。
[四]扩展用户信息
不管您使用的是前文中的哪种方式,实际上系统支持的用户信息也仅有 openid
、email
、roles
、avatar
和 employeeId
等五项必要信息。对于大多数实际业务来说,肯定会存在不足以支撑的问题。
从本系统的角度考虑,也并不想扩展更多的信息。一方面,从一个通用平台的角度讲,扩展再多的用户信息,总会存在不满足实际业务的情况;另一方面,具体扩展哪些信息会直接影响到系统的安全性以及 Token 的大小。所以,当前扩展的信息相对来是合理的,既不会导致关键信息的泄露,又尽可能的支撑到了业务扩展和使用便捷的需求。
除非极特殊情况,在实际开发过程中,是不建议开发者自己扩展这些信息的(除非你对本系统代码以及涉及的组件和技术非常熟悉)。还是建议采用单独设计一张用户信息扩展表,并且提供相应的 API 接口。以现有的必要信息作为参数,二次请求扩展用户信息 API 的方式来支撑业务功能开发。