总体而言,我对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
的服务实现
当尝试执行此处描述的操作时,我总是得到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();
}
}
答案 0 :(得分:1)
在使用Webflux时,应该使用ReactiveSecurityContextHolder
来检索主体,例如:Object principal = ReactiveSecurityContextHolder.getContext().getAuthentication().getPrincipal();
如您所见,使用非反应性的将返回null。
此答案中有与该主题相关的更多信息-https://stackoverflow.com/a/51350355/197342
答案 1 :(得分:0)
根据另一个答案,我设法通过以下方式解决了这个问题。
我添加了以下方法,以从通常位于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);
}