基于Spring Auth Server与Microsoft Entra ID实现用户模拟的方案咨询
核心思路
RFC8693的Token Exchange默认依赖subject_token,但你仅持有被模拟用户的邮箱,因此需要扩展Spring Authorization Server的令牌交换能力,允许通过邮箱直接生成模拟令牌,或者先通过邮箱获取用户标识再完成交换。以下是两种可行方案:
方案一:自定义Token Exchange扩展(推荐)
直接扩展Spring Authorization Server的Token Exchange逻辑,支持用target_user_email参数替代subject_token,跳过获取被模拟用户令牌的步骤。
1. 更新客户端配置
确保react-client的授权类型包含令牌交换,并新增模拟权限的Scope:
spring.security.oauth2: authorizationserver: client: react-client: registration: authorization-grant-types: - authorization_code - refresh_token - urn:ietf:params:oauth:grant-type:token-exchange scopes: - openid - profile - https://graph.microsoft.com/User.ReadBasic.All - impersonate_user # 新增模拟权限Scope
2. 自定义令牌交换参数转换器
继承默认的TokenExchangeGrantAuthenticationConverter,添加对target_user_email的解析逻辑,通过Azure Graph API根据邮箱获取用户的sub(唯一标识符):
@Component public class EmailAwareTokenExchangeConverter extends TokenExchangeGrantAuthenticationConverter { private final GraphServiceClient<Request> graphClient; public EmailAwareTokenExchangeConverter(GraphServiceClient<Request> graphClient) { this.graphClient = graphClient; } @Override protected Authentication extractSubject(OAuth2TokenExchangeGrantAuthenticationToken tokenExchangeToken) { // 优先尝试默认的subject_token解析逻辑 Authentication subjectAuth = super.extractSubject(tokenExchangeToken); if (subjectAuth != null) { return subjectAuth; } // 若没有subject_token,读取target_user_email参数并查询用户信息 String targetEmail = tokenExchangeToken.getAdditionalParameters().get("target_user_email").toString(); User targetUser = graphClient.users() .buildRequest() .filter("mail eq '" + targetEmail + "'") .get() .getCurrentPage() .get(0); // 返回被模拟用户的认证信息 return new UsernamePasswordAuthenticationToken( targetUser.getId(), null, Collections.emptyList() ); } }
3. 自定义令牌交换认证提供者
重写令牌生成逻辑,先验证管理员的模拟权限,再生成包含模拟标识的JWT:
@Component public class ImpersonationTokenExchangeProvider extends TokenExchangeAuthenticationProvider { private final OAuth2TokenGenerator<?> tokenGenerator; public ImpersonationTokenExchangeProvider(OAuth2TokenGenerator<?> tokenGenerator) { super(tokenGenerator); this.tokenGenerator = tokenGenerator; } @Override protected OAuth2Token generateAccessToken(OAuth2TokenExchangeAuthenticationContext context) { OAuth2TokenExchangeAuthenticationToken authToken = context.getAuthentication(); Authentication adminAuth = authToken.getPrincipal(); // 验证管理员是否拥有模拟权限 if (!adminAuth.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_IMPERSONATE_USER"))) { throw new OAuth2AccessDeniedException("无用户模拟权限"); } // 获取被模拟用户的认证信息,构建自定义Claims Authentication targetUserAuth = authToken.getSubject(); Map<String, Object> claims = new HashMap<>(); claims.put("sub", targetUserAuth.getName()); claims.put("email", authToken.getAdditionalParameters().get("target_user_email")); claims.put("impersonated_by", adminAuth.getName()); // 添加管理员标识,用于审计 // 生成模拟身份的JWT令牌 OAuth2TokenContext tokenContext = OAuth2TokenContext.builder() .registeredClient(context.getRegisteredClient()) .principal(targetUserAuth) .authorizationServerContext(context.getAuthorizationServerContext()) .tokenType(OAuth2TokenType.ACCESS_TOKEN) .claims(claims) .build(); return this.tokenGenerator.generate(tokenContext); } }
4. 注册自定义组件
在授权服务器配置中替换默认的转换器和提供者:
@Configuration @EnableAuthorizationServer public class AuthServerConfig { @Bean public OAuth2AuthorizationServerConfigurer authorizationServerConfigurer( EmailAwareTokenExchangeConverter converter, ImpersonationTokenExchangeProvider provider) { return new OAuth2AuthorizationServerConfigurer() .tokenExchange(tokenExchange -> tokenExchange .authenticationConverter(converter) .authenticationProvider(provider)); } }
方案二:先通过Graph API获取用户令牌再交换
若希望严格遵循RFC8693规范,可让认证服务器先获取被模拟用户的令牌,再执行令牌交换:
1. 配置Graph API权限
在Azure Entra ID中给my-client注册应用添加User.Read.All和DelegatedPermissionGrant.ReadWrite.All权限,并完成管理员授权。
2. 获取被模拟用户令牌
认证服务器使用客户端凭据流获取Graph API令牌,再通过/users/{userId}/oauth2PermissionGrants接口(或ROPC流,不推荐)获取被模拟用户的访问令牌。
3. 执行Token Exchange
以管理员的令牌作为subject_token,被模拟用户的令牌作为target_token,调用令牌交换接口生成模拟令牌。
资源服务器侧验证
资源服务器可通过检查impersonated_by Claim识别模拟令牌,并记录审计日志:
@Component public class ImpersonationTokenValidator implements OAuth2TokenValidator<Jwt> { @Override public OAuth2TokenValidatorResult validate(Jwt token) { if (token.getClaims().containsKey("impersonated_by")) { // 记录审计日志:管理员{impersonated_by}模拟用户{sub}执行操作 return OAuth2TokenValidatorResult.success(); } return OAuth2TokenValidatorResult.success(); } }
内容的提问来源于stack exchange,提问作者xabikip

