使用 Spring Security JWT 令牌签名实现 REST API 安全性

一种流行的方法是使用 JSON Web 令牌 (JWT)。 Spring Security 有助于在 Spring 应用程序中进行基于 JWT 的身份验证和授权。在本文中,我们将了解如何创建用于签署 JWT 令牌的 Spring Security 密钥,并在 Spring Boot 应用程序中使用它来保护 REST API。

添加 JSON Web Token 依赖项
在pom.xml项目文件中(如果使用 Maven),添加以下用于 JWT 令牌处理的依赖项:

<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>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.5</version>
    <scope>runtime</scope>
</dependency>

创建 JWT 实用程序类
接下来,创建一个用于处理 JWT 操作的实用程序类。该类将负责生成 JWT 令牌并验证它们。下面是 JWT 实用程序类的简单实现:

@Component
public class JwtUtil {
 
    @Value("${jcg.jwt.secret}")
    private String jwtSecret;
 
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put(
"Authorities", userDetails.getAuthorities());
 
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 86400))
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)
                .compact();
    }
 
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
 
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
 
    private Claims extractAllClaims(String token) {
         
        return Jwts.parser()
                .setSigningKey(getSignInKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
 
    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
 
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
 
    private Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
     
    private SecretKey getSignInKey() {
        byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

上面的代码块代表一个名为的类JwtUtil,负责处理我们应用程序中的 JWT(JSON Web Token)操作。以下是其功能的细分:

  • 该类使用 @Value 注解从 application.properties 文件中注入 JWT 密钥。
  • Token Generation令牌生成:generateToken 方法根据提供的 UserDetails 创建 JWT 令牌。该方法会设置主题(用户名)、发布日期、过期日期,并使用 HMAC SHA-256 算法和秘钥对令牌进行签名。
  • extractUsername 方法:该方法将 JWT 令牌作为输入,并返回从令牌的主题请求中提取的用户名。该方法将权利要求提取过程委托给 extractClaim 方法,传递令牌和函数引用(Claims::getSubject)以提取主题权利要求。
  • extractClaim 方法:该通用方法接收一个 JWT 标记和一个函数,该函数可从标记的 Claims 对象中解析特定的权利要求。它首先通过调用 extractAllClaims 方法从令牌中提取所有索赔。然后,它将提供的 claimsResolver 函数应用到 Claims 对象,以提取所需的权利要求。
  • extractAllClaims 方法:该方法负责解析 JWT 令牌,使用提供的签名密钥验证其签名,并从令牌正文中提取所有声明。它使用 Jwts.parser() 方法创建解析器实例,使用 setSigningKey(getSignInKey()) 设置签名密钥,然后使用 parseClaimsJws(token) 解析令牌。
  • Secret Key Retrieval:getSignInKey 方法会检索用于签署和验证 JWT 标记的秘钥。该方法会解码从 application.properties 文件中获取的 base64 编码秘钥,并将其转换为 SecretKey 对象。


生成并设置JWT密钥
生成强密钥对于确保 JWT 令牌的安全至关重要。这需要生成由 256 位组成的 HMAC 哈希字符串,并将其配置为文件中的 JWT 密钥application.properties。位于devglan.com/online-tools的在线工具生成器可以生成 256 位的 HMAC 哈希字符串。

在 application.properties 中设置 JWT Secret
密钥生成后,需要在application.propertiesSpring Boot 应用程序的文件中设置:
jcg.jwt.secret=YOUR_GENERATED_SECRET_KEY

实现认证令牌过滤器
接下来,让我们实现一个身份验证令牌过滤器,这对于使用 JWT 令牌保护 REST 端点至关重要。下面是身份验证令牌过滤器的示例:

@Component
public class JwtRequestFilter extends OncePerRequestFilter {
 
    @Autowired
    private JwtUtil jwtUtil;
 
    @Autowired
    private UserDetailsService userDetailsService;
 
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
 
        final String authorizationHeader = request.getHeader("Authorization");
 
        String username = null;
        String jwt = null;
 
        if (authorizationHeader != null && authorizationHeader.startsWith(
"Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }
 
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
 
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
 
            if (jwtUtil.validateToken(jwt, userDetails)) {
 
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken
                        .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                 
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        filterChain.doFilter(request, response);
    }
}

该过滤器拦截传入的请求,从请求标头中提取 JWT 令牌,验证它们,如果令牌有效,则在 Spring Security 上下文中设置身份验证。

配置Spring Security
接下来,配置 Spring Security 以使用 JWT 进行身份验证。创建一个SecurityConfig类并配置如下:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
 
    private static final String ADMIN = "ADMIN";
    private static final String USER =
"USER";
 
    @Bean
    public JwtRequestFilter jwtRequestFilter() {
        return new JwtRequestFilter();
    }
 
    @Bean
    public DaoAuthenticationProvider authenticationProvider() throws Exception {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
 
        return authProvider;
    }
 
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
                .cors(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(req -> req
                .requestMatchers(
"/admin/**").hasRole(ADMIN)
                .requestMatchers(
"/user/**").hasAnyRole(USER, ADMIN)
                .requestMatchers(
"/authenticate")
                .permitAll()
                .anyRequest()
                .authenticated())
                .sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
                .authenticationProvider(authenticationProvider())
                .addFilterBefore(jwtRequestFilter(), UsernamePasswordAuthenticationFilter.class);
 
        return http.build();
    }
 
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public UserDetailsService userDetailsService() throws Exception {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User
                .withUsername(
"thomas")
                .password(encoder().encode(
"paine"))
                .roles(ADMIN).build());
        manager.createUser(User
                .withUsername(
"bill")
                .password(encoder().encode(
"withers"))
                .roles(USER).build());
        return manager;
    }
 
    @Bean
    public PasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }
 
    @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);
    }
}

jwtRequestFilter 方法为 JwtRequestFilter 类定义了一个 bean。该过滤器会拦截传入的请求、提取 JWT 标记并对其进行身份验证。

DaoAuthenticationProvider Bean:authenticationProvider 方法为 DaoAuthenticationProvider 类定义了一个 Bean。该提供程序根据所提供的用户详细信息服务和密码编码器对用户进行身份验证。

SecurityFilterChain Bean: BeansecurityFilterChain 方法根据请求匹配器和角色设置授权规则。
授权规则:.authorizeHttpRequests() 方法为不同类型的请求指定了授权规则:

  • 向 /admin/** 端点发出的请求需要 ADMIN 角色。
  • 向 /user/** 端点发出的请求需要 USER 或 ADMIN 角色。
  • 对 /authenticate 端点的请求无需身份验证即可允许(permit all)。
  • 所有其他请求(anyRequest())都需要身份验证。

会话管理:会话管理配置为无状态(sessionCreationPolicy(STATELESS)),这意味着不会创建服务器端会话或用于存储用户身份验证状态。

AuthenticationManager Bean:authenticationManager 方法为 AuthenticationManager 接口定义了一个 Bean。它从身份验证配置中检索身份验证管理器。

UserDetailsService Bean:userDetailsService 方法使用 InMemoryUserDetailsManager 定义内存用户存储。我们创建了两个不同角色的用户:一个是用户名为 "thomas "的 ADMIN 用户,另一个是用户名为 "bill "的 USER 用户。

自定义用户详细信息实现
Spring Security 依赖于接口来实现身份验证和授权。下面是实现身份验证和授权接口的类UserDetails的实现:UserUserDetails

public class User implements UserDetails {
 
    private int id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;
 
    public User() {
    }
 
    public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this.password = password;
        this.username = username;
        this.authorities = authorities;
    }
 
    public User(String username, Collection<String> authorities) {
        this.username = username;
        this.authorities = authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }
 
    // UserDetails interface methods
    @Override
    public String getUsername() {
        return username;
    }
 
    @Override
    public String getPassword() {
        return password;
    }
 
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
 
    @Override
    public boolean isEnabled() {
        return true;
    }
 
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
 
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
 
}

User 类实现了 UserDetails 接口,代表应用程序中的一个用户。它包括用户名、密码和权限字段。
UserDetails 接口是 Spring Security 的核心接口,用于用户身份验证和授权。它代表系统中的委托人(用户),并提供访问用户详细信息和授权的方法。

  • getAuthorities()方法返回授予用户的权限(角色)。
  • getPassword()和getUsername()方法分别返回用户的密码和用户名。
  • isAccountNonExpired() isAccountNonLocked()、isCredentialsNonExpired()和isEnabled()方法分别在用户账户未过期、未锁定、凭证未过期和用户已启用的情况下返回true。

与 Spring Boot 集成
最后,让我们将 JWT 工具和 Spring Security 配置与 Spring Boot 应用程序集成。下面是一个用于身份验证的 REST 控制器的基本示例:

@RestController
public class AuthController {
 
    @Autowired
    private AuthenticationManager authenticationManager;
 
    @Autowired
    private JwtUtil jwtUtil;
 
    @Autowired
    private UserDetailsService userDetailsService;
 
    @PostMapping("/authenticate")
    public ResponseEntity createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception {
        try {
            authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
            );
        } catch (BadCredentialsException e) {
            throw new Exception(
"Incorrect username or password", e);
        }
 
         
        final UserDetails userDetails = userDetailsService
                .loadUserByUsername(authRequest.getUsername());
 
        final String jwt = jwtUtil.generateToken(userDetails);
 
        return ResponseEntity.ok(new AuthResponse(jwt));
    }
     
    @GetMapping(
"/auth/details")
    public UserDetails getDetails(){
        var detail = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return detail;
    }
 
}

该代码块定义了一个 AuthController 类,负责处理应用程序中与身份验证相关的 HTTP 请求。
  • 该控制器类自动连接了身份验证和令牌生成所需的依赖项(AuthenticationManager、JwtUtil 和 UserDetailsService)。
  • createAuthenticationToken 方法:该方法处理对 /authenticate 端点的 POST 请求。它通过将提供的凭证传递给 AuthenticationManager 来尝试对用户进行身份验证。如果验证成功,它会使用 JwtUtil 生成一个 JWT 令牌,并在响应体中返回。
  • getDetails 方法:该方法处理对 /auth/details 端点的 GET 请求。它会从 SecurityContextHolder 中检索已验证用户的详细信息(如用户名、授权)。

保护 REST 端点

@RestController
public class SimpleController {
     
    @RolesAllowed("ADMIN")
    @GetMapping(
"/admin")
    public ResponseEntity<String> testAdmin() {
        return ResponseEntity.ok(
"This is the Admin role");
    }
 
    @RolesAllowed(
"USER")
    @GetMapping(
"/user")
    public ResponseEntity<String> testUser() {
        return ResponseEntity.ok(
"This is the User role");
    }
}

该 SimpleController 类定义了只有具有特定角色的用户才能访问的端点。它执行基于角色的访问控制(RBAC),确保只有具有相应角色的用户才能执行某些操作。

  • testAdmin 方法:该方法处理对 /admin 端点的 GET 请求。该方法使用 @RolesAllowed("ADMIN")进行注解,指定只有角色为 "ADMIN "的用户才能访问该端点。
  • testUser 方法:该方法处理对 /user 端点的 GET 请求。与 testAdmin 方法类似,该方法也使用 @RolesAllowed("USER")注释,表明只有角色为 "USER "的用户才能访问该端点。

为了验证应用程序的功能,我们可以利用 POSTMAN 向 http://localhost:8080/authenticate 发送请求并获取 JWT 令牌。