如何在 Spring Boot REST 服务的 @PreAuthorize 注释中验证 OAuth 2.0 令牌用户详细信息

ico*_*oba 5 spring spring-boot spring-security-oauth2

我需要检查@PreAuthorize 注释。就像是:

@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")
Run Code Online (Sandbox Code Playgroud)

没关系,但我还需要使用请求路径中的那些来验证存储在 OAuth 2.0 令牌中的一些用户详细信息,因此我需要执行类似的操作(oauthToken.userDetails 只是一个示例:

@PreAuthorize("#pathProfileId.equals(oauthToken.userDetails.profileId)")
Run Code Online (Sandbox Code Playgroud)

(profileId 不是 userId 或 userName,它是我们在创建 OAuth 令牌时添加的用户详细信息)

使 OAuth 令牌属性在预授权注释安全表达式语言中可见的最简单方法是什么?

mib*_*iti 2

您有两个选择:

1-

将UserDetailsS ​​ervice 实例设置为DefaultUserAuthenticationConverter 并将转换器设置为JwtAccessTokenConverter,因此当 spring从 DefaultUserAuthenticationConverter调用extractAuthentication方法时,它会发现(userDetailsS​​ervice != null),因此它在调用此行时通过调用loadUserByUsername的实现来获取整个UserDetails对象:

userDetailsS​​ervice.loadUserByUsername((String) map.get(USERNAME))

在 spring 类 org 中的 next 方法中实现。springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter.java但只是添加它来澄清 spring 如何从映射获取主体对象(首先通过用户名获取它,如果 userDetailsS​​ervice 不为空,那么它获取整个对象):

//Note: This method implemented by spring but just putting it to show where spring exctract principal object and how extracting it
public Authentication extractAuthentication(Map<String, ?> map) {
        if (map.containsKey(USERNAME)) {
            Object principal = map.get(USERNAME);
            Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
            if (userDetailsService != null) {
                UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
                authorities = user.getAuthorities();
                principal = user;
            }
            return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
        }
        return null;
    }
Run Code Online (Sandbox Code Playgroud)

因此,您需要在微服务中实现的是:

@Bean//this method just used with token store bean example: new JwtTokenStore(tokenEnhancer());
public JwtAccessTokenConverter tokenEnhancer() {
    /**
    * CustomTokenConverter is a class extends JwtAccessTokenConverter 
    * which override "enhance" to add extra information to OAuth2AccessToken after
    * authenticate the user and get it by loadUserByUsername implementation 
    * like profileId in your case
    **/  
    JwtAccessTokenConverter converter = new CustomTokenConverter();

    DefaultAccessTokenConverter datc = new DefaultAccessTokenConverter();
    datc.setUserTokenConverter(userAuthenticationConverter());
    converter.setAccessTokenConverter(datc);

    //Other method code implementation....
}

@Autowired
private UserDetailsService userDetailsService;

@Bean
public UserAuthenticationConverter userAuthenticationConverter() {
    DefaultUserAuthenticationConverter duac = new DefaultUserAuthenticationConverter();
    duac.setUserDetailsService(userDetailsService);
    return duac;
 }
Run Code Online (Sandbox Code Playgroud)

注意:第一种方法将在每个请求中访问数据库,因此它通过用户名加载用户并获取 UserDetails 对象,以便将其分配给身份验证内的主体对象。


2-

如果出于任何原因,您认为最好不要在每个请求中访问数据库,并且执行请求中传递的令牌中的 profileId 等所需数据没有问题。

假设您知道在生成 oauth2 令牌时分配给用户的旧权限将始终在令牌中,直到它失效,即使您在数据库中为在请求中传递令牌的用户更改了它,这样用户就可以调用不再允许他/她的方法提取令牌之后并且在提取令牌之前允许。

因此,这意味着如果用户权限在生成令牌后发生更改,@PreAuthorize 将不会检查新权限,因为它不会被删除或添加到令牌中,并且您必须等到旧令牌无效或过期,以便用户被迫再次执行服务获取新的 oauth 令牌。

无论如何,在第二个选项中,您只需要重写CustomTokenConverter类中的extractAuthentication方法扩展 JwtAccessTokenConverter并忘记从第一个选项中的tokenEnhancer()方法设置访问令牌转换器converter.setAccessTokenConverter,这里是整个 CustomTokenConverter 您可以使用它来读取数据从令牌并返回主体对象而不仅仅是字符串用户名:

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

public class CustomTokenConverter extends JwtAccessTokenConverter {

    // This is the method you need to override to read data direct from token passed in request
    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        OAuth2Authentication authentication = super.extractAuthentication(map);

        Object userIdObj = map.get(AuthenticationUtils.USER_ID);
        UUID userId = userIdObj != null ? UUID.fromString(userIdObj.toString()) : null;
        Object profileIdObj = map.get(AuthenticationUtils.PROFILE_ID);
        UUID profileId = profileIdObj != null ? UUID.fromString(profileIdObj.toString()) : null;
        Object firstNameObj = map.get(AuthenticationUtils.FIRST_NAME);
        String firstName = firstNameObj != null ? String.valueOf(firstNameObj) : null;
        Object lastNameObj = map.get(AuthenticationUtils.LAST_NAME);
        String lastName = lastNameObj != null ? String.valueOf(lastNameObj) : null;

        JwtUser principal = new JwtUser(userId, profileId, authentication.getUserAuthentication().getName(), "N/A", authentication.getUserAuthentication().getAuthorities(), firstName, lastName);

        authentication = new OAuth2Authentication(authentication.getOAuth2Request(),
                new UsernamePasswordAuthenticationToken(principal, "N/A", authentication.getUserAuthentication().getAuthorities()));
        return authentication;
    }

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        JwtUser user = (JwtUser) authentication.getPrincipal();
        Map<String, Object> info = new LinkedHashMap<>(accessToken.getAdditionalInformation());
        if (user.getId() != null)
            info.put(AuthenticationUtils.USER_ID, user.getId());
        if (user.getProfileId() != null)
            info.put(AuthenticationUtils.PROFILE_ID, user.getProfileId());
        if (isNotNullNotEmpty(user.getFirstName()))
            info.put(AuthenticationUtils.FIRST_NAME, user.getFirstName());
        if (isNotNullNotEmpty(user.getLastName()))
            info.put(AuthenticationUtils.LAST_NAME, user.getLastName());

        DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
        customAccessToken.setAdditionalInformation(info);
        return super.enhance(customAccessToken, authentication);
    }

    private boolean isNotNullNotEmpty(String str) {
        return Optional.ofNullable(str).map(String::trim).map(string -> !str.isEmpty()).orElse(false);
    }

}
Run Code Online (Sandbox Code Playgroud)

最后:猜猜我怎么知道你在询问与 OAuth2 一起使用的 JWT?

因为我是你们公司的一员 :P 并且你知道这一点 :P