springboot

Spring Security 安全认证

By AI-Writer 15 min read

Spring Security 安全认证

Spring Security 是 Spring 生态中最权威的安全框架,提供完整的认证(Authentication)与授权(Authorization)解决方案。Spring Security 6.x 进行了重大重构,移除了部分废弃 API,提供了更现代的 Lambda DSL 配置方式。本文覆盖 JWT 无状态认证和 OAuth 2.0 资源服务器两大核心场景。

核心概念

  • 认证(Authentication):确认用户是谁。验证凭证(用户名+密码、JWT Token、OAuth Token 等)
  • 授权(Authorization):确认用户能做什么。判断用户是否拥有访问资源的权限
  • Principal:认证后的主体(通常是用户信息)
  • GrantedAuthority:用户被授予的权限(如 ROLE_USERSCOPE_read
  • SecurityContext:保存当前认证信息的上下文

Spring Security 6.x 依赖与基本配置

依赖引入

xml
<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>
<!-- JWT -->
<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>

Spring Security 6.x 默认启用所有端点保护(除 /actuator/health),添加 spring-boot-starter-security 后所有接口都需要认证。

最小化配置

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)  // 禁用 CSRF(前后端分离 API)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**", "/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .addFilterBefore(jwtAuthenticationFilter(),
                             UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();  // BCrypt 密码哈希(单向加密)
    }
}

JWT 无状态认证实现

整体流程

plaintext
用户登录 → 验证密码 → 生成 JWT → 返回 Token
后续请求 → Header: Authorization: Bearer <token>
        → JwtAuthFilter 拦截 → 验证 Token → 放行 / 拒绝

JWT 工具类

java
@Component
public class JwtUtils {

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

    @Value("${jwt.expiration-ms:86400000}")  // 默认 24 小时
    private long expirationMs;

    // 签发 Token
    public String generateToken(UserDetails user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", user.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .toList());
        return Jwts.builder()
                .claims(claims)
                .subject(user.getUsername())
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + expirationMs))
                .signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)),
                          Jwts.SIG.HS256)
                .compact();
    }

    // 验证并解析 Token
    public Claims parseToken(String token) {
        return Jwts.parser()
                .verifyWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    // 从 Token 提取用户名
    public String extractUsername(String token) {
        return parseToken(token).getSubject();
    }

    // 验证 Token 是否有效(未过期且签名正确)
    public boolean isTokenValid(String token, UserDetails user) {
        try {
            String username = extractUsername(token);
            return username.equals(user.getUsername()) && !isTokenExpired(token);
        } catch (JwtException e) {
            return false;
        }
    }

    private boolean isTokenExpired(String token) {
        return parseToken(token).getExpiration().before(new Date());
    }
}

JWT 认证过滤器

java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtils jwtUtils;
    private final UserDetailsService userDetailsService;

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

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain filterChain)
            throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

        // 提取 Token(格式:Bearer <token>)
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = authHeader.substring(7);

        try {
            String username = jwtUtils.extractUsername(token);
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();

            // 未认证且 Token 有效
            if (username != null && auth == null) {
                UserDetails user = userDetailsService.loadUserByUsername(username);

                if (jwtUtils.isTokenValid(token, user)) {
                    UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                            user, null, user.getAuthorities()
                        );
                    authentication.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request)
                    );
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        } catch (JwtException e) {
            // Token 无效,继续请求但不设置认证上下文
            // 可在此记录日志或返回 401
        }

        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        // 跳过登录接口
        return request.getServletPath().startsWith("/api/auth/");
    }
}

登录接口

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

    private final AuthenticationManager authenticationManager;
    private final JwtUtils jwtUtils;
    private final UserRepository userRepository;

    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
        // 1. 验证用户名密码(触发 Security 认证流程)
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.username(),
                request.password()
            )
        );

        // 2. 认证成功,生成 JWT
        UserDetails user = (UserDetails) authentication.getPrincipal();
        String token = jwtUtils.generateToken(user);

        return ResponseEntity.ok(new LoginResponse(token, "Bearer", 86400));
    }

    // 注册接口
    @PostMapping("/register")
    public ResponseEntity<Void> register(@Valid @RequestBody RegisterRequest request,
                                          @Autowired PasswordEncoder encoder) {
        if (userRepository.existsByUsername(request.username())) {
            throw new UserAlreadyExistsException();
        }
        User user = new User(
            request.username(),
            encoder.encode(request.password()),  // BCrypt 加密存储
            Set.of("ROLE_USER")
        );
        userRepository.save(user);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }

    public record LoginRequest(String username, String password) {}
    public record RegisterRequest(String username, String password) {}
    public record LoginResponse(String token, String type, int expiresIn) {}
}
java
@Configuration
public class SecurityConfig {
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

使用 AuthenticationManagerBuilder(自定义登录逻辑时)

如果不想用默认的 AuthenticationManager,也可以自行实现认证逻辑:

java
@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException(username));

        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            user.getAuthorities()  // Collection<? extends GrantedAuthority>
        );
    }
}

授权:方法级安全注解

@PreAuthorize 与 @PostAuthorize

java
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')")  // 整个控制器要求 ADMIN 角色
public class AdminController {

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

    @DeleteMapping("/users/{id}")
    @PreAuthorize("hasRole('ADMIN') and #id != authentication.principal.id")
    // 禁止管理员删除自己
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteById(id);
        return ResponseEntity.noContent().build();
    }

    // SpEL 表达式中使用方法参数
    @GetMapping("/users/{username}")
    @PreAuthorize("#username == authentication.principal.username or hasRole('ADMIN')")
    public User getUserProfile(@PathVariable String username) {
        return userService.findByUsername(username);
    }

    // @PostAuthorize:在方法执行后验证,适合基于返回值决定权限
    @GetMapping("/secret")
    @PostAuthorize("returnObject.owner == authentication.principal.username or hasRole('ADMIN')")
    public Document getSecretDocument(@PathVariable Long id) {
        return documentService.findById(id);
    }
}

启用方法安全注解

java
@SpringBootApplication
@EnableMethodSecurity(
    prePostEnabled = true,    // 启用 @PreAuthorize / @PostAuthorize
    securedEnabled = true,     // 启用 @Secured
    jsr250Enabled = true      // 启用 @RolesAllowed
)
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

OAuth 2.0 资源服务器

如果你的 API 要作为资源服务器验证第三方颁发的 OAuth 2.0 Token:

java
@Configuration
@EnableWebSecurity
public class OAuth2ResourceServerConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwtAuthenticationConverter(
                    new JwtAuthenticationConverterWithDefaults()
                ))
            );
        return http.build();
    }
}
yaml
# application.yml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # 方式1:使用授权服务器的公钥端点
          issuer-uri: https://auth.example.com
          # 方式2:直接指定公钥(离线验证)
          # jwk-set-uri: https://auth.example.com/.well-known/jwks.json

JWT 认证过滤器会自动验证 Token 签名、有效期等,/api/me 接口可直接获取用户信息:

java
@GetMapping("/api/me")
public UserProfile getCurrentUser(@AuthenticationPrincipal Jwt jwt) {
    // @AuthenticationPrincipal 自动注入解析后的 JWT
    return new UserProfile(
        jwt.getSubject(),           // 用户 ID
        jwt.getClaimAsString("name"),
        jwt.getClaimAsStringList("roles"),
        jwt.getClaimAsString("email")
    );
}

常见安全配置

CORS 跨域配置

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()));

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(List.of("http://localhost:3000"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/api/**", config);
        return source;
    }
}

密码加密

java
@Bean
public PasswordEncoder passwordEncoder() {
    // BCrypt:自带盐,每轮加密结果不同(包含盐值存储)
    return new BCryptPasswordEncoder(12);  // 强度因子 12(性能与安全平衡)
}

// 验证密码
if (passwordEncoder.matches(rawPassword, encodedPassword)) {
    // 密码匹配
}

小结

  • Spring Security 6.x 使用 Lambda DSL 配置 SecurityFilterChain,语法更简洁
  • JWT 无状态认证流程:登录时 AuthenticationManager 验证密码 → JwtUtils.generateToken() 签发 → JwtAuthFilter 拦截请求验证
  • BCryptPasswordEncoder 是密码存储的首选,绝不明文存储密码
  • @PreAuthorize 支持 SpEL 表达式,可引用方法参数和方法名实现精细权限控制
  • OAuth 2.0 资源服务器通过 spring.security.oauth2.resourceserver.jwt.issuer-uri 一行配置即可接入
#springboot #spring-security #jwt #oauth2 #authentication

评论

A

Written by

AI-Writer

Related Articles

springboot
#5

数据访问:JPA 与 MyBatis

Spring Data JPA 实体映射与 Repository 接口、MyBatis-Plus 增强用法、JdbcTemplate 原始查询、事务管理(@Transactional 传播行为与隔离级别)完整指南

Read More