【问题标题】:Spring Boot Webflux Security - reading Principal in service class when writing testsSpring Boot Webflux Security - 编写测试时读取服务类中的主体
【发布时间】:2019-11-02 09:01:19
【问题描述】:

我对 Spring 生态系统和 Webflux 很陌生。有两件事我想弄清楚,但找不到任何细节。

我的设置:

我正在使用 WebFlux 编写 Spring Boot 2 REST API(不使用控制器,而是使用处理函数)。身份验证服务器是一个单独的服务,它发出 JWT 令牌,这些令牌作为身份验证标头附加到每个请求。下面是一个简单的请求方法示例:

public Mono<ServerResponse> all(ServerRequest serverRequest) {
        return principal(serverRequest).flatMap(principal ->
                ReactiveResponses.listResponse(this.projectService.all(principal)));
    }

我用它来响应用户有权访问的所有“项目”列表的 GET 请求。

之后我有一个服务来检索该用户的项目列表并呈现一个 json 响应。

问题:

现在为了根据当前用户 ID 过滤项目,我需要从请求主体中读取它。这里的一个问题是我有很多需要当前用户信息的服务方法,并将其传递给服务似乎有点矫枉过正。一种解决方案是从以下位置读取服务内的主体:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

问题 1:

在编写功能代码时,这通常是一个好习惯吗(如果我这样做而不是传播主体)?尽管在每种方法中从请求读取主体并将其发送到服务很复杂,但这是一种好方法吗?

问题 2:

我是否应该改用 SecurityContextHolder Thread Local 来获取主体,如果这样做,我该如何为我的服务编写测试?

如果我使用安全上下文,我该如何测试我的服务实现,它需要一个类型为 JWTAuthenticationToken 的主体

当我尝试做类似这里描述的事情时,我总是得到nullUnit testing with Spring Security

在服务测试中,在测试中我到目前为止所做的是将主体传播到服务方法并使用 mockito 来模拟主体。这很简单。 在端点测试中,我在执行请求时使用@WithMockUser 填充主体,并且我模拟了服务层。这具有主要类型不同的缺点。

这是我的服务层测试类的外观:

@DataMongoTest
@Import({ProjectServiceImpl.class})
class ProjectServiceImplTest extends BaseServiceTest {

    @Autowired
    ProjectServiceImpl projectService;

    @Autowired
    ProjectRepository projectRepository;

    @Mock
    Principal principal;

    @Mock
    Principal principal2;

    @BeforeEach
    void setUp() {
        initMocks(this);

        when(principal.getName()).thenReturn("uuid");
        when(principal2.getName()).thenReturn("uuid2");
    }

    // Cleaned for brevity 

    @Test
    public void all_returnsOnlyOwnedProjects() {
        Flux<Project> saved = projectRepository.saveAll(
                Flux.just(
                        new Project(null, "First", "uuid"),
                        new Project(null, "Second", "uuid2"),
                        new Project(null, "Third", "uuid3")
                )
        );
        Flux<Project> all = projectService.all(principal2);
        Flux<Project> composite = saved.thenMany(all);

        StepVerifier
                .create(composite)
                .consumeNextWith(project -> {
                    assertThat(project.getOwnerUserId()).isEqualTo("uuid2");
                })
                .verifyComplete();
    }

}

【问题讨论】:

    标签: spring-boot spring-security spring-security-oauth2 spring-webflux


    【解决方案1】:

    当您使用 Webflux 时,您应该使用 ReactiveSecurityContextHolder 来检索主体,如下所示:Object principal = ReactiveSecurityContextHolder.getContext().getAuthentication().getPrincipal();

    如您所见,使用非反应式将返回 null。

    此答案中有更多与该主题相关的信息 - https://stackoverflow.com/a/51350355/197342

    【讨论】:

    • 非常感谢。我不知道我是怎么错过的。它确实将我带到了文档并帮助使用了自定义模拟令牌。我将在下面发布我的最终解决方案作为参考。
    【解决方案2】:

    基于另一个答案,我设法通过以下方式解决了这个问题。

    我添加了以下方法来从通常位于 JWT 令牌中的声明中读取 id。

        public static Mono<String> currentUserId() {
            return jwt().map(jwt -> jwt.getClaimAsString(USER_ID_CLAIM_NAME));
        }
    
    
        public static Mono<Jwt> jwt() {
            return ReactiveSecurityContextHolder.getContext()
                    .map(context -> context.getAuthentication().getPrincipal())
                    .cast(Jwt.class);
        }
    

    然后我在我的服务中根据需要使用它,我不会通过处理程序将它转发到服务。

    棘手的部分始终是测试。我可以使用自定义的 SecurityContextFactory 来解决这个问题。我创建了一个注释,我可以使用与 @WithMockUser 相同的方式附加它,但我需要一些声明细节。

    @Retention(RetentionPolicy.RUNTIME)
    @WithSecurityContext(factory = WithMockTokenSecurityContextFactory.class)
    public @interface WithMockToken {
        String sub() default "uuid";
        String email() default "test@test.com";
        String name() default "Test User";
    }
    

    然后是工厂:

    String token = "....ANY_JWT_TOKEN_GOES_HERE";
    
        @Override
        public SecurityContext createSecurityContext(WithMockToken tokenAnnotation) {
            SecurityContext context = SecurityContextHolder.createEmptyContext();
            HashMap<String, Object> headers = new HashMap<>();
            headers.put("kid", "SOME_ID");
            headers.put("typ", "JWT");
            headers.put("alg", "RS256");
            HashMap<String, Object> claims = new HashMap<>();
            claims.put("sub", tokenAnnotation.sub());
            claims.put("aud", new ArrayList<>() {{
                add("SOME_ID_HERE");
            }});
            claims.put("updated_at", "2019-06-24T12:16:17.384Z");
            claims.put("nickname", tokenAnnotation.email().substring(0, tokenAnnotation.email().indexOf("@")));
            claims.put("name", tokenAnnotation.name());
            claims.put("exp", new Date());
            claims.put("iat", new Date());
            claims.put("email", tokenAnnotation.email());
            Jwt jwt = new Jwt(token, Instant.now(), Instant.now().plus(1, ChronoUnit.HOURS), headers,
                    claims);
            JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(jwt, AuthorityUtils.NO_AUTHORITIES); // Authorities are needed to pass authentication in the Integration tests
            context.setAuthentication(jwtAuthenticationToken);
    
    
            return context;
        }
    
    

    然后一个简单的测试将如下所示:

        @Test
        @WithMockToken(sub = "uuid2")
        public void delete_whenNotOwner() {
            Mono<Void> deleted = this.projectService.create(projectDTO)
                    .flatMap(saved -> this.projectService.delete(saved.getId()));
    
            StepVerifier
                    .create(deleted)
                    .verifyError(ProjectDeleteNotAllowedException.class);
        }
    
    

    【讨论】:

      猜你喜欢
      • 2021-03-03
      • 2018-09-28
      • 1970-01-01
      • 2021-03-15
      • 2018-10-05
      • 1970-01-01
      • 2019-01-20
      • 2020-09-18
      • 2019-01-21
      相关资源
      最近更新 更多