问题
We are developing an application that uses OAuth 2 for two use cases:
- Access to backend microservies (using
client_credentials
) - Authenticating the application's users (using
authorization_code
, so redirecting the users to Keycloak for login, roughly configured like shown in the tutorial).
While authenticating our users, we receive part of the information from the auth server (such as login) and the other part can be found in a local user table. What we like to do is to create a Principal object containing also the data from the local database.
PrincipalExtractor seems to be the way to go. Since we have to use manual OAuth configuration to not interfere with OAuth use case 1, we create it and set it:
tokenServices.setPrincipalExtractor(ourPrincipalExtractor);
The implementation basically does a DB lookup and returns a CustomUser object in the mapping function. Now although this seems to work (extractor is called), it is not persisted in the session correctly. So in many of our REST resource we are injecting the current user:
someRequestHandler(@AuthenticationPrincipal CustomUser activeUser) {
and receive null there. Looking into an injected Authentication
it shows that it is an OAuth2Authentication object with the default Principal object (I think it is a Spring User
/ UserDetails
). So null because it is not our CustomUser
returned before.
Have we misunderstood the way PrincipalExtractor
works? Can it be a misconfiguration of our filter chain because we have two different OAuth mechanisms in the same application as mentioned before? A breakpoint in Spring's Principal repository showed us that CustomUser
is saved there, followed by a save with the original type which seems to overwrite it.
回答1:
I can tell you how I managed to do something similar using JWT. If you aren't using JWT then I'm not sure if this will help.
I had a very similar issue in that my injected principal was only containing the username. Not null like yours, but obviously not what I wanted. What I ended up doing was extending both the TokenEnhancer
and JwtAccessTokenConverter
.
I use the TokenEnhancer
to embed my extended principal of type CustomUserDetails
inside the JWT additional information.
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;
}
}
And then manually extract the extended principal when building the Authentication
object when processing an authenticated request.
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);
}
}
and the AuthorizationServer
configuration to tie it all together.
@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()");
}
}
I am then able to access my extended principal in my resource controller like this
@RestController
public class SomeResourceController {
@RequestMapping("/some-resource")
public ResponseEntity<?> someResource(Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
return ResponseEntity.ok("woo hoo!");
}
}
回答2:
Ok, to answer my own question:
PrincipalExtractor
seems to be the usual and standard way to customize the principal- It doesn't work in our case because we are using a JHipster application that simply overwrites the principal right after the login with it's own
User
. So all mapping inPrincipalExtractor
is reset. If anyone has the same question: Look intoUserService
.
That's the downside of using generated code you don't know in detail I guess.
来源:https://stackoverflow.com/questions/48708406/how-to-extend-oauth2-principal