Skip to content

Spring Boot 4

Spring Security 7

Authentication

方案一:JWT + 自定义过滤器

1. 添加依赖

pom.xml(Maven)中添加jjwt等JWT库,用于生成和解析Token。

xml
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.13.0</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.13.0</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.13.0</version>
    <scope>runtime</scope>
</dependency>
  • JWT 工具类
java
package study.helloworld.springsecuritydemo;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.crypto.SecretKey;

/** JJWT 工具类 使用 HS256 算法,支持自定义 claims,自动处理过期验证。 */
public class JwtUtils {

  private static final SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build();
  private static final long DEFAULT_EXPIRE_SECONDS = 3600L; // 默认1小时

  /**
   * 生成 token(默认过期时间)
   *
   * @param subject 用户标识(如 userId)
   * @param claims 自定义 claims(可为 null)
   * @return JWT 字符串
   */
  public static String generateToken(String subject, Map<String, Object> claims) {
    return generateToken(subject, claims, DEFAULT_EXPIRE_SECONDS);
  }

  /**
   * 生成 token(自定义过期时间)
   *
   * @param subject 用户标识
   * @param claims 自定义 claims(可为 null)
   * @param expireSeconds 过期秒数
   * @return JWT 字符串
   */
  public static String generateToken(
      String subject, Map<String, Object> claims, long expireSeconds) {
    Instant now = Instant.now();
    Instant expiration = now.plusSeconds(expireSeconds);

    var builder =
        Jwts.builder()
            .subject(subject)
            .issuedAt(Date.from(now))
            .expiration(Date.from(expiration))
            .signWith(SECRET_KEY, Jwts.SIG.HS256);

    if (claims != null && !claims.isEmpty()) {
      builder.claims(claims);
    }

    return builder.compact();
  }

  /**
   * 解析 token 并获取 Claims
   *
   * @param token JWT 字符串
   * @return Claims
   */
  private static Claims parseToken(String token) {
    Claims claims = null;
    try {
      Jws<Claims> jws = Jwts.parser().verifyWith(SECRET_KEY).build().parseSignedClaims(token);
      claims = jws.getPayload();
    } catch (JwtException | IllegalArgumentException e) {
      e.printStackTrace();
    }
    return claims;
  }

  /**
   * 验证 token 是否有效(签名正确且未过期)
   *
   * @param token JWT 字符串
   * @return true 有效,false 无效
   */
  public static boolean validateToken(String token) {
    return parseToken(token) != null;
  }

  /**
   * 从 token 中提取 subject(会验证签名和过期时间)
   *
   * @param token JWT 字符串
   * @return subject
   */
  public static String getSubject(String token) {
    Claims claims = parseToken(token);
    return claims != null ? claims.getSubject() : null;
  }

  // =================== 示例用法 ===================
  public static void main(String[] args) {
    // 1. 生成 token
    Map<String, Object> customClaims = new ConcurrentHashMap<>();
    customClaims.put("role", "admin");
    customClaims.put("email", "[email protected]");

    String token = generateToken("user123", customClaims, 7200L); // 2小时过期
    System.out.println("生成的 Token: " + token);

    // 2. 验证 token
    boolean valid = validateToken(token);
    System.out.println("验证结果: " + valid);

    // 3. 解析 token
    System.out.println("Subject: " + getSubject(token));
  }
}
2. 编写JWT认证过滤器

创建JwtAuthenticationFilter(继承OncePerRequestFilter),它会在每个请求到达Controller前被调用,负责:

  • 从HTTP请求的Authorization: Bearer <token>头中提取Token。
  • 验证Token的签名和有效期。
  • 如果Token有效,解析出用户信息(如用户名、权限),创建一个UsernamePasswordAuthenticationToken并设置到SecurityContextHolder中,代表当前用户已通过认证。
java
package study.helloworld.springsecuritydemo;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

  @Autowired private UserDetailsService userDetailsService;

  @Override
  protected void doFilterInternal(
      HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {
    // 从请求头中获取令牌
    String authorizationHeader = request.getHeader("Authorization");
    if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
      String token = authorizationHeader.substring(7);
      // 校验并解析令牌
      if (JwtUtils.validateToken(token)) {
        String subject = JwtUtils.getSubject(token);
        UserDetails userDetails = userDetailsService.loadUserByUsername(subject);
        // 若有效,构建 Authentication 并存入 SecurityContext
        Authentication authentication =
            new UsernamePasswordAuthenticationToken(subject, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authentication);
      }
    }
    filterChain.doFilter(request, response);
  }
}
3. 配置示例(SecurityConfig)
java
package study.helloworld.springsecuritydemo;

import jakarta.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import tools.jackson.databind.ObjectMapper;

@EnableWebSecurity
@Configuration
public class SecurityConfig {

  @Bean
  SecurityFilterChain securityFilterChain(
      HttpSecurity http, JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
    http.csrf(AbstractHttpConfigurer::disable);
    http.formLogin(AbstractHttpConfigurer::disable);
    http.httpBasic(AbstractHttpConfigurer::disable);
    http.logout(AbstractHttpConfigurer::disable);
    http.sessionManagement(
        sessionManagement ->
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

    http.authorizeHttpRequests(
        authorize ->
            authorize.requestMatchers("/auth/**").permitAll().anyRequest().authenticated());

    http.exceptionHandling(
        exception ->
            exception
                .authenticationEntryPoint(
                    (request, response, authException) -> {
                      // 设置响应状态码为 401 Unauthorized
                      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                      // 设置响应内容类型为 JSON
                      response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                      response.setCharacterEncoding(StandardCharsets.UTF_8.name());

                      // 构建 JSON 响应体(可以使用任意 JSON 库,这里以手动拼接为例)
                      Map<String, Object> body = new HashMap<>();
                      body.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                      body.put("message", "未认证,请先登录");
                      body.put("timestamp", System.currentTimeMillis());

                      ObjectMapper mapper = new ObjectMapper(); // 也可以用注入的 ObjectMapper
                      response.getWriter().write(mapper.writeValueAsString(body));
                    })
                .accessDeniedHandler(
                    (request, response, accessDeniedException) -> {
                      response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                      // 设置响应内容类型为 JSON
                      response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                      response.setCharacterEncoding(StandardCharsets.UTF_8.name());

                      // 构建 JSON 响应体(可以使用任意 JSON 库,这里以手动拼接为例)
                      Map<String, Object> body = new HashMap<>();
                      body.put("code", HttpServletResponse.SC_FORBIDDEN);
                      body.put("message", "未授权,请先授权");
                      body.put("timestamp", System.currentTimeMillis());

                      ObjectMapper mapper = new ObjectMapper(); // 也可以用注入的 ObjectMapper
                      response.getWriter().write(mapper.writeValueAsString(body));
                    }));

    http.addFilterAt(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
  }

  @Bean
  public AuthenticationManager authenticationManager(
      UserDetailsService userDetailsService,
      PasswordEncoder passwordEncoder,
      AuthenticationEventPublisher authenticationEventPublisher) {
    DaoAuthenticationProvider authenticationProvider =
        new DaoAuthenticationProvider(userDetailsService);
    authenticationProvider.setPasswordEncoder(passwordEncoder);
    ProviderManager providerManager = new ProviderManager(authenticationProvider);
    providerManager.setAuthenticationEventPublisher(authenticationEventPublisher);
    return providerManager;
  }

  @Bean
  public UserDetailsService userDetailsService() {
    UserDetails userDetails =
        User.withUsername("user").password("{noop}password").roles("USER").build();
    return new InMemoryUserDetailsManager(userDetails);
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }

  @Bean
  public AuthenticationEventPublisher authenticationEventPublisher(
      ApplicationEventPublisher applicationEventPublisher) {
    return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
  }
}
4. 登录接口的实现

创建一个AuthController,提供一个/auth/jwt/login接口。该接口接收用户名/密码,验证成功后生成JWT并返回给客户端。

java
package study.helloworld.springsecuritydemo.controller;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import study.helloworld.springsecuritydemo.JwtUtils;

@RestController()
@RequestMapping("/auth")
public class AuthController {

  @Autowired private AuthenticationManager authenticationManager;

  @RequestMapping("/jwt/login")
  public ResponseEntity<Map<String, Object>> jwtLogin(LoginRequest loginRequest)
      throws IOException {
    Map<String, Object> map = new HashMap<>();
    try {
      Authentication authenticationRequest =
          UsernamePasswordAuthenticationToken.unauthenticated(
              loginRequest.username(), loginRequest.password());
      Authentication authenticationResponse =
          this.authenticationManager.authenticate(authenticationRequest);

      String token = JwtUtils.generateToken(authenticationResponse.getName(), null);
      map.put("token", token);
    } catch (AuthenticationException e) {
      e.printStackTrace();
    }
    return ResponseEntity.ok(map);
  }

  public record LoginRequest(String username, String password) {}
}

方案二:OAuth2资源服务器

1. 添加依赖
xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security-oauth2-resource-server</artifactId>
</dependency>
2. 配置示例(JwtConfig)
java
package com.howxuexi.springsecurityoauth2resourceserverdemo.config;

import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.stereotype.Component;

/** JWT 工具类 使用 HS256 算法,支持自定义 claims,自动处理过期验证。 */
@Component
public class JwtConfig {

  public static final SecretKey SECRET_KEY;
  private static final long DEFAULT_EXPIRE_SECONDS = 3600L; // 默认1小时

  static {
    byte[] secret =
        "UibraTiIZ7OgBJGUkv6KfVoKNOoOElHLfHuFcjZiA64MFh47MzpTVfLGBqSqd5if5mXkUVhFvkFyva8VSOapudtRC5FkmGF8aCmxF5C6uiYUYxGRGTHBgkqRsDVUwWxQ4Nn5IcWUFNva2hp2yxJ8GJnzt3nHJYxvjPfumnxM42EIyvEq13T7QFYmr5VXBSIgxA64uuKWLWAAYB2iz05pDG9L0dZeoeTFvnfRiPHyd5IIER4vrzLbfTf5HhWbCmKV"
            .getBytes();
    SECRET_KEY = new SecretKeySpec(secret, "HmacSHA256");
  }

  @Autowired private JwtEncoder jwtEncoder;

  /**
   * 生成 token(默认过期时间)
   *
   * @param subject 用户标识(如 userId)
   * @param claims 自定义 claims(可为 null)
   * @return JWT 字符串
   */
  public String generateToken(String subject, Map<String, Object> claims) {
    return generateToken(subject, claims, DEFAULT_EXPIRE_SECONDS);
  }

  /**
   * 生成 token(自定义过期时间)
   *
   * @param subject 用户标识
   * @param claims 自定义 claims(可为 null)
   * @param expireSeconds 过期秒数
   * @return JWT 字符串
   */
  public String generateToken(String subject, Map<String, Object> claims, long expireSeconds) {
    Instant now = Instant.now();
    Instant expiration = now.plusSeconds(expireSeconds);

    var builder =
        JwtClaimsSet.builder()
            .issuer("http://localhost:9000") // 签发者(iss)
            .issuedAt(now) // 签发时间(iat)
            .expiresAt(expiration) // 过期时间(exp)
            .subject(subject); // 主题 = 用户名(sub)

    if (claims != null && !claims.isEmpty()) {
      builder.claims(claims1 -> Optional.ofNullable(claims).ifPresent(_ -> claims1.putAll(claims)));
    }

    return jwtEncoder
        .encode(
            JwtEncoderParameters.from(JwsHeader.with(MacAlgorithm.HS256).build(), builder.build()))
        .getTokenValue();
  }
}
3. 配置示例(SecurityConfig)
java
package com.howxuexi.springsecurityoauth2resourceserverdemo.config;

import com.nimbusds.jose.jwk.source.ImmutableSecret;
import jakarta.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import tools.jackson.databind.ObjectMapper;

@Configuration
public class SecurityConfig {

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(AbstractHttpConfigurer::disable);
    http.formLogin(AbstractHttpConfigurer::disable);
    http.httpBasic(AbstractHttpConfigurer::disable);
    http.logout(AbstractHttpConfigurer::disable);
    http.sessionManagement(
        sessionManagement ->
            sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

    // 仅对 /api/** 路径生效,不影响其他路径(如静态资源、actuator 等)
    http.securityMatcher("/api/**");

    http.authorizeHttpRequests(
            auth ->
                auth
                    // 登录接口不需要认证
                    .requestMatchers("/api/auth/login")
                    .permitAll()
                    // 其他所有 /api/** 请求必须携带有效 JWT
                    .anyRequest()
                    .authenticated())
        // 启用 OAuth2 Resource Server,使用 JWT 方式校验 token
        .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

    http.exceptionHandling(
        exception ->
            exception
                .authenticationEntryPoint(
                    (request, response, authException) -> {
                      // 设置响应状态码为 401 Unauthorized
                      response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                      // 设置响应内容类型为 JSON
                      response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                      response.setCharacterEncoding(StandardCharsets.UTF_8.name());

                      // 构建 JSON 响应体(可以使用任意 JSON 库,这里以手动拼接为例)
                      Map<String, Object> body = new HashMap<>();
                      body.put("code", HttpServletResponse.SC_UNAUTHORIZED);
                      body.put("message", "未认证,请先登录");
                      body.put("timestamp", System.currentTimeMillis());

                      ObjectMapper mapper = new ObjectMapper(); // 也可以用注入的 ObjectMapper
                      response.getWriter().write(mapper.writeValueAsString(body));
                    })
                .accessDeniedHandler(
                    (request, response, accessDeniedException) -> {
                      response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                      // 设置响应内容类型为 JSON
                      response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                      response.setCharacterEncoding(StandardCharsets.UTF_8.name());

                      // 构建 JSON 响应体(可以使用任意 JSON 库,这里以手动拼接为例)
                      Map<String, Object> body = new HashMap<>();
                      body.put("code", HttpServletResponse.SC_FORBIDDEN);
                      body.put("message", "未授权,请先授权");
                      body.put("timestamp", System.currentTimeMillis());

                      ObjectMapper mapper = new ObjectMapper(); // 也可以用注入的 ObjectMapper
                      response.getWriter().write(mapper.writeValueAsString(body));
                    }));
    return http.build();
  }

  @Bean
  public JwtEncoder jwtEncoder() {
    return new NimbusJwtEncoder(new ImmutableSecret<>(JwtConfig.SECRET_KEY));
  }

  @Bean
  public JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withSecretKey(JwtConfig.SECRET_KEY).build();
  }

  @Bean
  public AuthenticationManager authenticationManager(
      UserDetailsService userDetailsService,
      PasswordEncoder passwordEncoder,
      AuthenticationEventPublisher authenticationEventPublisher) {
    DaoAuthenticationProvider authenticationProvider =
        new DaoAuthenticationProvider(userDetailsService);
    authenticationProvider.setPasswordEncoder(passwordEncoder);
    ProviderManager providerManager = new ProviderManager(authenticationProvider);
    providerManager.setAuthenticationEventPublisher(authenticationEventPublisher);
    return providerManager;
  }

  @Bean
  public UserDetailsService userDetailsService() {
    UserDetails userDetails =
        User.withUsername("user").password("{noop}password").roles("USER").build();
    return new InMemoryUserDetailsManager(userDetails);
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }
}
4. 登录接口的实现

创建一个AuthController,提供一个/api/auth/login接口。该接口接收用户名/密码,验证成功后生成JWT并返回给客户端。

java
package com.howxuexi.springsecurityoauth2resourceserverdemo.controller;

import com.howxuexi.springsecurityoauth2resourceserverdemo.config.JwtConfig;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController()
@RequestMapping("/api/auth")
public class AuthController {

  @Autowired private AuthenticationManager authenticationManager;

  @Autowired
  private JwtConfig jwtConfig;

  @RequestMapping("/login")
  public ResponseEntity<Map<String, Object>> login(LoginRequest loginRequest) {
    Map<String, Object> map = new HashMap<>();
    try {
      Authentication authenticationRequest =
          UsernamePasswordAuthenticationToken.unauthenticated(
              loginRequest.username(), loginRequest.password());
      Authentication authenticationResponse =
          this.authenticationManager.authenticate(authenticationRequest);

      String token = jwtConfig.generateToken(authenticationResponse.getName(), null);
      map.put("token", token);
    } catch (AuthenticationException e) {
      e.printStackTrace();
    }
    return ResponseEntity.ok(map);
  }

  public record LoginRequest(String username, String password) {}
}

Last updated:

Released under the MIT License.