Modern Architecture
& Coding Solutions

Spring Security 6 + JWT + OAuth2 实战:从零构建安全认证中心

2026 年,Spring Security 6 已成为 Java 后端安全的绝对标配。但许多开发者仍在使用过时的 WebSecurityConfigurerAdapter,或对 JWT 的无状态校验、OAuth2 的授权码流程一知半解。本文将从零搭建一个生产级认证中心,涵盖 Spring Security 6 新配置范式(Lambda DSL)、JWT 令牌生成与解析、RBAC 权限控制、OAuth2 三方登录四大模块,代码完整可运行。

一、为什么需要认证中心?

在微服务架构中,每个服务都独立做身份验证是灾难性的。认证中心(Authorization Server) 集中处理登录、颁发令牌,各业务服务通过令牌校验身份,从而实现 SSO(单点登录) 和统一权限管理。

Spring Security 6 与 Spring Boot 3.x 配合,弃用了旧版 WebSecurityConfigurerAdapter,全面拥抱 Lambda DSL + 组件化配置。我们将基于 Spring Boot 3.4.2 + Spring Security 6.4.1 构建。

二、项目初始化与依赖

使用 Spring Initializr 创建项目,依赖如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.2</version>
</parent>

<dependencies>
    <!-- Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- Web 支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- JWT 操作 (jjwt 0.12.x) -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.12.6</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.12.6</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.12.6</version>
        <scope>runtime</scope>
    </dependency>
    <!-- 数据库与 JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

注意:jjwt 0.12.x 的 API 与 0.11.x 有较大变化,parserBuilder() 方法已被移除,需使用新的 Jwts.parser() 构建方式。

三、用户存储与认证逻辑

3.1 用户实体与 Repository

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;
    private String username;
    private String password; // 存储 BCrypt 哈希
    private String email;
    private String roles; // 如 "ROLE_USER,ROLE_ADMIN"
    // getters & setters
}

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

3.2 自定义 UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("用户不存在"));

        List<GrantedAuthority> authorities = Arrays.stream(user.getRoles().split(","))
                .map(role -> new SimpleGrantedAuthority(role.trim()))
                .collect(Collectors.toList());

        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                authorities
        );
    }
}

四、Spring Security 6 核心配置(Lambda DSL)

Spring Security 6 的最大变化是移除了 WebSecurityConfigurerAdapter,改用 SecurityFilterChain Bean + Lambda DSL 进行配置。

旧版(Spring Security 5.x)已废弃的方式

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/auth/**").permitAll()
            .anyRequest().authenticated();
    }
}

新版(Spring Security 6.x)Lambda DSL 方式

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 替代 @EnableGlobalMethodSecurity
public class SecurityConfig {

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

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, 
                                                       CustomUserDetailsService userDetailsService,
                                                       PasswordEncoder passwordEncoder) throws Exception {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder);
        return http.authenticationProvider(authProvider).getSharedObject(AuthenticationManager.class);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, 
                                           JwtAuthenticationFilter jwtFilter) throws Exception {
        http
            // Lambda DSL:csrf() 和 cors() 现在需要显式配置
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> 
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            // 新的 authorizeHttpRequests() 替代 authorizeRequests()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/auth/login", "/auth/register", "/oauth2/**").permitAll()  // antMatchers → requestMatchers
                .anyRequest().authenticated()
            )
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

关键变化总结

旧版(Spring Security 5.x)新版(Spring Security 6.x)
extends WebSecurityConfigurerAdapter@Bean SecurityFilterChain
authorizeRequests()authorizeHttpRequests()
antMatchers()requestMatchers()
链式 .and() 调用Lambda DSL
@EnableGlobalMethodSecurity@EnableMethodSecurity

五、JWT 工具类(jjwt 0.12.x)

jjwt 0.12.x 的 API 发生了重大变化,parserBuilder() 已被移除,需使用新的 Jwts.parser() 构建方式。

@Component
public class JwtUtils {

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

    @Value("${jwt.expirationMs}")
    private int expirationMs;

    // 生成 JWT
    public String generateToken(String username, List<String> roles) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expirationMs);

        return Jwts.builder()
                .subject(username)
                .claim("roles", roles)
                .issuedAt(now)
                .expiration(expiryDate)
                .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
                .compact();
    }

    // 解析 JWT(jjwt 0.12.x 新 API)
    public Claims getClaims(String token) {
        return Jwts.parser()
                .verifyWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    public boolean validateToken(String token) {
        try {
            getClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    public String getUsername(String token) {
        return getClaims(token).getSubject();
    }
}

六、JWT 认证过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtils jwtUtils;
    private final CustomUserDetailsService userDetailsService;

    public JwtAuthenticationFilter(JwtUtils jwtUtils, CustomUserDetailsService userDetailsService) {
        this.jwtUtils = jwtUtils;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain chain) throws ServletException, IOException {
        String token = extractJwtFromRequest(request);
        if (token != null && jwtUtils.validateToken(token)) {
            String username = jwtUtils.getUsername(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    private String extractJwtFromRequest(HttpServletRequest request) {
        String header = request.getHeader("Authorization");
        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }
        return null;
    }
}

七、登录与注册接口

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

    @Autowired
    private AuthenticationManager authManager;

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        Authentication authentication = authManager.authenticate(
                new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
        );

        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        List<String> roles = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());

        String token = jwtUtils.generateToken(userDetails.getUsername(), roles);
        return ResponseEntity.ok(new JwtResponse(token));
    }

    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
        if (userRepository.findByUsername(request.getUsername()).isPresent()) {
            return ResponseEntity.badRequest().body("用户名已存在");
        }
        User user = new User();
        user.setUsername(request.getUsername());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        user.setEmail(request.getEmail());
        user.setRoles("ROLE_USER");
        userRepository.save(user);
        return ResponseEntity.ok("注册成功");
    }
}

八、RBAC 权限控制(方法级注解)

通过 @PreAuthorize 实现方法级权限校验:

@RestController
@RequestMapping("/admin")
public class AdminController {

    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/users")
    public List<User> listAllUsers() {
        return userService.findAll();
    }

    @PreAuthorize("hasAuthority('SCOPE_write')")
    @PostMapping("/data")
    public String updateData() {
        return "Data updated";
    }
}

九、OAuth2 三方登录(以 GitHub 为例)

9.1 配置 application.yml

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: ${GITHUB_CLIENT_ID}
            client-secret: ${GITHUB_CLIENT_SECRET}
            scope: read:user
        provider:
          github:
            authorization-uri: https://github.com/login/oauth/authorize
            token-uri: https://github.com/login/oauth/access_token
            user-info-uri: https://api.github.com/user
            user-name-attribute: id

9.2 OAuth2 登录成功处理器

@Component
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private JwtUtils jwtUtils;

    @Autowired
    private UserRepository userRepository;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
        Map<String, Object> attributes = token.getPrincipal().getAttributes();
        String githubId = attributes.get("id").toString();
        String name = (String) attributes.get("login");

        User user = userRepository.findByUsername(githubId).orElseGet(() -> {
            User newUser = new User();
            newUser.setUsername(githubId);
            newUser.setPassword(UUID.randomUUID().toString());
            newUser.setRoles("ROLE_USER");
            return userRepository.save(newUser);
        });

        String jwt = jwtUtils.generateToken(user.getUsername(), List.of(user.getRoles().split(",")));
        response.sendRedirect("http://localhost:3000/oauth2/redirect?token=" + jwt);
    }
}

9.3 在 SecurityConfig 中配置 OAuth2 登录

http
    .oauth2Login(oauth2 -> oauth2
        .successHandler(oAuth2LoginSuccessHandler)
    )
    .authorizeHttpRequests(authz -> authz
        .requestMatchers("/auth/**", "/oauth2/**").permitAll()
        .anyRequest().authenticated()
    );

访问 /oauth2/authorization/github 即可跳转到 GitHub 授权页。

十、统一异常处理(生产环境必备)

Spring Security 的认证授权异常发生在过滤器层面,@RestControllerAdvice 无法捕获,需单独配置:

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json");
        response.getWriter().write("{\"code\":\"unauthorized\",\"message\":\"请先登录\"}");
    }
}

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json");
        response.getWriter().write("{\"code\":\"forbidden\",\"message\":\"权限不足\"}");
    }
}

SecurityConfig 中挂接:

http.exceptionHandling(exceptions -> exceptions
    .authenticationEntryPoint(customAuthenticationEntryPoint)
    .accessDeniedHandler(customAccessDeniedHandler)
);

十一、测试验证

# 1. 注册用户
curl -X POST http://localhost:8080/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"test","password":"123456","email":"test@demo.com"}'

# 2. 登录获取 JWT
curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"test","password":"123456"}'
# 返回: {"token":"eyJ...", "type":"Bearer"}

# 3. 访问受保护接口
curl -X GET http://localhost:8080/admin/users \
  -H "Authorization: Bearer eyJ..."

十二、生产环境最佳实践

  1. JWT Secret 管理:使用环境变量或配置中心,长度至少 256 位
  2. 令牌刷新:可结合 Redis 实现 Refresh Token 机制
  3. 日志与审计:结合 MDC 记录登录日志,参考本站 《Java 日志最佳实践 2026》
  4. 监控告警:接入 Prometheus 监控认证服务,参考 《Java 应用接入 Prometheus + Grafana 全记录》
  5. 容器化部署:参考 《Spring Boot 3.4 Docker 镜像最佳实践》

十三、总结

本文完整实现了基于 Spring Security 6 + JWT + OAuth2 的认证中心,核心要点:

  • Lambda DSL 替代 WebSecurityConfigurerAdapter,配置更简洁
  • jjwt 0.12.x 新 API(Jwts.parser() 替代 parserBuilder()
  • @EnableMethodSecurity 替代 @EnableGlobalMethodSecurity
  • requestMatchers() 替代 antMatchers()
  • OAuth2 登录与 JWT 无缝集成

掌握这套方案,你就能为微服务架构构建坚实的身份与权限基座。


📌 系列拓展阅读


📚 参考文献

  1. Spring Security 官方文档. 6.4 Reference. https://docs.spring.io/spring-security/reference/
  2. JJWT GitHub. 0.12.6 API. https://github.com/jwtk/jjwt
  3. Baeldung. Migrate Application from Spring Security 5 to Spring Security 6. https://www.baeldung.com/spring-security-migrate-5-to-6
  4. Spring Security 6.x Migration Guide. WebSecurityConfigurerAdapter → SecurityFilterChain. https://github.com/giuseppe-trisciuoglio/developer-kit
  5. CSDN. Spring Security6 & OAuth2 实战:从零构建多场景认证授权系统. https://blog.csdn.net/wine/article/details/152388559
赞(0) 打赏
未经允许不得转载:MACS Dev Hub » Spring Security 6 + JWT + OAuth2 实战:从零构建安全认证中心

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫

登录

找回密码

注册