source

스프링 보안: OAuth2 클레임을 역할과 매핑하여 리소스 서버 엔드포인트 보호

manycodes 2023. 3. 12. 10:58
반응형

스프링 보안: OAuth2 클레임을 역할과 매핑하여 리소스 서버 엔드포인트 보호

Spring Boot을 사용하여 리소스 서버를 설정하고 Spring Security에서 제공하는 OAuth2를 사용하는 엔드포인트를 보호합니다.Spring Boot을 사용하고 있습니다.2.1.8.RELEASE예를 들어 Spring Security를 사용합니다.5.1.6.RELEASE.

인증 서버로서 Keyclock을 사용하고 있습니다.리소스 서버에서 인증, 액세스 토큰 발급 및 토큰 유효성 검사 사이의 모든 프로세스가 올바르게 작동합니다.다음으로 발행된 토큰과 디코딩된 토큰(일부 부분이 절단된 토큰)의 예를 나타냅니다.

{
  "jti": "5df54cac-8b06-4d36-b642-186bbd647fbf",
  "exp": 1570048999,
  "aud": [
    "myservice",
    "account"
  ],
  "azp": "myservice",
  "realm_access": {
    "roles": [
      "offline_access",
      "uma_authorization"
    ]
  },
  "resource_access": {
    "myservice": {
      "roles": [
        "ROLE_user",
        "ROLE_admin"
      ]
    },
    "account": {
      "roles": [
        "manage-account",
        "manage-account-links",
        "view-profile"
      ]
    }
  },
  "scope": "openid email offline_access microprofile-jwt profile address phone",
}

액세스 토큰 내의 정보를 사용하여 다른 엔드포인트에 조건부 허가를 제공하도록 Spring Security를 설정하려면 어떻게 해야 합니까?

최종적으로 다음과 같은 컨트롤러를 작성합니다.

@RestController
public class Controller {

    @Secured("ROLE_user")
    @GetMapping("userinfo")
    public String userinfo() {
        return "not too sensitive action";
    }

    @Secured("ROLE_admin")
    @GetMapping("administration")
    public String administration() {
        return "TOOOO sensitive action";
    }
}

좀 더 시간을 들여 본 결과, 커스텀을 구현한 솔루션을 찾을 수 있었습니다.jwtAuthenticationConverter권한 컬렉션에 리소스 고유의 역할을 추가할 수 있습니다.

    http.oauth2ResourceServer()
                .jwt()
                .jwtAuthenticationConverter(new JwtAuthenticationConverter()
                {
                    @Override
                    protected Collection<GrantedAuthority> extractAuthorities(final Jwt jwt)
                    {
                        Collection<GrantedAuthority> authorities = super.extractAuthorities(jwt);
                        Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
                        Map<String, Object> resource = null;
                        Collection<String> resourceRoles = null;
                        if (resourceAccess != null &&
                            (resource = (Map<String, Object>) resourceAccess.get("my-resource-id")) !=
                            null && (resourceRoles = (Collection<String>) resource.get("roles")) != null)
                            authorities.addAll(resourceRoles.stream()
                                                            .map(x -> new SimpleGrantedAuthority("ROLE_" + x))
                                                            .collect(Collectors.toSet()));
                        return authorities;
                    }
                });

여기서 my-resource-idresource_access 클레임에 표시되는 리소스 ID와 ResourceServerSecurityConfigr의 API와 관련된 값입니다.

주의해 주세요extractAuthorities실제로는 권장되지 않기 때문에 보다 미래 대비적인 솔루션은 완전한 컨버터를 구현하는 것입니다.

    import org.springframework.core.convert.converter.Converter;
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.oauth2.jwt.Jwt;
    import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
    import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;

    import java.util.Collection;
    import java.util.Collections;
    import java.util.Map;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

    public class CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken>
    {
        private static Collection<? extends GrantedAuthority> extractResourceRoles(final Jwt jwt, final String resourceId)
        {
            Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
            Map<String, Object> resource;
            Collection<String> resourceRoles;
            if (resourceAccess != null && (resource = (Map<String, Object>) resourceAccess.get(resourceId)) != null &&
                (resourceRoles = (Collection<String>) resource.get("roles")) != null)
                return resourceRoles.stream()
                                    .map(x -> new SimpleGrantedAuthority("ROLE_" + x))
                                    .collect(Collectors.toSet());
            return Collections.emptySet();
        }

        private final JwtGrantedAuthoritiesConverter defaultGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();

        private final String resourceId;

        public CustomJwtAuthenticationConverter(String resourceId)
        {
            this.resourceId = resourceId;
        }

        @Override
        public AbstractAuthenticationToken convert(final Jwt source)
        {
            Collection<GrantedAuthority> authorities = Stream.concat(defaultGrantedAuthoritiesConverter.convert(source)
                                                                                                       .stream(),
                                                                     extractResourceRoles(source, resourceId).stream())
                                                             .collect(Collectors.toSet());
            return new JwtAuthenticationToken(source, authorities);
        }
    }

Spring Boot 2.1.9를 사용하여 두 솔루션을 모두 테스트했습니다.릴리스, 스프링 보안 5.2.0.릴리스 및 Keycloak 7.0.0 도커 공식 이미지.

일반적으로 말하면, 실제 인가 서버가 무엇이든(즉,IdentityServer4, Keycloak...) 클레임을 Spring Security 조성금으로 전환하는 것이 적절한 장소인 것 같습니다.

여기 또 다른 해결책이 있습니다.

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
    }

문제가 발생하는 것은 JWT의 resource_server->client_id 아래에 역할이 배치되어 있기 때문입니다.그런 다음 이를 추출하려면 사용자 지정 토큰 변환기가 필요합니다.

롤과 같은 최상위 클레임 이름으로 역할을 표시하는 클라이언트 매퍼를 사용하도록 keyclock을 구성할 수 있습니다.JwtGranted만 필요하므로 Spring Security 구성이 간단해집니다.Authorities Converter with authoritiesClaimName은 @hillel_guy가 채택한 접근방식으로 설정되어 있습니다.

Keyclock 클라이언트매퍼는 다음과 같이 설정됩니다.

여기에 이미지 설명 입력

@했듯이 @hille_guy를 사용하여AbstractHttpConfigurer가야 할 길이에요.이것은 스프링 부트 2.3.4와 스프링 보안 5.3.4에서 심리스하게 동작했습니다.다음 OAuth2ResourceServerConfigr에 대해서는 spring-security API 매뉴얼을 참조하십시오.

갱신하다

코멘트에 기재되어 있는 완전한 예:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private static final String JWT_ROLE_NAME = "roles";
    private static final String ROLE_PREFIX = "ROLES_";

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests().anyRequest().authenticated()
                .and().csrf().disable()
                .cors()
                .and().oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
    }

    private JwtAuthenticationConverter jwtAuthenticationConverter() {
        // create a custom JWT converter to map the roles from the token as granted authorities
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(JWT_ROLE_NAME); // default is: scope, scp
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix(ROLE_PREFIX ); // default is: SCOPE_

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return jwtAuthenticationConverter;
    }
}

제작하고 싶었어요rolesscope.
이게 도움이 됐으면 좋겠다.

2022년 갱신

리소스 서버 보안을 구성하는 일련의 튜토리얼과 샘플을 관리하고 있습니다.

  • 서블릿 어플리케이션과 리액티브어플리케이션 모두
  • 디코딩, JWT 및 인스펙티브액세스 토큰
  • 커스텀 " " " " " "Authentication(예:
  • 물론 Keyclock을 포함한 임의의 OIDC 인가 서버(대부분의 샘플은 복수의 레름/아이덴티티 프로바이더를 서포트하고 있습니다.

또한 레포에는 maven-central에 게시된 libs 집합이 포함되어 있습니다.

  • 유닛 및 통합 테스트 중 OAuth2 ID 모의(당국 및 오픈)ID 클레임(개인 클레임
  • 속성 파일에서 리소스 서버 구성(역할, 역할 접두사 및 사례 처리, CORS 구성, 세션 관리, 퍼블릭 루트 등에 대한 소스 클레임 포함)

JWT 디코더가 있는 서블릿 샘플

@EnableMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig {}
com.c4-soft.springaddons.security.issuers[0].location=https://localhost:8443/realms/master
com.c4-soft.springaddons.security.issuers[0].authorities.claims=realm_access.roles,resource_access.spring-addons-public.roles,resource_access.spring-addons-confidential.roles
com.c4-soft.springaddons.security.cors[0].path=/sample/**
<dependency>
    <groupId>com.c4-soft.springaddons</groupId>
    <artifactId>spring-addons-webmvc-jwt-resource-server</artifactId>
    <version>6.0.3</version>
</dependency>

아뇨, 더 이상 필요 없습니다.

모의 인증을 사용한 유닛 테스트

★★★@Component를 사용하지 않음(「http request」)@Service,@Repository의 개요)

@Import({ SecurityConfig.class, SecretRepo.class })
@AutoConfigureAddonsSecurity
class SecretRepoTest {

    // auto-wire tested component
    @Autowired
    SecretRepo secretRepo;

    @Test
    void whenNotAuthenticatedThenThrows() {
        // call tested components methods directly (do not use MockMvc nor WebTestClient)
        assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
    }

    @Test
    @WithMockJwtAuth(claims = @OpenIdClaims(preferredUsername = "Tonton Pirate"))
    void whenAuthenticatedAsSomeoneElseThenThrows() {
        assertThrows(Exception.class, () -> secretRepo.findSecretByUsername("ch4mpy"));
    }

    @Test
    @WithMockJwtAuth(claims = @OpenIdClaims(preferredUsername = "ch4mpy"))
    void whenAuthenticatedWithSameUsernameThenReturns() {
        assertEquals("Don't ever tell it", secretRepo.findSecretByUsername("ch4mpy"));
    }

}

★★★@Controller:@WebMvcTest ᄂ, ᄂ, ᄂ, ᄂ, ᄂ, ᄂ, ᄂ, ᄂ, ᄂ, ᄂ, ᄂ, ᄂ@WebfluxTest대화)

@WebMvcTest(GreetingController.class) // Use WebFluxTest or WebMvcTest
@AutoConfigureAddonsWebSecurity // If your web-security depends on it, setup spring-addons security
@Import({ SecurityConfig.class }) // Import your web-security configuration
class GreetingControllerAnnotatedTest {

    // Mock controller injected dependencies
    @MockBean
    private MessageService messageService;

    @Autowired
    MockMvcSupport api;

    @BeforeEach
    public void setUp() {
        when(messageService.greet(any())).thenAnswer(invocation -> {
            final JwtAuthenticationToken auth = invocation.getArgument(0, JwtAuthenticationToken.class);
            return String.format("Hello %s! You are granted with %s.", auth.getName(), auth.getAuthorities());
        });
        when(messageService.getSecret()).thenReturn("Secret message");
    }

    @Test
    void greetWitoutAuthentication() throws Exception {
        api.get("/greet").andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockAuthentication(authType = JwtAuthenticationToken.class, principalType = Jwt.class, authorities = "ROLE_AUTHORIZED_PERSONNEL")
    void greetWithDefaultMockAuthentication() throws Exception {
        api.get("/greet").andExpect(content().string("Hello user! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
    }
}

고도의 사용 사례

합니다.Authentication구현: 프라이빗 클레임을 보안과 관련되지만 역할이 아닌 것으로 해석(Java 코드에 노출)합니다(예에서는 사용자 간에 위임 부여).

또한 스프링 보안 SpEL을 확장하여 다음과 같은 DSL을 구축하는 방법도 보여 줍니다.

@GetMapping("greet/on-behalf-of/{username}")
@PreAuthorize("is(#username) or isNice() or onBehalfOf(#username).can('greet')")
public String getGreetingFor(@PathVariable("username") String username) {
    return ...;
}

Azure AD 선서를 사용하는 경우, 보다 쉬운 방법이 있습니다.

        http 
        .cors()
        .and()
        .authorizeRequests()
        .anyRequest()
        .authenticated() 
        .and()
        .oauth2ResourceServer()
        .jwt()
        .jwtAuthenticationConverter(new AADJwtBearerTokenAuthenticationConverter("roles", "ROLE_")); 

ADDJwtBearerTokenAuthenticationConverter를 사용하면 첫 번째 인수로 클레임 이름을 추가하고 두 번째 인수로 역할 접두사를 추가할 수 있습니다.

라이브러리를 찾을 수 있도록 가져오기:

import com.azure.spring.aad.webapi.AADJwtBearerTokenAuthenticationConverter;

언급URL : https://stackoverflow.com/questions/58205510/spring-security-mapping-oauth2-claims-with-roles-to-secure-resource-server-endp

반응형