如何在 Spring Security 中测试 AuthenticationPrincipal 并获取 ID 令牌?

Posted

技术标签:

【中文标题】如何在 Spring Security 中测试 AuthenticationPrincipal 并获取 ID 令牌?【英文标题】:How to test AuthenticationPrincipal and getting an ID Token in Spring Security? 【发布时间】:2019-08-05 10:25:52 【问题描述】:

我有以下 LogoutResource 类,它返回一个 ID 令牌。

package com.mycompany.myapp.web.rest;

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

/**
* REST controller for managing global OIDC logout.
*/
@RestController
public class LogoutResource 
    private ClientRegistration registration;

    public LogoutResource(ClientRegistrationRepository registrations) 
        this.registration = registrations.findByRegistrationId("oidc");
    

    /**
     * @code POST  /api/logout : logout the current user.
     *
     * @param request the @link HttpServletRequest.
     * @param idToken the ID token.
     * @return the @link ResponseEntity with status @code 200 (OK) and a body with a global logout URL and ID token.
     */
    @PostMapping("/api/logout")
    public ResponseEntity<?> logout(HttpServletRequest request,
                                    @AuthenticationPrincipal(expression = "idToken") OidcIdToken idToken) 
        String logoutUrl = this.registration.getProviderDetails()
            .getConfigurationMetadata().get("end_session_endpoint").toString();

        Map<String, String> logoutDetails = new HashMap<>();
        logoutDetails.put("logoutUrl", logoutUrl);
        logoutDetails.put("idToken", idToken.getTokenValue());
        request.getSession().invalidate();
        return ResponseEntity.ok().body(logoutDetails);
    

这可行,但我想测试一下。我尝试了以下方法:

package com.mycompany.myapp.web.rest;

import com.mycompany.myapp.JhipsterApp;
import com.mycompany.myapp.config.Constants;
import com.mycompany.myapp.security.AuthoritiesConstants;
import org.junit.Before;
import org.junit.Test;
import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAmount;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

/**
 * Integration tests for the @link LogoutResource REST controller.
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = JhipsterApp.class)
public class LogoutResourceIT 

    @Autowired
    private ClientRegistrationRepository registrations;

    @Autowired
    private MappingJackson2HttpMessageConverter jacksonMessageConverter;

    private final static String ID_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" +
        ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsIm" +
        "p0aSI6ImQzNWRmMTRkLTA5ZjYtNDhmZi04YTkzLTdjNmYwMzM5MzE1OSIsImlhdCI6MTU0M" +
        "Tk3MTU4MywiZXhwIjoxNTQxOTc1MTgzfQ.QaQOarmV8xEUYV7yvWzX3cUE_4W1luMcWCwpr" +
        "oqqUrg";

    private MockMvc restLogoutMockMvc;

    @Before
    public void before() 
        LogoutResource logoutResource = new LogoutResource(registrations);
        this.restLogoutMockMvc = MockMvcBuilders.standaloneSetup(logoutResource)
            .setMessageConverters(jacksonMessageConverter).build();
    

    @Test
    public void getLogoutInformation() throws Exception 

        Map<String, Object> claims = new HashMap<>();
        claims.put("groups", "ROLE_USER");
        claims.put("sub", 123);
        OidcIdToken idToken = new OidcIdToken(ID_TOKEN, Instant.now(),
            Instant.now().plusSeconds(60), claims);

        String logoutUrl = this.registrations.findByRegistrationId("oidc").getProviderDetails()
            .getConfigurationMetadata().get("end_session_endpoint").toString();
        restLogoutMockMvc.perform(post("/api/logout")
            .with(authentication(createMockOAuth2AuthenticationToken(idToken))))
            .andExpect(status().isOk())
            .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
            .andExpect(jsonPath("$.logoutUrl").value(logoutUrl));
    

    private OAuth2AuthenticationToken createMockOAuth2AuthenticationToken(OidcIdToken idToken) 
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.USER));
        OidcUser user = new DefaultOidcUser(authorities, idToken);

        return new OAuth2AuthenticationToken(user, authorities, "oidc");
    

但是,这会导致以下错误:

Caused by: java.lang.IllegalArgumentException: tokenValue cannot be empty
    at org.springframework.util.Assert.hasText(Assert.java:284)
    at org.springframework.security.oauth2.core.AbstractOAuth2Token.<init>(AbstractOAuth2Token.java:55)
    at org.springframework.security.oauth2.core.oidc.OidcIdToken.<init>(OidcIdToken.java:53)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:490)
    at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:172)

有谁知道模拟 AuthenticationPrincipal 并让它返回预设 ID 令牌的方法?

【问题讨论】:

【参考方案1】:

Spring 安全团队的 Joe Grandja 提供的答案:


AuthenticationPrincipalArgumentResolver 未在您的测试中注册。

注意:当启用“完整”spring-web-mvc 时,它会自动注册,例如 @EnableWebMvc

但是,在您的 @Before 中,您有:

MockMvcBuilders.standaloneSetup() - 这不会初始化完整的 web-mvc 基础架构 - 只是一个子集

试试这个:

MockMvcBuilders.webAppContextSetup(this.context) - 这将注册AuthenticationPrincipalArgumentResolver,您的测试应该会解析OidcIdToken


我将测试更改为以下内容,现在一切都通过了。谢谢乔!

    package com.mycompany.myapp.web.rest;
    
    import com.mycompany.myapp.JhipsterApp;
    import com.mycompany.myapp.security.AuthoritiesConstants;
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.http.MediaType;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.context.SecurityContextHolder;
    import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
    import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
    import org.springframework.security.oauth2.core.oidc.OidcIdToken;
    import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
    import org.springframework.security.oauth2.core.oidc.user.OidcUser;
    import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.setup.MockMvcBuilders;
    import org.springframework.web.context.WebApplicationContext;
    
    import java.time.Instant;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    
    /**
     * Integration tests for the @link LogoutResource REST controller.
     */
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes = JhipsterApp.class)
    public class LogoutResourceIT 
    
        @Autowired
        private ClientRegistrationRepository registrations;
    
        @Autowired
        private WebApplicationContext context;
    
        private final static String ID_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" +
            ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsIm" +
            "p0aSI6ImQzNWRmMTRkLTA5ZjYtNDhmZi04YTkzLTdjNmYwMzM5MzE1OSIsImlhdCI6MTU0M" +
            "Tk3MTU4MywiZXhwIjoxNTQxOTc1MTgzfQ.QaQOarmV8xEUYV7yvWzX3cUE_4W1luMcWCwpr" +
            "oqqUrg";
    
        private MockMvc restLogoutMockMvc;
    
        @Before
        public void before() throws Exception 
            Map<String, Object> claims = new HashMap<>();
            claims.put("groups", "ROLE_USER");
            claims.put("sub", 123);
            OidcIdToken idToken = new OidcIdToken(ID_TOKEN, Instant.now(),
                Instant.now().plusSeconds(60), claims);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken(idToken));
            SecurityContextHolderAwareRequestFilter authInjector = new SecurityContextHolderAwareRequestFilter();
            authInjector.afterPropertiesSet();
    
            this.restLogoutMockMvc = MockMvcBuilders.webAppContextSetup(this.context).build();
        
    
        @Test
        public void getLogoutInformation() throws Exception 
            String logoutUrl = this.registrations.findByRegistrationId("oidc").getProviderDetails()
                .getConfigurationMetadata().get("end_session_endpoint").toString();
            restLogoutMockMvc.perform(post("/api/logout"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
                .andExpect(jsonPath("$.logoutUrl").value(logoutUrl))
                .andExpect(jsonPath("$.idToken").value(ID_TOKEN));
        
    
        private OAuth2AuthenticationToken authenticationToken(OidcIdToken idToken) 
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(AuthoritiesConstants.USER));
            OidcUser user = new DefaultOidcUser(authorities, idToken);
            return new OAuth2AuthenticationToken(user, authorities, "oidc");
        
    

【讨论】:

以上是关于如何在 Spring Security 中测试 AuthenticationPrincipal 并获取 ID 令牌?的主要内容,如果未能解决你的问题,请参考以下文章

如何在Grails 2单元测试中获取spring security提供的currentUser

如何在 Spring Security 中测试 AuthenticationPrincipal 并获取 ID 令牌?

如何使用 Spring Security 对 Spring 4 DAO 方法进行单元测试?

如何在 Spring Security Bean 中模拟自定义 UserService 详细信息以进行单元测试?

如何使用 Spring Security @Secured 注解测试 Spring Boot @WebFluxTest

如何修复 Spring Security Authorization 标头未通过?