如何扩展 OAuth2 主体
How to extend OAuth2 principal
我们正在开发一个将 OAuth 2 用于两个用例的应用程序:
- 访问后端微服务(使用
client_credentials
)
- 验证应用程序的用户(使用
authorization_code
,因此将用户重定向到Keycloak进行登录,大致配置如tutorial所示)。
在对我们的用户进行身份验证时,我们从身份验证服务器接收部分信息(例如登录),另一部分可以在本地用户中找到table。我们喜欢做的是创建一个 Principal 对象,其中还包含来自本地数据库的数据。
PrincipalExtractor seems to be the way to go。由于我们必须使用手动 OAuth 配置才能不干扰 OAuth 用例 1,因此我们创建并设置它:
tokenServices.setPrincipalExtractor(ourPrincipalExtractor);
该实现基本上是在映射函数中执行数据库查找和 returns CustomUser 对象。现在虽然这似乎有效(调用提取器),但它没有正确地保存在会话中。因此,在我们的许多 REST 资源中,我们正在注入当前用户:
someRequestHandler(@AuthenticationPrincipal CustomUser activeUser) {
并在那里收到空值。查看注入的 Authentication
它表明它是一个具有默认 Principal 对象的 OAuth2Authentication 对象(我认为它是一个 Spring User
/ UserDetails
)。所以 null 因为它不是我们之前返回的CustomUser
。
我们是否误解了 PrincipalExtractor
的工作方式?会不会是我们的过滤器链配置错误,因为我们在同一个应用程序中有两个不同的 OAuth 机制,如前所述? Spring 的 Principal 存储库中的一个断点向我们显示 CustomUser
保存在那里,然后使用似乎覆盖它的原始类型进行保存。
我可以告诉你我是如何使用 JWT 完成类似的事情的。如果您不使用 JWT,那么我不确定这是否有帮助。
我有一个非常相似的问题,因为我注入的委托人只包含用户名。不像你的那样 null,但显然不是我想要的。我最终做的是扩展 TokenEnhancer
和 JwtAccessTokenConverter
.
我使用 TokenEnhancer
在 JWT 附加信息中嵌入类型为 CustomUserDetails
的扩展主体。
public class CustomAccessTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
Object principal = authentication.getUserAuthentication().getPrincipal();
if (principal instanceof CustomUserDetails) {
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("userDetails", principal);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
}
}
return accessToken;
}
}
然后在处理经过身份验证的请求时构建 Authentication
对象时手动提取扩展主体。
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = super.extractAuthentication(map);
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
LinkedHashMap userDetails = (LinkedHashMap) map.get("userDetails");
if (userDetails != null) {
// build your extended principal here
String localUserTableField = (String) userDetails.get("localUserTableField");
CustomUserDetails extendedPrincipal = new CustomUserDetails(localUserTableField);
Collection<? extends GrantedAuthority> authorities = userAuthentication.getAuthorities();
userAuthentication = new UsernamePasswordAuthenticationToken(extendedPrincipal,
userAuthentication.getCredentials(), authorities);
}
}
return new OAuth2Authentication(authentication.getOAuth2Request(), userAuthentication);
}
}
和 AuthorizationServer
配置将它们结合在一起。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
CustomJwtAccessTokenConverter accessTokenConverter = new CustomJwtAccessTokenConverter();
accessTokenConverter.setSigningKey("a1b2c3d4e5f6g");
return accessTokenConverter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomAccessTokenEnhancer();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(passwordEncoder());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.passwordEncoder(passwordEncoder());
security.checkTokenAccess("isAuthenticated()");
}
}
然后我可以像这样在我的资源控制器中访问我的扩展主体
@RestController
public class SomeResourceController {
@RequestMapping("/some-resource")
public ResponseEntity<?> someResource(Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return ResponseEntity.ok("woo hoo!");
}
}
好的,回答我自己的问题:
PrincipalExtractor
似乎是自定义主体的常用标准方法
- 它在我们的案例中不起作用,因为我们使用的是 JHipster 应用程序,它在登录后立即用自己的
User
覆盖主体。所以 PrincipalExtractor
中的所有映射都被重置。如果有人有同样的问题:查看 UserService
.
我猜这就是使用生成的代码的缺点,你不知道细节。
我们正在开发一个将 OAuth 2 用于两个用例的应用程序:
- 访问后端微服务(使用
client_credentials
) - 验证应用程序的用户(使用
authorization_code
,因此将用户重定向到Keycloak进行登录,大致配置如tutorial所示)。
在对我们的用户进行身份验证时,我们从身份验证服务器接收部分信息(例如登录),另一部分可以在本地用户中找到table。我们喜欢做的是创建一个 Principal 对象,其中还包含来自本地数据库的数据。
PrincipalExtractor seems to be the way to go。由于我们必须使用手动 OAuth 配置才能不干扰 OAuth 用例 1,因此我们创建并设置它:
tokenServices.setPrincipalExtractor(ourPrincipalExtractor);
该实现基本上是在映射函数中执行数据库查找和 returns CustomUser 对象。现在虽然这似乎有效(调用提取器),但它没有正确地保存在会话中。因此,在我们的许多 REST 资源中,我们正在注入当前用户:
someRequestHandler(@AuthenticationPrincipal CustomUser activeUser) {
并在那里收到空值。查看注入的 Authentication
它表明它是一个具有默认 Principal 对象的 OAuth2Authentication 对象(我认为它是一个 Spring User
/ UserDetails
)。所以 null 因为它不是我们之前返回的CustomUser
。
我们是否误解了 PrincipalExtractor
的工作方式?会不会是我们的过滤器链配置错误,因为我们在同一个应用程序中有两个不同的 OAuth 机制,如前所述? Spring 的 Principal 存储库中的一个断点向我们显示 CustomUser
保存在那里,然后使用似乎覆盖它的原始类型进行保存。
我可以告诉你我是如何使用 JWT 完成类似的事情的。如果您不使用 JWT,那么我不确定这是否有帮助。
我有一个非常相似的问题,因为我注入的委托人只包含用户名。不像你的那样 null,但显然不是我想要的。我最终做的是扩展 TokenEnhancer
和 JwtAccessTokenConverter
.
我使用 TokenEnhancer
在 JWT 附加信息中嵌入类型为 CustomUserDetails
的扩展主体。
public class CustomAccessTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
Object principal = authentication.getUserAuthentication().getPrincipal();
if (principal instanceof CustomUserDetails) {
Map<String, Object> additionalInfo = new HashMap<>();
additionalInfo.put("userDetails", principal);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
}
}
return accessToken;
}
}
然后在处理经过身份验证的请求时构建 Authentication
对象时手动提取扩展主体。
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
OAuth2Authentication authentication = super.extractAuthentication(map);
Authentication userAuthentication = authentication.getUserAuthentication();
if (userAuthentication != null) {
LinkedHashMap userDetails = (LinkedHashMap) map.get("userDetails");
if (userDetails != null) {
// build your extended principal here
String localUserTableField = (String) userDetails.get("localUserTableField");
CustomUserDetails extendedPrincipal = new CustomUserDetails(localUserTableField);
Collection<? extends GrantedAuthority> authorities = userAuthentication.getAuthorities();
userAuthentication = new UsernamePasswordAuthenticationToken(extendedPrincipal,
userAuthentication.getCredentials(), authorities);
}
}
return new OAuth2Authentication(authentication.getOAuth2Request(), userAuthentication);
}
}
和 AuthorizationServer
配置将它们结合在一起。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
CustomJwtAccessTokenConverter accessTokenConverter = new CustomJwtAccessTokenConverter();
accessTokenConverter.setSigningKey("a1b2c3d4e5f6g");
return accessTokenConverter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomAccessTokenEnhancer();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(passwordEncoder());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.passwordEncoder(passwordEncoder());
security.checkTokenAccess("isAuthenticated()");
}
}
然后我可以像这样在我的资源控制器中访问我的扩展主体
@RestController
public class SomeResourceController {
@RequestMapping("/some-resource")
public ResponseEntity<?> someResource(Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return ResponseEntity.ok("woo hoo!");
}
}
好的,回答我自己的问题:
PrincipalExtractor
似乎是自定义主体的常用标准方法- 它在我们的案例中不起作用,因为我们使用的是 JHipster 应用程序,它在登录后立即用自己的
User
覆盖主体。所以PrincipalExtractor
中的所有映射都被重置。如果有人有同样的问题:查看UserService
.
我猜这就是使用生成的代码的缺点,你不知道细节。