获取用户信息
概述
用户信息是任何信息系统关键的核心信息,系统中很多业务功能都会涉及使用或关联用户信息。
在传统的、非前后端分离的系统中,用户登录系统之后,用户的相关信息就会存储在 Session 中(或者扩展的存储中,例如:Redis 或 Cache 等),通过用户的唯一标识就可以随时随地的、便捷的获取到用户的信息。
在前后端分离的系统中或者微服务架构的系统中,由于架构方式的不同以及架构的复杂性,就很难像传统单体系统方式,直接从 Session 或者某类存储中读取到用户信息。即使使用了像 Redis 之类的支持分布式的集中存储来实现统一读取用户信息,也势必导致代码的冗余性和耦合度提升。
为了方便代码开发,本系统结合系统所使用组件自身的特点,提供了几种获取用户信息的途径和方法。
警告
传统的、非前后端分离的系统,核心元素是 Session,而 Session 是存储在后端的内存中。前后端分离的系统或者微服务架构,核心元素是 Token,而 Token 是存储在客户端中。这就导致两种架构的"玩法"非常不同,所以如果您选择了前后端分离架构或者微服务架构,首先就要改变代码实现的思想与方式,切勿将传统系统开发思路和方式,在前后端分离或者微服务架构系统下"生搬硬套"。
另外,可能有些系统就是使用 Redis 存储用户信息或者 Token,用的时候直接写代码去 Redis 读。可能你觉得这种方式也很“方便”,不像本文所述的这么“危言耸听”。这是因为这种方式不符合本系统的设计理念和设计美学,也是本系统与很多同类产品不同原因。
[一]后端
在开发后端代码时,可以使用以下两种方式,获取到用户信息
[1]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(响应式)环境服务,那么请注入ReactiveBearerTokenResolverBearerTokenResolver是从请求中获取到 Token,所以依赖于请求。至于如何从请求中获取到 Token,并且通过BearerTokenResolver解析数据,就需要你结合业务自己实现
[2]从 Session 中读取(不推荐)
虽然说基于 Token 的前后端分离的架构是无状态的,完全不需要依赖于传统的 Session。但这也为代码的开发带来了极大的不便,特别是对于传统类型项目用户信息又是必不可少的。
为此,Dante Cloud 做了极大地努力,不仅统一了微服务环境下的 Session 体系,而且自 3.5.7.0 版本起,新增了支持跨服务的从 Session 中获取用户信息的方式。
可以使用新增的工具类 ServletSecurityUtils,通过其方法 getUserPrincipal(HttpServletRequest request) 来获取到用户信息。
响应式服务也提供了从 Session 中获取用户信息的工具类
ReactiveSecurityUtils。
警告
- 此种方法看似简单,与传统单体项目模式又非常像。但是使用的前提是 “一定要保证Session的一致性”,这种一致性包括前端与后端以及服务与服务间,否则要么获取不到信息,要么信息不准确
- 虽然看着与单体用法很像,但仅是 “为后端提供一种获取用户信息的便捷方式”,切勿像单体一样用户的信息都往 Session 里面塞,特别是权限相关信息,因为毕竟本系统是以 Token 为核心。
提示
如果前端是非 Web 类型,例如:桌面或者 Native 的移动端,不是说不可以用该种方式,但是在使用时一定要注意在所使用的 Http 客户端(Node 有 Axios,Java 有 OkHttp3 等)中模拟 Session 环境。
即:先调用 /open/identity/session 生成 Session,然后在 Http 客户端模拟 Cookie 保存 Session,最后每次请求时可以携带 Cookie 以及 Session 相关的头信息。具体原理解释,可以详见高阶文档 【Spring Cloud 之 Session 共享及一致性处理】
[3]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()返回的是客户端的认证信息。
注
此种方式需要自己对 SecurityContextHolder.getContext() 中 Authentication 信息进行解析。因为使用的是 Spring Security 体系,所以SecurityContextHolder.getContext() 还是依赖于 Session 环境,原理与前一种方式类似。
[二]前端
前端主要通过从 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 的方式来支撑业务功能开发。
