oauth2.0 如何在springboot控制器授权中使用自定义JWT声明?

tp5buhyn  于 2022-12-11  发布在  Spring
关注(0)|答案(1)|浏览(138)

在Sping Boot 中,我有一个使用Keycloak生成的JWT标记,它具有一些自定义声明:

{
  ...
  custom_claim:123
}

我可以在我的rest控制器中添加一个Authentication参数,我可以看到这个属性,但是它在一个名为RefreshableKeycloakSecurityContext的类中的一个深嵌套位置principal.context.token.otherclaims[0]中。在PreAuthorization中使用这个声明的最佳方式是什么?我想我需要的是将令牌中的声明与路径参数进行比较,如果它们不匹配,则返回一个错误。

@PreAuthorization("custom_claim=#my-path-parm")
yizd12fk

yizd12fk1#

要使用和避免的库

您在安全上下文中有一个RefreshableKeycloakSecurityContext的示例,因为您正在使用Spring的Keycloak适配器。您不应该:它是very deprecated,甚至与Spring Boot3不兼容。
有关在此other answer中使用spring-boot和Keycloak配置Spring应用程序安全性的详细信息

如何比较路径变量和令牌声明

您可以将Authentication示例作为控制器参数“自动神奇地”注入。默认情况下,对于资源服务器,您将获得带有JWT解码器的JwtAuthenticationToken和带有内省(“opaque”标记)的BearerTokenAuthentication

@RequestMapping("/{machin}")
@PreAuthorize("#machin eq #auth.tokenAttributes['yourclaim']") 
public SomeDto controllerMethod(@PathVariable("machin") String machin, AbstractOAuth2TokenAuthenticationToken<?> auth) {
   ...
}

私人理赔访问简化

在spring-security中,claim-set的类型为Map<String, Object>。访问claims值需要空值检查和强制转换,这会产生大量的代码,特别是对于嵌套的claims。
我建议您编写一个自定义Authentication示例,使用AbstractAuthenticationToken作为基类,来 Package 私有声明解析和转换(声明值为Object)。

http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwt -> new YourAuthenticationImpl(jwt, authoritiesConverter.convert(jwt)));

您也可以提供自订DSL,让安全性运算式更具可读性,更重要的是,更容易维护:表达式定义将在单个位置而不是分散在控制器方法中。
我写了一个set of 3 tutorials,它以如下表达式结尾:

@GetMapping("/on-behalf-of/{username}")
@PreAuthorize("is(#username) or isNice() or onBehalfOf(#username).can('greet')")
public String getGreetingFor(@PathVariable("username") String username, ProxiesAuthentication auth) {
    return "Hi %s from %s!".formatted(username, auth.getName());
}

本教程还将教您如何对安全表达式进行单元测试,即使使用私有声明和自定义身份验证也是如此。使用前面的表达式保护的终结点的测试如下:

@Test
@ProxiesAuth(
        authorities = { "AUTHOR" },
        claims = @OpenIdClaims(preferredUsername = "Tonton Pirate"),
        proxies = { @Proxy(onBehalfOf = "ch4mpy", can = { "greet" }) })
void whenNotNiceWithProxyThenCanGreetFor() throws Exception {
    mockMvc.get("/greet/on-behalf-of/ch4mpy")
        .andExpect(status().isOk())
        .andExpect(content().string("Hi ch4mpy from Tonton Pirate!"));
}

@Test
@ProxiesAuth(
        authorities = { "AUTHOR", "NICE" },
        claims = @OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenNiceWithoutProxyThenCanGreetFor() throws Exception {
    mockMvc.get("/greet/on-behalf-of/ch4mpy")
        .andExpect(status().isOk())
        .andExpect(content().string("Hi ch4mpy from Tonton Pirate!"));
}

@Test
@ProxiesAuth(
        authorities = { "AUTHOR" },
        claims = @OpenIdClaims(preferredUsername = "Tonton Pirate"),
        proxies = { @Proxy(onBehalfOf = "jwacongne", can = { "greet" }) })
void whenNotNiceWithoutRequiredProxyThenForbiddenToGreetFor() throws Exception {
    mockMvc.get("/greet/on-behalf-of/greeted")
        .andExpect(status().isForbidden());
}

@Test
@ProxiesAuth(
        authorities = { "AUTHOR" },
        claims = @OpenIdClaims(preferredUsername = "Tonton Pirate"))
void whenHimselfThenCanGreetFor() throws Exception {
    mockMvc.get("/greet/on-behalf-of/Tonton Pirate")
        .andExpect(status().isOk())
        .andExpect(content().string("Hi Tonton Pirate from Tonton Pirate!"));
}

相关问题