gpt4 book ai didi

spring-boot - Spring Boot Webflux Security - 编写测试时读取服务类中的 Principal

转载 作者:行者123 更新时间:2023-12-05 08:52:31 25 4
gpt4 key购买 nike

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

我的设置:

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

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

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

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

问题:

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

对象主体 = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

问题 1:

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

问题 2:

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

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

而且我在尝试执行此处描述的操作时总是得到 null:Unit 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();
}

}

最佳答案

根据其他答案,我设法通过以下方式解决了这个问题。

我添加了以下方法来从通常位于 JWT token 中的声明中读取 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);
}

关于spring-boot - Spring Boot Webflux Security - 编写测试时读取服务类中的 Principal,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56670759/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com