使用JWT的REST API的Spring Security

639 阅读19分钟

REST API的Spring安全与JWT

Spring被认为是Java生态系统中一个值得信赖的框架,并被广泛使用。将Spring称为一个框架已不再有效,因为它更像是一个涵盖各种框架的总称。这些框架之一是Spring Security,它是一个强大的、可定制的认证和授权框架。它被认为是保护基于Spring的应用程序的事实上的标准。

尽管它很受欢迎,但我必须承认,当涉及到单页应用程序时,它的配置并不简单明了。我怀疑原因是它开始时更多地是作为一个面向MVC应用的框架,网页渲染发生在服务器端,通信是基于会话的。

如果后端是基于Java和Spring的,那么使用Spring Security进行认证/授权并将其配置为无状态通信是有意义的。虽然有很多文章解释了如何做到这一点,但对我来说,第一次设置它仍然是令人沮丧的,我不得不阅读和总结来自多个来源的信息。这就是为什么我决定写这篇文章,我将尝试总结并涵盖所有需要的微妙细节和配置过程中可能遇到的缺陷。

定义术语

在深入探讨技术细节之前,我想明确定义Spring Security上下文中使用的术语,以确保我们都说同样的语言。

这些是我们需要解决的术语

  • 认证是指根据所提供的凭证来验证用户身份的过程。一个常见的例子是当你登录到一个网站时输入一个用户名和密码。你可以把它看作是对 "你是谁"这一问题的回答。
  • 授权指的是确定用户是否有适当的权限来执行一个特定的动作或阅读特定的数据的过程,假设用户被成功认证。你可以把它看作是对 "用户可以做/读这个吗"这一问题的回答。
  • 原则上指的是当前认证的用户。
  • 授予的权限指的是被认证的用户的权限。
  • 角色指的是被认证用户的一组权限。

创建一个基本的Spring应用程序

在开始配置Spring安全框架之前,让我们先创建一个基本的Spring Web应用程序。为此,我们可以使用Spring Initializr并生成一个模板项目。对于一个简单的Web应用来说,只需要一个Spring Web框架的依赖关系就足够了。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

一旦我们创建了这个项目,我们就可以在其中添加一个简单的REST控制器,如下所示。

@RestController @RequestMapping("hello")
public class HelloRestController {

    @GetMapping("user")
    public String helloUser() {
        return "Hello User";
    }

    @GetMapping("admin")
    public String helloAdmin() {
        return "Hello Admin";
    }

}

在此之后,如果我们构建并运行该项目,我们可以在Web浏览器中访问以下URL。

  • http://localhost:8080/hello/user will return the string .Hello User
  • http://localhost:8080/hello/admin 将返回字符串 。Hello Admin

现在,我们可以将Spring安全框架添加到我们的项目中,我们可以通过在pom.xml 文件中添加以下依赖关系来实现。

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

在我们提供相应的配置之前,添加其他Spring框架依赖项通常不会对应用程序产生直接影响,但Spring Security不同,它确实会产生直接影响,而这通常会让新用户感到困惑。添加后,如果我们重建并运行项目,然后试图访问上述URL之一,而不是查看结果,我们会被重定向到http://localhost:8080/login 。这是默认行为,因为Spring Security框架要求所有URL都要进行开箱验证。

为了通过认证,我们可以使用默认的用户名user ,并在控制台找到一个自动生成的密码。

Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce

请记住,每次我们重新运行应用程序时,密码都会改变。如果我们想改变这种行为,使密码静态化,我们可以在我们的application.properties 文件中添加以下配置。

spring.security.user.password=Test12345_

现在,如果我们在登录表格中输入凭证,我们将被重定向到我们的URL,我们将看到正确的结果。请注意,开箱即用的认证过程是基于会话的,如果我们想注销,我们可以访问以下URL。http://localhost:8080/logout

这种开箱即用的行为对于经典的MVC网络应用来说可能是有用的,因为我们有基于会话的认证,但对于单页应用来说,它通常是没有用的,因为在大多数用例中,我们有客户端渲染和基于JWT的无状态认证。在这种情况下,我们将不得不大量定制Spring Security框架,我们将在文章的剩余部分进行定制。

作为一个例子,我们将实现一个经典的书店网络应用,并创建一个后端,提供CRUD API来创建作者和书籍,以及用户管理和认证的API。

Spring安全架构概述

在我们开始定制配置之前,首先让我们讨论一下Spring Security认证在幕后是如何工作的。

下图介绍了流程,显示了认证请求是如何被处理的。

Spring Security架构

Spring Security Architecture

现在,让我们把这张图分解成各个组件,并分别讨论它们。

Spring Security过滤器链

当你把Spring Security框架添加到你的应用程序中时,它会自动注册一个过滤器链,拦截所有传入的请求。这个链由各种过滤器组成,每个过滤器都处理一个特定的用例。

比如说。

  • 根据配置,检查请求的URL是否可以公开访问。
  • 在基于会话的认证的情况下,检查用户是否已经在当前会话中得到认证。
  • 检查用户是否被授权执行请求的动作,等等。

我想提及的一个重要细节是,Spring Security的过滤器是以最低的顺序注册的,是第一个被调用的过滤器。对于某些用例,如果你想把你的自定义过滤器放在它们前面,你需要在它们的顺序上添加填充。这可以通过以下配置来完成。

spring.security.filter.order=10

一旦我们把这个配置添加到我们的application.properties 文件中,我们将在Spring Security过滤器的前面有10个自定义过滤器的空间。

AuthenticationManager

你可以把AuthenticationManager 认为是一个协调器,你可以在这里注册多个提供者,根据请求类型,它将把认证请求交付给正确的提供者。

认证提供者(AuthenticationProvider)

AuthenticationProvider 处理特定类型的认证。它的接口只暴露了两个功能。

  • authenticate 对请求进行认证。
  • supports 检查这个提供者是否支持指定的认证类型。

我们在示例项目中使用的接口的一个重要实现是DaoAuthenticationProvider ,它从UserDetailsService ,检索用户详细信息。

UserDetailsService

UserDetailsService 被描述为一个核心接口,在Spring文档中加载用户特定的数据。

在大多数用例中,认证提供者根据数据库中的凭证提取用户身份信息,然后进行验证。由于这种用例非常普遍,Spring开发者决定将其提取为一个单独的接口,该接口公开了一个函数。

  • loadUserByUsername 接受用户名作为参数并返回用户身份对象。

使用JWT与Spring Security进行认证

在讨论了Spring Security框架的内部结构后,让我们来配置它,以便用JWT令牌进行无状态认证。

为了定制Spring Security,我们需要在classpath中添加一个配置类,注释为@EnableWebSecurity 。此外,为了简化定制过程,该框架还公开了一个WebSecurityConfigurerAdapter 类。我们将扩展这个适配器并覆盖它的两个功能,以便:

  1. 用正确的提供者配置认证管理器
  2. 配置网络安全(公共URLs、私人URLs、授权等)。
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO configure authentication manager
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO configure web security
    }

}

在我们的示例应用程序中,我们将用户身份存储在MongoDB数据库中,在users 集合。这些身份由User 实体映射,其CRUD操作由UserRepo Spring Data资源库定义。

现在,当我们接受认证请求时,我们需要使用所提供的凭证从数据库中检索出正确的身份,然后进行验证。为此,我们需要实现UserDetailsService 接口,它的定义如下

public interface UserDetailsService {

    UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException;

}

在这里,我们可以看到需要返回实现UserDetails 接口的对象,而我们的User 实体实现了该接口(关于实现细节,请看样本项目的资源库)。考虑到它只暴露了单一功能的原型,我们可以把它当作一个功能接口,并以lambda表达式的方式提供实现。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserRepo userRepo;

    public SecurityConfig(UserRepo userRepo) {
        this.userRepo = userRepo;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(username -> userRepo
            .findByUsername(username)
            .orElseThrow(
                () -> new UsernameNotFoundException(
                    format("User: %s, not found", username)
                )
            ));
    }

    // Details omitted for brevity

}

在这里,auth.userDetailsService 函数调用将使用我们对UserDetailsService 接口的实现来启动DaoAuthenticationProvider 实例,并将其注册到认证管理器中。

与认证提供者一起,我们需要配置一个具有正确密码编码模式的认证管理器,该模式将被用于凭证验证。为此,我们需要将PasswordEncoder 接口的首选实现作为一个bean公开。

在我们的示例项目中,我们将使用bcrypt密码洗牌算法。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserRepo userRepo;

    public SecurityConfig(UserRepo userRepo) {
        this.userRepo = userRepo;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(username -> userRepo
            .findByUsername(username)
            .orElseThrow(
                () -> new UsernameNotFoundException(
                    format("User: %s, not found", username)
                )
            ));
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // Details omitted for brevity

}

在配置了认证管理器后,我们现在需要配置Web安全。我们正在实现一个REST API,需要用JWT令牌进行无状态认证;因此,我们需要设置以下选项

  • 启用CORS并禁用CSRF
  • 设置会话管理为无状态。
  • 设置未授权请求异常处理程序。
  • 设置端点的权限。
  • 添加JWT令牌过滤器。

这个配置的实现如下。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserRepo userRepo;
    private final JwtTokenFilter jwtTokenFilter;

    public SecurityConfig(UserRepo userRepo,
                          JwtTokenFilter jwtTokenFilter) {
        this.userRepo = userRepo;
        this.jwtTokenFilter = jwtTokenFilter;
    }

    // Details omitted for brevity

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Enable CORS and disable CSRF
        http = http.cors().and().csrf().disable();

        // Set session management to stateless
        http = http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and();

        // Set unauthorized requests exception handler
        http = http
            .exceptionHandling()
            .authenticationEntryPoint(
                (request, response, ex) -> {
                    response.sendError(
                        HttpServletResponse.SC_UNAUTHORIZED,
                        ex.getMessage()
                    );
                }
            )
            .and();

        // Set permissions on endpoints
        http.authorizeRequests()
            // Our public endpoints
            .antMatchers("/api/public/**").permitAll()
            .antMatchers(HttpMethod.GET, "/api/author/**").permitAll()
            .antMatchers(HttpMethod.POST, "/api/author/search").permitAll()
            .antMatchers(HttpMethod.GET, "/api/book/**").permitAll()
            .antMatchers(HttpMethod.POST, "/api/book/search").permitAll()
            // Our private endpoints
            .anyRequest().authenticated();

        // Add JWT token filter
        http.addFilterBefore(
            jwtTokenFilter,
            UsernamePasswordAuthenticationFilter.class
        );
    }

    // Used by spring security if CORS is enabled.
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

}

请注意,我们在Spring Security内部UsernamePasswordAuthenticationFilter 之前添加了JwtTokenFilter 。我们这样做是因为我们此时需要访问用户身份来执行认证/授权,其提取发生在基于提供的JWT令牌的JWT令牌过滤器内。这一点的实现如下。

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    private final JwtTokenUtil jwtTokenUtil;
    private final UserRepo userRepo;

    public JwtTokenFilter(JwtTokenUtil jwtTokenUtil,
                          UserRepo userRepo) {
        this.jwtTokenUtil = jwtTokenUtil;
        this.userRepo = userRepo;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain)
            throws ServletException, IOException {
        // Get authorization header and validate
        final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (isEmpty(header) || !header.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        // Get jwt token and validate
        final String token = header.split(" ")[1].trim();
        if (!jwtTokenUtil.validate(token)) {
            chain.doFilter(request, response);
            return;
        }

        // Get user identity and set it on the spring security context
        UserDetails userDetails = userRepo
            .findByUsername(jwtTokenUtil.getUsername(token))
            .orElse(null);

        UsernamePasswordAuthenticationToken
            authentication = new UsernamePasswordAuthenticationToken(
                userDetails, null,
                userDetails == null ?
                    List.of() : userDetails.getAuthorities()
            );

        authentication.setDetails(
            new WebAuthenticationDetailsSource().buildDetails(request)
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }

}

在实现我们的登录API函数之前,我们还需要处理一个步骤--我们需要访问认证管理器。默认情况下,它是不能公开访问的,我们需要在我们的配置类中明确地把它作为一个Bean公开。

这可以按以下方式完成。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // Details omitted for brevity

    @Override @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

现在,我们已经准备好实现我们的登录API函数。

@Api(tags = "Authentication")
@RestController @RequestMapping(path = "api/public")
public class AuthApi {

    private final AuthenticationManager authenticationManager;
    private final JwtTokenUtil jwtTokenUtil;
    private final UserViewMapper userViewMapper;

    public AuthApi(AuthenticationManager authenticationManager,
                   JwtTokenUtil jwtTokenUtil,
                   UserViewMapper userViewMapper) {
        this.authenticationManager = authenticationManager;
        this.jwtTokenUtil = jwtTokenUtil;
        this.userViewMapper = userViewMapper;
    }

    @PostMapping("login")
    public ResponseEntity<UserView> login(@RequestBody @Valid AuthRequest request) {
        try {
            Authentication authenticate = authenticationManager
                .authenticate(
                    new UsernamePasswordAuthenticationToken(
                        request.getUsername(), request.getPassword()
                    )
                );

            User user = (User) authenticate.getPrincipal();

            return ResponseEntity.ok()
                .header(
                    HttpHeaders.AUTHORIZATION,
                    jwtTokenUtil.generateAccessToken(user)
                )
                .body(userViewMapper.toUserView(user));
        } catch (BadCredentialsException ex) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
    }

}

在这里,我们使用认证管理器来验证所提供的凭证,如果成功的话,我们会生成JWT令牌,并将其作为响应头与响应体中的用户身份信息一起返回。

使用Spring Security进行授权

在上一节中,我们设置了一个认证过程并配置了公共/私人URL。这对于简单的应用来说可能已经足够了,但对于大多数真实世界的用例来说,我们总是需要为我们的用户制定基于角色的访问策略。在本章中,我们将解决这个问题,并使用Spring Security框架建立一个基于角色的授权模式。

在我们的示例应用程序中,我们定义了以下三个角色。

  • USER_ADMIN 允许我们管理应用程序的用户。
  • AUTHOR_ADMIN 允许我们管理作者。
  • BOOK_ADMIN 允许我们管理书籍。

现在,我们需要将它们应用到相应的URL上。

  • api/public 是公开访问的。
  • api/admin/user 可以访问具有USER_ADMIN 角色的用户。
  • api/author 可以访问具有AUTHOR_ADMIN 角色的用户。
  • api/book 可以访问具有BOOK_ADMIN 角色的用户。

Spring Security框架为我们提供了两个选项来设置授权模式。

  • 基于URL的配置
  • 基于注解的配置

首先,我们来看看基于URL的配置是如何工作的。它可以应用于Web安全配置,如下所示。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // Details omitted for brevity

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // Enable CORS and disable CSRF
        http = http.cors().and().csrf().disable();

        // Set session management to stateless
        http = http
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and();

        // Set unauthorized requests exception handler
        http = http
            .exceptionHandling()
            .authenticationEntryPoint(
                (request, response, ex) -> {
                    response.sendError(
                        HttpServletResponse.SC_UNAUTHORIZED,
                        ex.getMessage()
                    );
                }
            )
            .and();

        // Set permissions on endpoints
        http.authorizeRequests()
            // Our public endpoints
            .antMatchers("/api/public/**").permitAll()
            .antMatchers(HttpMethod.GET, "/api/author/**").permitAll()
            .antMatchers(HttpMethod.POST, "/api/author/search").permitAll()
            .antMatchers(HttpMethod.GET, "/api/book/**").permitAll()
            .antMatchers(HttpMethod.POST, "/api/book/search").permitAll()
            // Our private endpoints
            .antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN)
            .antMatchers("/api/author/**").hasRole(Role.AUTHOR_ADMIN)
            .antMatchers("/api/book/**").hasRole(Role.BOOK_ADMIN)
            .anyRequest().authenticated();

        // Add JWT token filter
        http.addFilterBefore(
            jwtTokenFilter,
            UsernamePasswordAuthenticationFilter.class
        );
    }

    // Details omitted for brevity

}

如你所见,这种方法简单明了,但它有一个缺点。我们应用程序中的授权模式可能很复杂,如果我们在一个地方定义所有的规则,它就会变得非常大,非常复杂,而且难以阅读。正因为如此,我通常喜欢使用基于注解的配置。

Spring Security框架为Web安全定义了以下注解:

  • @PreAuthorize 支持Spring表达式语言,用于执行方法之前提供基于表达式的访问控制。
  • @PostAuthorize 支持Spring表达式语言,用于执行方法提供基于表达式的访问控制(提供访问方法结果的能力)。
  • @PreFilter 支持Spring表达式语言,用于执行方法根据我们定义的自定义安全规则过滤集合或数组。
  • @PostFilter 支持Spring表达式语言,用于在执行方法根据我们定义的自定义安全规则过滤返回的集合或数组(提供访问方法结果的能力)。
  • @Secured 不支持Spring表达式语言,用于指定一个方法的角色列表。
  • @RolesAllowed 不支持Spring表达式语言,是JSR 250的等效注解,即 注解。@Secured

这些注解在默认情况下是禁用的,在我们的应用程序中可以按以下方式启用。

@EnableWebSecurity
@EnableGlobalMethodSecurity(
    securedEnabled = true,
    jsr250Enabled = true,
    prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // Details omitted for brevity

}

启用它们之后,我们可以像这样在我们的API端点上执行基于角色的访问策略。

@Api(tags = "UserAdmin")
@RestController @RequestMapping(path = "api/admin/user")
@RolesAllowed(Role.USER_ADMIN)
public class UserAdminApi {

	// Details omitted for brevity

}

@Api(tags = "Author")
@RestController @RequestMapping(path = "api/author")
public class AuthorApi {

	// Details omitted for brevity

	@RolesAllowed(Role.AUTHOR_ADMIN)
	@PostMapping
	public void create() { }

	@RolesAllowed(Role.AUTHOR_ADMIN)
	@PutMapping("{id}")
	public void edit() { }

	@RolesAllowed(Role.AUTHOR_ADMIN)
	@DeleteMapping("{id}")
	public void delete() { }

	@GetMapping("{id}")
	public void get() { }

	@GetMapping("{id}/book")
	public void getBooks() { }

	@PostMapping("search")
	public void search() { }

}

@Api(tags = "Book")
@RestController @RequestMapping(path = "api/book")
public class BookApi {

	// Details omitted for brevity

	@RolesAllowed(Role.BOOK_ADMIN)
	@PostMapping
	public BookView create() { }

	@RolesAllowed(Role.BOOK_ADMIN)
	@PutMapping("{id}")
	public void edit() { }

	@RolesAllowed(Role.BOOK_ADMIN)
	@DeleteMapping("{id}")
	public void delete() { }

	@GetMapping("{id}")
	public void get() { }

	@GetMapping("{id}/author")
	public void getAuthors() { }

	@PostMapping("search")
	public void search() { }

}

请注意,安全注释可以在类和方法层面上提供。

所演示的例子很简单,并不代表现实世界的场景,但Spring Security提供了丰富的注解,如果你选择使用它们,你可以处理复杂的授权模式。

角色名称默认前缀

在这个单独的小节中,我想强调一个更微妙的细节,它让很多新用户感到困惑。

Spring Security框架区分了两个术语。

  • Authority 代表一个单独的权限。
  • Role 代表一组权限。

两者都可以用一个叫做GrantedAuthority 的接口来表示,之后用Spring Expression Language在Spring Security注解里面进行检查,如下所示。

  • Authority:@PreAuthorize("hasAuthority('EDIT_BOOK')")
  • Role:@PreAuthorize("hasRole('BOOK_ADMIN')")

为了使这两个术语之间的区别更加明确,Spring Security框架默认为角色名称添加一个ROLE_ 前缀。因此,它不会检查一个名为BOOK_ADMIN 的角色,而是检查ROLE_BOOK_ADMIN

就我个人而言,我发现这种行为令人困惑,所以我更愿意在我的应用程序中禁用它。它可以在Spring Security的配置中被禁用,如下所示。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // Details omitted for brevity

    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
    }

}

使用Spring Security进行测试

在使用Spring Security框架时,为了用单元测试或集成测试来测试我们的端点,我们需要将spring-security-testspring-boot-starter-test 一起添加。我们的pom.xml 构建文件将看起来像这样。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

这个依赖关系让我们可以访问一些注释,这些注释可以用来给我们的测试函数添加安全上下文。

这些注解是

  • @WithMockUser 可以添加到测试方法中以模拟与模拟用户一起运行。
  • @WithUserDetails 可以添加到测试方法中,以模拟运行从 返回的 。UserDetailsService UserDetails
  • @WithAnonymousUser 可以添加到测试方法中,以模拟与匿名用户一起运行。当用户想以一个特定用户的身份运行大部分测试,并覆盖一些方法为匿名时,这很有用。
  • @WithSecurityContext 决定使用什么 ,上面描述的所有三个注释都是基于它的。如果我们有一个特定的用例,我们可以创建自己的注解,使用 来创建任何我们想要的 。它的讨论超出了我们文章的范围,请参考Spring Security文档以了解更多细节。SecurityContext @WithSecurityContext SecurityContext

使用特定用户运行测试的最简单方法是使用@WithMockUser 注解。我们可以用它创建一个模拟用户,并按如下方式运行测试。

@Test @WithMockUser(username="customUsername@example.io", roles={"USER_ADMIN"})
public void test() {
	// Details omitted for brevity
}

不过这种方法有几个缺点。首先,模拟用户并不存在,如果你运行集成测试,随后从数据库中查询用户信息,测试将失败。第二,模拟用户是org.springframework.security.core.userdetails.User 类的实例,这是Spring框架对UserDetails 接口的内部实现,如果我们有自己的实现,这可能会在以后的测试执行中造成冲突。

如果前面的缺点是我们应用程序的障碍,那么@WithUserDetails 注释就是我们要做的。当我们有自定义的UserDetailsUserDetailsService 实现时,就可以使用它。它假定用户存在,所以我们必须在数据库中创建实际的行,或者在运行测试之前提供UserDetailsService 模拟实例。

这就是我们如何使用这个注解。

@Test @WithUserDetails("customUsername@example.io")
public void test() {
	// Details omitted for brevity
}

在我们的示例项目的集成测试中,这是一个首选注解,因为我们有上述接口的自定义实现。

使用@WithAnonymousUser ,允许以匿名用户的身份运行。当你希望用一个特定的用户来运行大多数测试,但以匿名用户的身份运行少数测试时,这特别方便。例如,下面将用模拟用户运行test1test2测试案例,用匿名用户运行test3

@SpringBootTest
@AutoConfigureMockMvc
@WithMockUser
public class WithUserClassLevelAuthenticationTests {

    @Test
    public void test1() {
        // Details omitted for brevity
    }

    @Test
    public void test2() {
        // Details omitted for brevity
    }

    @Test @WithAnonymousUser
    public void test3() throws Exception {
        // Details omitted for brevity
    }
}

总结

最后,我想说的是,Spring Security框架可能不会赢得任何选美比赛,它肯定有一个陡峭的学习曲线。我遇到过很多情况,由于它最初配置的复杂性,它被一些自制的解决方案所取代。但是,一旦开发者了解了它的内部结构并设法设置了初始配置,它就会变得相对简单易用。

在这篇文章中,我试图展示配置的所有微妙细节,我希望你会发现这些例子很有用。关于完整的代码例子,请参考我的Spring Security项目样本的Git仓库。

了解基础知识

什么是Spring Security?

Spring Security是一个强大且高度可定制的认证和授权框架。它是保护基于Spring的应用程序安全的事实标准。

如何使用REST API的Spring Security?

开箱即用的Spring Security带有基于会话的认证,这对传统的MVC Web应用程序很有用,但我们可以配置它以支持基于JWT的无状态认证,用于REST APIs。

Spring Security的安全性如何?

Spring Security是相当安全的。它很容易与基于Spring的应用程序集成,支持多种类型的认证,并且能够进行声明式安全编程。

为什么使用Spring Security?

因为它能与其他Spring生态系统无缝集成,而且许多开发者更愿意重复使用现有的解决方案,而不是重新发明轮子。

什么是JWT?

JSON Web Token(JWT)是一种编码信息的标准,可作为JSON对象安全地传输。

JWT如何与Spring Security一起工作?

我们为认证提供了一个公共的POST API,在传递正确的凭证后,它将生成一个JWT。如果用户试图访问受保护的API,只有当请求有一个有效的JWT时,它才会允许访问。验证将发生在Spring Security过滤器链中注册的过滤器中。