使用 JWT 身份验证保护你的 Spring Boot 应用

本文深入探讨如何使用 JSON Web Tokens (JWT) 进行身份验证来保护 Spring Boot 应用程序。我们将探索 Spring Security、JWT 基础知识,然后实现具有用户注册、登录和访问控制的安全 API。我们的数据将使用 Spring Data JPA 保存在 PostgreSQL 数据库中。

为什么选择 Spring Security 
Spring Security 是用于保护 Spring 应用程序的行业标准框架。它提供全面的身份验证、授权和访问控制功能。通过利用 Spring Security,我们可以有效地管理用户对我们的 API 端点的访问。

JWT 身份验证
JWT 是一种基于令牌的身份验证机制。与传统的基于会话的方法不同,JWT 将用户信息存储在紧凑、自包含的令牌中。此令牌随每次请求一起发送,允许服务器验证用户的身份,而无需依赖服务器端会话。

以下是 JWT 优势的细分:

  • 无状态:无需服务器上的会话管理。
  • 安全:采用数字签名防止篡改。
  • 灵活:可以配置各种声明来存储用户信息。

持久
在本文中,我们将使用 PostgreSQL 作为数据库。您可以在任何数据库管理系统中维护数据库。为了获得方便的部署选项,请考虑使用基于云的解决方案,例如 Rapidapp,它提供托管的 PostgreSQL 数据库,简化设置和维护。


确保您已经使用您最喜欢的依赖项管理工具(例如 maven、gradle)安装了以下依赖项。

pom.xml

 <dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.7.3</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.32</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.12.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.12.5</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

启用 Spring Web 
为了启用 Spring Web Security,您需要在SecurityConfig.java文件中进行配置,如下所示。


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private static final String[] AUTH_WHITELIST = {
        "/api/v1/auth/login",
        "/api/v1/auth/register"
    };

    private final JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeRequests(authorizeRequests ->
                authorizeRequests
                        .requestMatchers(AUTH_WHITELIST).permitAll()
                    .anyRequest().authenticated()
            )
            .sessionManagement(sessionManagement ->
                sessionManagement
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

  • 第 2 行:添加@EnableWebSecurity到SecurityConfig类以保护 API 端点。
  • 第 6 行:允许来自/api/v1/auth/login和/api/v1/auth/register端点的请求,无需进行身份验证。
  • 第 16 行:禁用 CSRF 保护,因为 JWT 身份验证是无状态的。
  • 第 24 行:设置会话创建策略,以STATELESS确保不维护会话。
  • 第 25 行:将 添加JwtAuthFilter到 之前的安全过滤器链中UsernamePasswordAuthenticationFilter。我们JwtAuthFilter很快会解释类。

JWT 身份
为了启用 JWT 身份验证,您需要在JwtAuthFilter.java文件中进行配置,如下所示。

JwtAuthFilter.java

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (request.getServletPath().contains("/api/v1/auth")) {
            filterChain.doFilter(request, response);
            return;
        }

        final String authorizationHeader = request.getHeader("Authorization");
        final String jwtToken;
        final String email;

        if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        jwtToken = authorizationHeader.substring(7);
        email = jwtService.extractEmail(jwtToken);

        if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(email);
            if (jwtService.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

  • 第 10 行:不要对/api/v1/auth端点应用 JWT 身份验证过滤器。
  • 第 24 行:从 header 中提取 JWT token Authorization。它的格式是Bearer <token>,所以是substring(7)。
  • 第 25 行:从 JWT 令牌中提取电子邮件JwtService,我们将在下一节中对其进行介绍。
  • 第 28-32 行:使用 验证 JWT 令牌JwtService,使用UserDetailsfrom加载用户详细信息UserDetailsService并将身份验证存储在 中SecurityContextHolder。

实现
此类包含所有 JWT 相关功能,如下所示。

JwtService.java

@Service
public class JwtService {

    @Value("${jwt.secret}")
    private String secret;

    public String extractEmail(String jwtToken) {
        return extractClaim(jwtToken, Claims::getSubject);
    }

    public <T> T extractClaim(String jwtToken, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(jwtToken);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String jwtToken) {
        return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(jwtToken).getPayload();
    }

    private SecretKey getSigningKey() {
        byte [] bytes = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(bytes);
    }

    public boolean validateToken(String jwtToken, UserDetails userDetails) {
        final String email = extractEmail(jwtToken);
        return email.equals(userDetails.getUsername()) && !isTokenExpired(jwtToken);
    }

    private boolean isTokenExpired(String jwtToken) {
        return extractExpiration(jwtToken).before(new Date());
    }

    private Date extractExpiration(String jwtToken) {
        return extractClaim(jwtToken, Claims::getExpiration);
    }

    public String generateToken(User u) {
        return createToken(u.getEmail());
    }

    private String createToken(String email) {
        return Jwts.builder()
                .subject(email)
                .issuedAt(new Date(System.currentTimeMillis()))
                .expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
                .signWith(getSigningKey())
                .compact();
    }
}

  • 第 5 行:这是用于签署 JWT 令牌的密钥。这应该小心保护,我们不能分享或公开它。所有其他功能都是不言自明的。

UserDetailsS​​ervice 旨在展示 Spring Boot 安全身份验证如何从数据库加载用户详细信息,如下所示。


@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {
    private final UserRepository userRepository;


    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
                .map(user -> User.builder().username(user.getEmail())
                        .password(user.getPassword())
                        .build())
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));
    }
}


到目前为止,我们只关注 JWT 身份验证。但是,下一节中我们将如何生成 JWT 令牌?它的用例是什么?

注册
在生成 JWT 令牌来验证用户身份之前,我们需要注册用户。我们将使用它AuthController来注册用户。

AuthController.java

@RestController
@RequestMapping(path = "api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;


    @PostMapping(path = "/register")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void register(@RequestBody RegisterRequest registerRequest) {
        authService.register(registerRequest);
    }

    @PostMapping(path = "/login")
    public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
        return ResponseEntity.ok(authService.login(loginRequest));
    }
}


在上面的控制器中,我们用来AuthService注册和登录用户。AuthService用于UserRepository与数据库交互以进行与用户相关的操作。

AuthService.java

@Service
@RequiredArgsConstructor
public class AuthService {

    private final UserRepository userRepository;
    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public void register(RegisterRequest registerRequest) {
        User u = User.builder()
                .email(registerRequest.getEmail())
                .password(bCryptPasswordEncoder.encode(registerRequest.getPassword()))
                .firstName(registerRequest.getFirstName())
                .lastName(registerRequest.getLastName())
                .build();
        userRepository.save(u);
    }

    public String login(LoginRequest loginRequest) {
        authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
        User u = userRepository.findByEmail(loginRequest.getEmail()).orElseThrow(() -> new EntityNotFoundException("User not found"));
        return jwtService.generateToken(u);

    }
}

  • 第 10 行:使用请求负载中提供的详细信息注册用户。用于bCryptPasswordEncoder在将密码存储到数据库之前对其进行哈希处理。
  • 第 21 行:authenticationManager由于它知道如何验证用户名和密码,因此登录操作已完成。

授限访问 UserController
您可以看到用户对象的示例端点实现。


@RestController
@RequestMapping(path = "api/v1")
public class UserController {
    private final UserRepository userRepository;
    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("/users")
    public List<User> getUsers() {
        return userRepository.findAll();
    }
}

假设你使用电子邮件admin密码注册了一个新用户ssshhhh。然后为了生成 JWT 令牌,你可以使用以下 curl 请求。

curl -X POST -H "Content-Type: application/json" \
  -d '{"email": "admin", "password": "ssshhhh"}' http://localhost:8080/api/v1/auth/login


它将返回一个 JWT 令牌,您可以使用它来验证用户身份。将其存储在某处。

现在,为了访问受限用户端点,您可以使用以下 curl 请求。

curl -X GET -H "Authorization: Bearer <token>" http://localhost:8080/api/v1/users

总结
本动手教程为您提供了在 Spring Boot 应用程序中实现 JWT 身份验证的知识。我们探索了用户注册、登录和访问控制,利用 Spring Security 和 JPA 实现数据持久性。通过遵循这些步骤并根据您的特定需求自定义代码示例,您可以保护 API 端点并确保授权用户访问。请记住优先考虑安全最佳实践。以下是一些需要考虑的其他要点:

  • 密钥管理:将您的 JWT 密钥安全地存储在环境变量或专用密钥管理服务中。切勿将其暴露在您的代码库中。
  • Token过期:为JWT token设置合理的过期时间,防止因token被泄露导致未经授权的访问。
  • 错误处理:针对无效或过期的令牌实施适当的错误处理机制,以便向用户提供信息反馈。
  • 高级功能:探索高级 JWT 功能,例如用于延长会话寿命的刷新令牌和用于细粒度授权的基于角色的访问控制 (RBAC)。有了 JWT 身份验证,您的 Spring Boot 应用程序就有望成为一个安全且强大的平台。您可以放心部署,因为您知道用户访问得到了妥善控制。

源码:GitHub.