【问题标题】:Spring Security - Principal from Authorization Server is different from Resource ServerSpring Security - 授权服务器的主体与资源服务器不同
【发布时间】:2020-04-10 23:35:21
【问题描述】:

我在我的授权服务器中创建了userinfo 端点。

@GetMapping("/userinfo")
public Principal me(Principal principal) {
    return principal;
}

它返回这个 JSON:

{
    ...
    "userAuthentication": {
        ...
        "principal": {
            "id": 2,
            "username": "xyz",
            "password": "......",
            "accountNonExpired": true,
            "accountNonLocked": true,
            "credentialsNonExpired": true,
            "enabled": true,
            "authorities": [
                {
                "authority": "ROLE_DONOR"
                }
            ],
            "createdAt": "2019-11-08T20:50:46"
        },
        ...
        "name": "xyz"
    },
    ...
    "principal": {
        "id": 2,
        "username": "xyz",
        "password": "......",
        "accountNonExpired": true,
        "accountNonLocked": true,
        "credentialsNonExpired": true,
        "enabled": true,
        "authorities": [
        {
            "authority": "ROLE_DONOR"
        }
        ],
        "createdAt": "2019-11-08T20:50:46"
    },
    ...
    "name": "xyz"
}

在我的一个资源服务器用户服务 API 中,我尝试 sysout Principal 的值来查看它的值:

@GetMapping("/{id}")
public ResourceResponseDto findById(@PathVariable("id") long id, Principal principal) throws IOException {
    ObjectMapper objectMapper = new ObjectMapper();
    String x = objectMapper.writeValueAsString(principal);
    System.out.println(x);
    return ...;
}

principal 的值不同。其值相当于上面的principal.username,省略其他字段:

{
    "userAuthentication": {
        ...
        "principal": "xyz",
        ...
        "name": "xyz"
    },
    ...
    "principal": "xyz",
    ...
    "name": "xyz"
}

这是怎么发生的?

我需要获取id 的值,但它已经消失了。 principalobject 的字段消失了。这导致我认为我的其他方法出错:

@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == principal.id")
public ResourceResponseDto findById(@PathVariable("id") long id) {
    //
}

我收到此错误;

Failed to evaluate expression 'hasRole('ADMIN') or #id == principal.id'

请帮忙。谢谢。

【问题讨论】:

  • 首先检查Principal在每种情况下的什么类

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


【解决方案1】:

据我了解,您正在实施 OAuth2 Spring 身份验证服务器。 Principle 只是一个声明方法getName() 的接口。对于身份验证和资源服务器,principle 由类OAuth2Authentication 实现。方法getPrinciple():

public Object getPrincipal() {
    return this.userAuthentication == null ? this.storedRequest.getClientId() : this.userAuthentication.getPrincipal();
}

如果 OAuth2 客户端正在被认证,它没有userAuthentication,只有clientId。如果用户正在接受身份验证,您会看到完整的 userAuthentication 对象。

【讨论】:

    【解决方案2】:

    来自Authentication.getPrincipal() 文档:

    被认证的主体的身份。在使用用户名和密码的身份验证请求的情况下,这将是用户名。调用者应填充身份验证请求的主体。 AuthenticationManager 实现通常会返回一个 包含更丰富信息的身份验证作为主体使用 应用程序。许多身份验证提供程序将创建一个 UserDetails 对象作为主体。

    因此,您负责在身份验证过程中填充Principal

    将附加信息传递给Principal 的一种方法是扩展UsernamePasswordAuthenticationFilter 并覆盖'attemptAuthentication()' 方法:

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, 
                     HttpServletResponse response) {
        String username = obtainUsername(request);
        String password = obtainPassword(request);
    
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        setDetails(request, authRequest);
    
        Authentication authenticated = this.getAuthenticationManager().authenticate(authRequest);
    
        return new UsernamePasswordAuthenticationToken(
                new YourObjectForPrincipal(...),
                authenticated.getCredentials(), authenticated.getAuthorities());
    }
    

    注意将YourObjectForPrincipal 对象作为UsernamePasswordAuthenticationToken 的第一个参数传递给YourObjectForPrincipal,该对象可能包含您想要的任何数据。 要获得您的扩展用户,只需强制转换为所需的对象:

    (YourObjectForPrincipal)authentication.getPrincipal();
    

    这可能不是最好的解决方案,但它对我有用。 我希望它会有所帮助。

    【讨论】:

    • 我会在授权服务器中实现这个吗?而YourObjectForPrincipal 指的是我的对象,其中包含idusername 等,对吗?
    • @Julez 您可以在任一侧添加它。对于资源服务器,可以通过 AuthorizationServerSecurityConfigurer.addTokenEndpointAuthenticationFilter() 添加
    • 我创建了一个扩展 UsernamePasswordAuthenticationFilter 的类,其构造函数接受 AuthenticationManager 实例。我用this.authenticationManager. 替换了this.getAuthenticationManager()。在我的身份验证服务器配置中,我有这个:AuthorizationServerSecurityConfigurer.addTokenEndpointAuthenticationFilter(new CustomUsernamePasswordAuthenticationFilter(this.authenticationManager))。但还是一样。
    • 我不能在资源服务器 (API) 中这样做,因为 ResourceServerConfigurerAdapter 没有 @Override public void configure(AuthorizationServerSecurityConfigurer oAuthServer) throws Exception {...}
    • 你是对的。我为 Auth Server 指定了提案。 ResourceServerConfigurerAdapterconfigure(HttpSecurity http)。您可以使用http.addFilterBefore(yourFilter, UsernamePasswordAuthenticationFilter.class) 添加您的过滤器。
    【解决方案3】:

    也许已经晚了,但这是我的解决方案。 由于我在资源服务器中使用了UserInfoTokenServices,我发现它使用了FixedPrincipalExtractor,在这个类中我看到了这个:

    private static final String[] PRINCIPAL_KEYS = new String[] { "user", "username",
                "userid", "user_id", "login", "id", "name" };
    
        @Override
        public Object extractPrincipal(Map<String, Object> map) {
            for (String key : PRINCIPAL_KEYS) {
                if (map.containsKey(key)) {
                    return map.get(key);
                }
            }
            return null;
        }
    

    所以它直接返回了在您的应用中为“xyz”的“用户”值。 所以我创建了一个类来实现 PrincipalExtractor 并覆盖 extractPrincipal 方法:

    import java.util.Map;
    import org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor;
    import org.springframework.stereotype.Component;
    import com.alibaba.fastjson.JSON;
    import com.project.LoginUser;
    
    @Component
    public class CustomPrincipalExtractor implements PrincipalExtractor {
    
        @Override
        @SuppressWarnings("unchecked")
        public Object extractPrincipal(Map<String, Object> map) {
            Map<String, Object> principal = (Map<String, Object>) map.get("principal");
            return JSON.parseObject(JSON.toJSONString(principal), LoginUser.class);
        }
    
    }
    

    这里的参数映射类似于授权服务器中的 Principal,所以我使用“principal”键获取我的 LoginUser 类,该键实现了 UserDetails 并添加了一些额外的信息,如 id、电子邮件......就像你的校长。这里我使用了fastjson来解析,你也可以使用ObjectMapper。 然后在您的资源服务器中定义 UserInfoTokenServices bean。这是我的代码:

    @EnableResourceServer
    @EnableWebSecurity
    @RequiredArgsConstructor
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
        private final ResourceServerProperties sso;
        private final CustomPrincipalExtractor customPrincipalExtractor;
    
        @Primary
        @Bean
        public UserInfoTokenServices tokenService() {
            UserInfoTokenServices userInfoTokenServices = new UserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
            userInfoTokenServices.setPrincipalExtractor(customPrincipalExtractor);
            return userInfoTokenServices;
        }
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            ...
        }
    }
    

    在这里我创建了一个 UserInfoTokenServices 并设置了我的自定义 PrincipalExtractor。不要忘记添加这些属性:

    security.oauth2.resource.user-info-uri = http://domain:port/your/user-info-url
    security.oauth2.resource.prefer-token-info = false
    

    现在您可以在资源服务器中获取自己的主体对象。

    【讨论】:

      猜你喜欢
      • 2020-07-27
      • 2018-05-09
      • 2021-10-29
      • 2015-05-22
      • 2018-08-29
      • 2016-05-21
      • 2014-07-09
      • 1970-01-01
      • 2015-05-14
      相关资源
      最近更新 更多