How do I map OAuth 2 token to UserDetails object i

2019-03-10 16:37发布

问题:

I have 2 separate Spring Boot applications, one serving as an an OAuth 2 authorization server, and the other as resource server. I'm using Spring's RemoteTokenServices in my resource server to check tokens from the authorization server. Now, I'm trying to define protected controller code in my resource server application, but I'm not sure how to map the UserDetails class to the authentication principal provided through the OAuth 2 mechanism.

I have set up my authorization server with a custom TokenEnhancer that adds more details to the token such that /oauth/check_token?token=<token> returns with custom fields, which I want to map to my resource server controllers.

In a more monolithic setup where the authorization server is also the resource server, I can define controller methods that make use of the authenticated principal this way:

//User implements UserDetails
public Map<String, Object> getResource(@AuthenticationPrincipal User user) {
    //code that uses the user object
}

However, this doesn't seem to work as straight forward in a more distributed approach. The mapping fails, and the user parameter ends up being a null object. I tried using the following approach:

public Map<String, Object> getResource(Authentication authentication) {
    //code that uses the authentication object
}

While the code above successfully maps the authentication details, it doesn't provide a way for me to directly access the custom fields I've set through the TokenEnhancer I mentioned earlier. I can't seem to find anything from the Spring docs regarding this.

回答1:

To resolve the issue, let me go through a bit of architectural background first. The UserDetails object automatically mapped through the @AuthenticationPrincipal comes from the principal field of the active Authentication object. A resource server controller has access to an OAuth2Authencation object, which is a specialized instance of Authentication for Spring OAuth2 security framework, just by simply declaring it as part of the method parameters.

public void controllerMethod(OAuth2Authentication authentication) {
    //controller definition
}

Knowing this, the problem now shifts to how to make sure that the getPrincipal() method in the Authentication object is an instance of my custom UserDetails class. The RemoteTokenServices I use in the resource server application uses an instance of AccessTokenConverter to interpret token details sent by the authorization server. By default, it uses DefaultAccessTokenConverter, which just sets the authentication principal as the username, which is a String. This converter makes use of UserAuthenticationConverter to convert the data coming from the authorization server into an instance of Authentication. This is what I needed to customize:

DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
tokenConverter.setUserTokenConverter(new DefaultUserAuthenticationConverter() {

    @Override
    public Authentication extractAuthentication(Map<String, ?> map) {
        Authentication authentication = super.extractAuthentication(map);
        // User is my custom UserDetails class
        User user = new User();
        user.setSpecialKey(map.get("specialKey").toString());
        return new UsernamePasswordAuthenticationToken(user,
                authentication.getCredentials(), authentication.getAuthorities());
    }

});
tokenServices.setAccessTokenConverter(tokenConverter);

With all these set up, the @AuthenticationPrincipal mechanism now works as expected.



回答2:

Did you enable the AuthenticationPrincipalArgumentResolver, like following xml?

<mvc:annotation-driven>
        <mvc:argument-resolvers>
                <bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
        </mvc:argument-resolvers>
</mvc:annotation-driven>

And you need to implement the UserDetails to return your own CustomerUser object, then you can use the annotation to get principal directly.