当前位置: 首页 > 知识库问答 >
问题:

在编写测试时,在服务类中读取主体

姜弘新
2023-03-14

我对Spring的生态系统和网络流量相当陌生。有两件事我想弄清楚,但找不到任何细节。

我的设置:

我正在使用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线程本地来获取主体,如果我这样做了,我如何为我的服务编写测试?

当我尝试做这里描述的事情时,我总是得到null:使用Spring Security进行单元测试

在服务测试中,在测试中,到目前为止我所做的是将主体传播到服务方法中,并使用mockito来模拟主体。这很简单。在endpoint测试中,我使用@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();
    }

}

共有2个答案

习华灿
2023-03-14

根据另一个答案,我设法用以下方法解决了这个问题。

我添加了以下方法来从通常位于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);
    }

端木昱
2023-03-14

在使用Webflux时,应该使用ReactiveSecurityContextHolder检索主体,如下所示:Object principal=ReactiveSecurityContextHolder。getContext()。getAuthentication()。getPrincipal()

正如您所看到的,使用非反应式将返回null。

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

 类似资料:
  • 编写 junit 测试时: 我的代码编辑器(IntelliJ)显示警告 只有非静态嵌套类可以用作@嵌套测试类。 如何在 kotlin 的 junit 测试中编写嵌套类?

  • 问题内容: 我正在尝试开始为我的角度应用程序编写单元测试,并且由于无法确定如何以一种可测试的方式模拟服务而非常快地遇到了一个障碍。 有没有一种方法可以模拟REST调用,否则好像我需要在测试中镜像服务中的所有内容,这对我来说似乎不对,但是我对测试写作相当陌生,所以也许应该这样有待完成。任何帮助将不胜感激。 我的服务如下: 到目前为止,我的测试包括: 问题答案: 您可以像这样模拟ngResource发

  • 我已经编写了一个webservice,现在想为它编写一些单元测试。我偶然发现了Michael Hunger的内存服务器。 由于文档稀少,我很难只设置一个单元测试。我克隆了该项目,将其包含在我的工作区中,并将其作为依赖项添加到我的项目中。 为了测试我的web服务,我编写了以下方法来创建内存中的neo4j服务器: 当我运行此代码时,我得到以下输出: Mai 29,2014 10:13:17 PMsun

  • 我正在使用木偶人刮一个网站,有一个任务在特定的时间执行。我需要读取服务器时钟,这是张贴在页面上。页面上的时钟是不断变化的,我需要能够读取它,所以只要它显示“7:00:00 AM”,我的功能就会触发(不是一毫秒之前,否则我需要访问的页面将不会加载)。我也不想在早上7点以后等一秒钟,否则我可能订不到我的预约。 下面是我需要阅读的元素的html。当您在控制台中查看时,时间(html文本)会闪烁并不断变化

  • 在我的服务类中,我有@Autowired HttpServletRequest,并且在我的服务方法中使用相同的对象,但是对于那个服务方法测试类,我不能在我的测试方法中模拟HttpServletRequest对象,请检查下面的代码。我正在获取请求对象的空指针异常

  • 测试用来验证非测试的代码是否按照期望的方式运行的 Rust 函数。测试函数体通常执行如下三种操作: 设置任何所需的数据或状态 运行需要测试的代码 断言其结果是我们所期望的 让我们看看 Rust 提供的专门用来编写测试的功能:test 属性、一些宏和 should_panic 属性。 作为最简单例子,Rust 中的测试就是一个带有 test 属性注解的函数。属性(attribute)是关于 Rust