【问题标题】:Mock JWT AuthenticationPrincipal while testing REST end points在测试 REST 端点时模拟 JWT AuthenticationPrincipal
【发布时间】:2021-05-14 07:55:13
【问题描述】:

我们正在构建 REST API 并添加 AuthenticationPrincipal 作为方法参数

@Override
public ResponseEntity<MappingJacksonValue> listProduct(
    @Valid @RequestParam(value = "fields", required = false) final String fields,
    @Valid @RequestParam(value = "offset", required = false) final Integer offset,
    @Valid @RequestParam(value = "limit", required = false) final Integer limit,
    @AuthenticationPrincipal final Jwt jwt) throws Exception {

    //Extract roles from jwt and do advanced role checking. 
}

使用 mockmvc 进行测试时

@Autowired
private WebApplicationContext context;

@BeforeEach
public void setup() {
    mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(springSecurity())
            .build();
} 

@Test
void testGetListProductsWithFields() throws Exception {
    Map<String, Object> attributeMap = new HashMap<>();
    attributeMap.put(null, "");

    //Code

    final var result = mvc
    .perform(get("/api" + requestFields)
        .with(jwt().authorities(new OAuth2UserAuthority("ROLE_my_roll", attributeMap)))
        .accept(MediaType.APPLICATION_JSON)
        .contentType(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk())
    .andReturn();
}

JWT 令牌正在使用 ModelAttributeMethodProcessor 的构造函数 (org.springframework.security.oauth2.jwt.Jwt) 进行初始化,并且由于没有发送令牌,因此抛出了断言错误。想法是不要连接到身份验证提供程序来生成令牌

呼叫跟踪

Jwt(AbstractOAuth2Token).<init>(String, Instant, Instant) line: 55  
Jwt.<init>(String, Instant, Instant, Map<String,Object>, Map<String,Object>) line: 69   
NativeConstructorAccessorImpl.newInstance0(Constructor<?>, Object[]) line: not available [native method]    
NativeConstructorAccessorImpl.newInstance(Object[]) line: 62    
DelegatingConstructorAccessorImpl.newInstance(Object[]) line: 45    
Constructor<T>.newInstance(Object...) line: 490 
BeanUtils.instantiateClass(Constructor<T>, Object...) line: 204 
ServletModelAttributeMethodProcessor(ModelAttributeMethodProcessor).constructAttribute(Constructor<?>, String, MethodParameter, WebDataBinderFactory, NativeWebRequest) line: 320   
ServletModelAttributeMethodProcessor(ModelAttributeMethodProcessor).createAttribute(String, MethodParameter, WebDataBinderFactory, NativeWebRequest) line: 224  
ServletModelAttributeMethodProcessor.createAttribute(String, MethodParameter, WebDataBinderFactory, NativeWebRequest) line: 85  
ServletModelAttributeMethodProcessor(ModelAttributeMethodProcessor).resolveArgument(MethodParameter, ModelAndViewContainer, NativeWebRequest, WebDataBinderFactory) line: 139   
HandlerMethodArgumentResolverComposite.resolveArgument(MethodParameter, ModelAndViewContainer, NativeWebRequest, WebDataBinderFactory) line: 121    
ServletInvocableHandlerMethod(InvocableHandlerMethod).getMethodArgumentValues(NativeWebRequest, ModelAndViewContainer, Object...) line: 167 
ServletInvocableHandlerMethod(InvocableHandlerMethod).invokeForRequest(NativeWebRequest, ModelAndViewContainer, Object...) line: 134    
ServletInvocableHandlerMethod.invokeAndHandle(ServletWebRequest, ModelAndViewContainer, Object...) line: 105    
RequestMappingHandlerAdapter.invokeHandlerMethod(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 878  
RequestMappingHandlerAdapter.handleInternal(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 792   
RequestMappingHandlerAdapter(AbstractHandlerMethodAdapter).handle(HttpServletRequest, HttpServletResponse, Object) line: 87 
TestDispatcherServlet(DispatcherServlet).doDispatch(HttpServletRequest, HttpServletResponse) line: 1040 
TestDispatcherServlet(DispatcherServlet).doService(HttpServletRequest, HttpServletResponse) line: 943   
TestDispatcherServlet(FrameworkServlet).processRequest(HttpServletRequest, HttpServletResponse) line: 1006  
TestDispatcherServlet(FrameworkServlet).doGet(HttpServletRequest, HttpServletResponse) line: 898    
TestDispatcherServlet(HttpServlet).service(HttpServletRequest, HttpServletResponse) line: 626   
TestDispatcherServlet(FrameworkServlet).service(HttpServletRequest, HttpServletResponse) line: 883  
TestDispatcherServlet.service(HttpServletRequest, HttpServletResponse) line: 72 
TestDispatcherServlet(HttpServlet).service(ServletRequest, ServletResponse) line: 733   
MockFilterChain$ServletFilterProxy.doFilter(ServletRequest, ServletResponse, FilterChain) line: 167 
MockFilterChain.doFilter(ServletRequest, ServletResponse) line: 134 
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 320 
FilterSecurityInterceptor.invoke(FilterInvocation) line: 126    
FilterSecurityInterceptor.doFilter(ServletRequest, ServletResponse, FilterChain) line: 90   
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
ExceptionTranslationFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 118 
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
SessionManagementFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 137    
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
AnonymousAuthenticationFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 111  
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
SecurityContextHolderAwareRequestFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 158    
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
RequestCacheAwareFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 63 
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
BearerTokenAuthenticationFilter.doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain) line: 114    
BearerTokenAuthenticationFilter(OncePerRequestFilter).doFilter(ServletRequest, ServletResponse, FilterChain) line: 119  
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
LogoutFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 116   
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
CsrfFilter(OncePerRequestFilter).doFilter(ServletRequest, ServletResponse, FilterChain) line: 103   
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
HeaderWriterFilter.doHeadersAfter(HttpServletRequest, HttpServletResponse, FilterChain) line: 92    
HeaderWriterFilter.doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain) line: 77  
HeaderWriterFilter(OncePerRequestFilter).doFilter(ServletRequest, ServletResponse, FilterChain) line: 119   
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
SecurityContextPersistenceFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 105   
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
WebAsyncManagerIntegrationFilter.doFilterInternal(HttpServletRequest, HttpServletResponse, FilterChain) line: 56    
WebAsyncManagerIntegrationFilter(OncePerRequestFilter).doFilter(ServletRequest, ServletResponse, FilterChain) line: 119 
FilterChainProxy$VirtualFilterChain.doFilter(ServletRequest, ServletResponse) line: 334 
FilterChainProxy.doFilterInternal(ServletRequest, ServletResponse, FilterChain) line: 215   
FilterChainProxy.doFilter(ServletRequest, ServletResponse, FilterChain) line: 178   
SecurityMockMvcConfigurer$DelegateFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 132   
MockFilterChain.doFilter(ServletRequest, ServletResponse) line: 134 
MockMvc.perform(RequestBuilder) line: 183   

尝试将 jwt 构造函数参数作为请求参数传递。

Map<String, Object> attributeMap = new HashMap<>();
attributeMap.put(null, "");

//Code

final Map<String, String> headerMap = new HashMap<>();
headerMap.put("alg", "none");

final var mapper = new ObjectMapper();
final var jsonMap = mapper.writeValueAsString(headerMap);

final var result = mvc
.perform(get("/api" + requestFields)
        .with(jwt().authorities(new OAuth2UserAuthority("ROLE_my_roll", attributeMap)))
        .param("tokenValue", "tokenValue")
        .param("!headers", jsonMap)
        .accept(MediaType.APPLICATION_JSON)
        .contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();

但它会引发不同的错误

Cannot convert value of type 'java.lang.String' to required type 'java.util.Map': no matching editors or conversion strategy found

尝试添加转换器。但没有运气

@Component
public class StringToMapConverter implements Converter<String, Map<String, Object>> {

    @Override
    public Map<String, Object> convert(final String source) {
        try {
            return new ObjectMapper().readValue(source,
                    new TypeReference<Map<String, Object>>() {});
        } catch (final IOException e) {
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    public JavaType getInputType(final TypeFactory typeFactory) {
        return typeFactory.constructFromCanonical(String.class.getName());
    }

    @Override
    public JavaType getOutputType(final TypeFactory typeFactory) {
        return typeFactory.constructFromCanonical(Map.class.getName());
    }
}

我可以通过扩展WebMvcConfigurationSupport 并证明argumentResolver 来解决这个问题

//Test configuration

@ContextConfiguration
@EnableWebSecurity
@TestConfiguration
public class ProductApiConfiguration extends WebMvcConfigurationSupport {

    @Override
    protected void addArgumentResolvers(
            final List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(getArgumentResolver());
    }

    private HandlerMethodArgumentResolver getArgumentResolver() {
        return new HandlerMethodArgumentResolver() {
            @Override
            public boolean supportsParameter(final MethodParameter parameter) {
                return parameter.getParameterType().isAssignableFrom(Jwt.class);
            }

            @Override
            public Object resolveArgument(final MethodParameter parameter,
                    final ModelAndViewContainer mavContainer,
                    final NativeWebRequest webRequest, final WebDataBinderFactory binderFactory)
                    throws Exception {

                final var roles = new JSONArray();
                roles.add("my_role");

                final Map<String, JSONArray> jsonMap = new HashMap<>();
                jsonMap.put("roles", roles);

                final var claimObject = new JSONObject(jsonMap);
                final var jwt = Jwt.withTokenValue("token")
                        .header("alg", "none")
                        .claim("realm_access", claimObject)
                        .build();
                final Collection<GrantedAuthority> authorities =
                        AuthorityUtils.createAuthorityList("ROLE_my_roll");
                final var token = new JwtAuthenticationToken(jwt, authorities);
                return token.getToken();
            }
        };
    }
}

由于这不是每个测试,我无法为每个测试更改 jwt 令牌并验证不同的场景。

当一个 API 被 webmvc 测试时,有没有一种简单的方法来模拟控制器类中的方法参数?

【问题讨论】:

    标签: spring-boot spring-security jwt spring-test spring-test-mvc


    【解决方案1】:

    我们可以通过设置 SecurityContextHolder 来解决这个问题

    @Autowired
    private MockMvc mvc;
    
    @Autowired
    private WebApplicationContext context;
    
    @MockBean
    SecurityContext securityContext;
    
    @Test
    public void test() {
    
        //some code
    
        given(securityContext.getAuthentication())
            .willReturn(Utils
                .getMockJwtToken("my_role", "testSubject"));
        SecurityContextHolder.setContext(securityContext);
    
        final var result = this.mvc
            .perform(get("/api" + requestFields)
            .accept(MediaType.APPLICATION_JSON)
            .contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andReturn();
    }
    

    和测试工具类

    public class Utils {
    
        private Utils() {
        
        }
    
        public static JwtAuthenticationToken getMockJwtToken(String role, String subject){
            final var roles = new JSONArray();
            roles.add(role);
    
            final Map<String, JSONArray> jsonMap = new HashMap<>();
            jsonMap.put("roles", roles);
    
            final var claimObject = new JSONObject(jsonMap);
            final var jwt = Jwt.withTokenValue("token")
                    .header("alg", "none")
                    .claim("realm_access", claimObject)
                    .subject(subject)
                    .build();
            final Collection<GrantedAuthority> authorities =
                    AuthorityUtils.createAuthorityList("ROLE_my_role));
            return new JwtAuthenticationToken(jwt, authorities);
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-02-10
      • 1970-01-01
      • 1970-01-01
      • 2016-11-14
      • 1970-01-01
      相关资源
      最近更新 更多