Spring Boot webservice (REST) - 如何将 JUnit 5 测试从基本身份验证更改为 OAuth2 (Keycloak)

Posted

技术标签:

【中文标题】Spring Boot webservice (REST) - 如何将 JUnit 5 测试从基本身份验证更改为 OAuth2 (Keycloak)【英文标题】:Spring Boot webservice (REST) - How to change JUnit 5 tests from basic authentication to OAuth2 (Keycloak) 【发布时间】:2021-11-01 22:19:18 【问题描述】:

我有一个带有REST 控制器和基本身份验证(用户名和密码)的Spring Boot 网络服务。

在此基础上我开发了 JUnit 5 测试。

现在我切换到OAuth2,目前正在尝试Resource Owner Password Credentials 授权类型。

我需要对我的JUnit 5 测试进行哪些更改才能现在使用OAuth2 运行? 当然,在使用 OAuth2 运行我的新测试之前,我必须先开始 Keycloak,然后再进行测试。

以下是我对当前基本身份验证和新 OAuth2 的设置。

基本身份验证(旧实现)

在我的网络服务端,网络安全配置类如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter 
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception 
        httpSecurity
            .httpBasic()
            .and()
            .authorizeRequests()
            .antMatchers("/").permitAll()
            
            .antMatchers("/articles/**").hasRole("ADMIN")
            // More antMatchers...
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .csrf().disable()
            .formLogin().disable();
        

    @Bean
    @Override
    public UserDetailsService userDetailsService() 
        UserDetails admin = User
            .withUsername("admin")
            .password("noop" + "admin123")
            .roles("ADMIN")
            .build();
            
        // More users...

        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();

        userDetailsManager.createUser(admin);
        ...

        return userDetailsManager;
       

对于 JUnit 5 测试,我总是使用用户 admin,例如

@SpringBootTest
@AutoConfigureMockMvc
@WithUserDetails(value = "admin")
@TestInstance(Lifecycle.PER_CLASS)
public class MyRestControllerMockMvcTest 
    @Autowired
    private MockMvc mockMvc;
    
    @BeforeAll
    public void init(ApplicationContext appContext) throws Exception 
        TestUtils.setupSecurityContext(appContext);
        
        // some initialization
    
    
    @AfterAll
    public void cleanup(ApplicationContext appContext) throws Exception 
        TestUtils.setupSecurityContext(appContext);
        
        // some cleanup
    
    
    @Test
    public void getSomeInformationFromMyRestController() throws Exception 
        MvcResult mvcResult = TestUtils.performGet(mockMvc, "...REST controller endpoint...", status().isOk());

        MockHttpServletResponse response = mvcResult.getResponse();
        ObjectMapper objectMapper = new ObjectMapper();

        ... = objectMapper.readValue(response.getContentAsString(), ...);

        assertNotNull(...);
        


public class TestUtils 
    public static void setupSecurityContext(ApplicationContext appContext) 
        UserDetailsService uds = (UserDetailsService) appContext.getBean("userDetailsService");
        UserDetails userDetails = uds.loadUserByUsername ("admin");
        Authentication authToken = new UsernamePasswordAuthenticationToken (userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authToken);
       

    public static MvcResult performGet(MockMvc mockMvc, String endpoint, ResultMatcher status) throws Exception 
        MvcResult mvcResult = mockMvc.perform(get(endpoint))
            .andDo(print())
            .andExpect(status)
            .andReturn();

        return mvcResult;
    

现在查看@BeforeAll@AfterAll 中的测试设置,我突然不确定是否必须这样做

TestUtils.setupSecurityContext(appContext);

因为我现在用

@WithUserDetails(value = "admin")
@TestInstance(Lifecycle.PER_CLASS)

在课堂上。只是好奇如果没有TestUtils.setupSecurityContext(appContext);,测试是否仍能运行,将尝试。


OAUTH2(新实现,替换上面的基本身份验证)

application.properties

...
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8183/auth/realms/myrealm/protocol/openid-connect/certs

使用OAuth2,我将我的网络服务(资源服务器)中的网络安全配置类更改如下:

@EnableWebSecurity
public class WebSecurityConfig 
    @Bean
    SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception 
        httpSecurity
            .authorizeRequests()
            .antMatchers("/").permitAll()

            .antMatchers("/articles/**").hasRole("ADMIN")
            // More antMatchers...          
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .csrf().disable()
            .oauth2ResourceServer()
            .jwt()
            .jwtAuthenticationConverter(jwtAuthenticationConverter())
            ;

        return httpSecurity.build();
    
    
    private JwtAuthenticationConverter jwtAuthenticationConverter() 
        final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new MyRoleConverter());
        
        return jwtAuthenticationConverter;
        

    public class MyRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> 
        @Override
        public Collection<GrantedAuthority> convert(final Jwt jwt) 
            jwt.getClaims().get("realm_access");
            
            // Create roles

            return ...;
        
    

我的用户现在在Keycloak 中定义。 Keycloak 配置为使用Resource Owner Password Credentials

【问题讨论】:

在大多数情况下,身份验证方式对您的控制器来说并不重要,所以我想知道您是否必须做任何不同的事情。你有问题吗? 是否有可能运行测试以完全破坏安全性?怎么样? 在测试类上将此注释更改为@AutoConfigureMockMvc(secure = false),它不知道“安全”。 Spring Boot 2.5.4,Junit 5。 【参考方案1】:

@jzheaux 是对的(当然,他是 spring-security 团队的成员......)。

您的安全配置会发生变化,但测试不会改变……在大多数情况下:您可能希望在测试安全上下文中拥有一个正确类型的 Authentication

如果您的新安全配置使用JwtAuthenticationToken 填充安全上下文,那么在测试安全上下文中也有JwtAuthenticationToken 会很好。 @WithUserDetails(value = "admin") 不会构建 JwtAuthenticationToken

你应该看看我写的this lib,特别是@WithMockJwtAuth。用法演示there:

    @Test
    @WithMockJwtAuth(authorities = "ROLE_AUTHORIZED_PERSONNEL", claims = @OpenIdClaims(sub = "Ch4mpy"))
    public void greetJwtCh4mpy() throws Exception 
        api.get("/greet").andExpect(content().string("Hello Ch4mpy! You are granted with [ROLE_AUTHORIZED_PERSONNEL]."));
    

附: 你会在同一个 git repo 示例中找到其他类型的 AuthenticationJwtAuthenticationToken 更适合 OIDC,例如 KeycloakAuthenticationToken(由 Keycloak 团队专门为 Keycloak 编写)或 OidcAuthentication(由我自己为任何 OpenID Connect 编写兼容授权服务器),以及@WithMockKeycloakAuth@WithMockOidcAuth

【讨论】:

以上是关于Spring Boot webservice (REST) - 如何将 JUnit 5 测试从基本身份验证更改为 OAuth2 (Keycloak)的主要内容,如果未能解决你的问题,请参考以下文章

Spring boot 开发WebService遇到的问题之一

spring boot整合cxf发布和调用webservice

使用 spring-boot 和 Weblogic 公开 SOAP Webservice

Spring Boot 实现RESTful webservice服务端实例

Spring Boot 实现RESTful webservice服务端示例

spring boot rest webservice,如何改进干净的代码?