【问题标题】:How can I validate OAuth 2.0 token user details in @PreAuthorize annotation in Spring Boot REST service如何在 Spring Boot REST 服务的 @PreAuthorize 注释中验证 OAuth 2.0 令牌用户详细信息
【发布时间】:2019-07-09 16:34:31
【问题描述】:

我需要检查@PreAuthorize 注释。比如:

@PreAuthorize("hasRole('ROLE_VIEWER') or hasRole('ROLE_EDITOR')")

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

@PreAuthorize("#pathProfileId.equals(oauthToken.userDetails.profileId)")

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

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

【问题讨论】:

    标签: spring spring-boot spring-security-oauth2


    【解决方案1】:

    你有两个选择:

    1-

    将 UserDetailsS​​ervice 实例设置为 DefaultUserAuthenticationConverter 并将转换器设置为 JwtAccessTokenConverter 所以当 spring 从 DefaultUserAuthenticationConverter 调用 extractAuthentication 方法时,它发现 (userDetailsS​​ervice != null) 所以它得到了整个 UserDetails 对象,在调用此行时调用 loadUserByUsername 的实现:

    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;
        }
    

    所以你需要在你的微服务中实现的是:

    @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;
     }
    

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


    2-

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

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

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

    无论如何,在第二个选项中,您只需要覆盖 CustomTokenConverter 类中的 extractAuthentication 方法 extends JwtAccessTokenConverter 并忘记设置访问令牌转换器 converter.setAccessTokenConverter 来自第一个选项中的 tokenEnhancer() 方法,这里是整个 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);
        }
    
    }
    

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

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

    【讨论】:

      猜你喜欢
      • 2017-10-02
      • 2017-10-11
      • 2018-04-29
      • 2021-08-12
      • 1970-01-01
      • 2016-06-02
      • 2016-12-31
      • 2016-11-10
      • 1970-01-01
      相关资源
      最近更新 更多