Spring Security 完全指南

Spring Security 完全指南

目录

  1. [Spring Security 概述](#Spring Security 概述)
  2. 快速入门
  3. 核心概念
  4. 认证配置
  5. 授权配置
  6. 密码加密
  7. 自定义登录
  8. 记住我功能
  9. 会话管理
  10. [CSRF 防护](#CSRF 防护)
  11. [JWT 集成](#JWT 集成)
  12. [OAuth2 集成](#OAuth2 集成)
  13. 方法级安全
  14. 实战案例
  15. 最佳实践

1. Spring Security 概述

1.1 什么是 Spring Security

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,是 Spring 生态系统中保护应用程序的事实标准。

1.2 主要功能

复制代码
Spring Security 功能:
├── 认证(Authentication)- 验证用户身份
├── 授权(Authorization)- 控制访问权限
├── 防护攻击 - CSRF、XSS、会话固定等
├── 集成支持 - LDAP、OAuth2、SAML 等
└── 方法级安全 - 注解控制方法访问

1.3 核心模块

xml 复制代码
<!-- 核心模块 -->
spring-security-core      <!-- 核心功能 -->
spring-security-web       <!-- Web 安全 -->
spring-security-config    <!-- 配置支持 -->
spring-security-oauth2    <!-- OAuth2 支持 -->
spring-security-jwt       <!-- JWT 支持 -->

2. 快速入门

2.1 添加依赖

xml 复制代码
<!-- Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- 如果需要测试 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
groovy 复制代码
// Gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

2.2 默认行为

添加依赖后,Spring Security 自动启用:

  • 所有请求都需要认证

  • 生成默认登录页面 /login

  • 生成默认登出页面 /logout

  • 创建默认用户 user,密码在控制台输出

  • 启用 CSRF 防护

    Using generated security password: 8e4f5c2a-1234-5678-9abc-def012345678

2.3 配置默认用户

yaml 复制代码
# application.yml
spring:
  security:
    user:
      name: admin
      password: admin123
      roles: ADMIN

2.4 基本配置类

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**", "/login", "/register").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout")
                .permitAll()
            );
        
        return http.build();
    }
}

3. 核心概念

3.1 SecurityContext

java 复制代码
// 获取当前认证信息
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();

// 获取用户信息
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

// 判断是否认证
boolean isAuthenticated = authentication.isAuthenticated();

3.2 Authentication

java 复制代码
public interface Authentication extends Principal, Serializable {
    // 获取权限集合
    Collection<? extends GrantedAuthority> getAuthorities();
    
    // 获取凭证(密码)
    Object getCredentials();
    
    // 获取详细信息
    Object getDetails();
    
    // 获取主体(用户)
    Object getPrincipal();
    
    // 是否已认证
    boolean isAuthenticated();
    
    // 设置认证状态
    void setAuthenticated(boolean isAuthenticated);
}

3.3 UserDetails

java 复制代码
public interface UserDetails extends Serializable {
    // 获取权限
    Collection<? extends GrantedAuthority> getAuthorities();
    
    // 获取密码
    String getPassword();
    
    // 获取用户名
    String getUsername();
    
    // 账户是否未过期
    boolean isAccountNonExpired();
    
    // 账户是否未锁定
    boolean isAccountNonLocked();
    
    // 凭证是否未过期
    boolean isCredentialsNonExpired();
    
    // 是否启用
    boolean isEnabled();
}

3.4 UserDetailsService

java 复制代码
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

3.5 GrantedAuthority

java 复制代码
public interface GrantedAuthority extends Serializable {
    String getAuthority();
}

// 常用实现
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ADMIN");

3.6 过滤器链

复制代码
Spring Security 过滤器链:
├── SecurityContextPersistenceFilter  - 安全上下文持久化
├── UsernamePasswordAuthenticationFilter - 用户名密码认证
├── BasicAuthenticationFilter - Basic 认证
├── RememberMeAuthenticationFilter - 记住我
├── AnonymousAuthenticationFilter - 匿名认证
├── ExceptionTranslationFilter - 异常处理
└── FilterSecurityInterceptor - 授权过滤器

4. 认证配置

4.1 内存用户

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder().encode("password"))
            .roles("USER")
            .build();
        
        UserDetails admin = User.builder()
            .username("admin")
            .password(passwordEncoder().encode("admin"))
            .roles("USER", "ADMIN")
            .build();
        
        return new InMemoryUserDetailsManager(user, admin);
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

4.2 数据库用户

java 复制代码
// 用户实体
@Entity
@Table(name = "users")
public class User implements UserDetails {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    private boolean enabled = true;
    
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
        name = "user_roles",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority(role.getName()))
            .collect(Collectors.toList());
    }
    
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    
    @Override
    public boolean isEnabled() {
        return enabled;
    }
    
    // getter/setter
}

// 角色实体
@Entity
@Table(name = "roles")
public class Role {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String name;
    
    // getter/setter
}
java 复制代码
// 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("用户不存在: " + username));
        return user;
    }
}
java 复制代码
// 配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Autowired
    private CustomUserDetailsService userDetailsService;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .userDetailsService(userDetailsService);
        
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

4.3 自定义 AuthenticationProvider

java 复制代码
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public Authentication authenticate(Authentication authentication) 
            throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        
        UserDetails user = userDetailsService.loadUserByUsername(username);
        
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("密码错误");
        }
        
        if (!user.isEnabled()) {
            throw new DisabledException("账户已禁用");
        }
        
        return new UsernamePasswordAuthenticationToken(
            user, password, user.getAuthorities());
    }
    
    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

5. 授权配置

5.1 URL 授权

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            // 公开访问
            .requestMatchers("/", "/home", "/public/**").permitAll()
            .requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
            
            // 角色控制
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
            
            // 权限控制
            .requestMatchers("/api/users/**").hasAuthority("USER_READ")
            .requestMatchers(HttpMethod.POST, "/api/users/**").hasAuthority("USER_WRITE")
            .requestMatchers(HttpMethod.DELETE, "/api/users/**").hasAuthority("USER_DELETE")
            
            // IP 限制
            .requestMatchers("/internal/**").hasIpAddress("192.168.1.0/24")
            
            // 其他请求需要认证
            .anyRequest().authenticated()
        );
    
    return http.build();
}

5.2 表达式授权

java 复制代码
.authorizeHttpRequests(auth -> auth
    // 使用 SpEL 表达式
    .requestMatchers("/admin/**").access(
        AuthorizationManagers.allOf(
            AuthorityAuthorizationManager.hasRole("ADMIN"),
            new WebExpressionAuthorizationManager("hasIpAddress('192.168.1.0/24')")
        )
    )
)

5.3 自定义授权管理器

java 复制代码
@Component
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
    
    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, 
                                       RequestAuthorizationContext context) {
        HttpServletRequest request = context.getRequest();
        Authentication auth = authentication.get();
        
        // 自定义授权逻辑
        if (auth == null || !auth.isAuthenticated()) {
            return new AuthorizationDecision(false);
        }
        
        // 检查特定条件
        String path = request.getRequestURI();
        if (path.startsWith("/api/admin")) {
            boolean hasAdminRole = auth.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
            return new AuthorizationDecision(hasAdminRole);
        }
        
        return new AuthorizationDecision(true);
    }
}

// 使用
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, 
        CustomAuthorizationManager customAuthorizationManager) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/**").access(customAuthorizationManager)
        );
    
    return http.build();
}

6. 密码加密

6.1 BCryptPasswordEncoder

java 复制代码
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

// 使用
@Autowired
private PasswordEncoder passwordEncoder;

// 加密密码
String encodedPassword = passwordEncoder.encode("rawPassword");

// 验证密码
boolean matches = passwordEncoder.matches("rawPassword", encodedPassword);

6.2 DelegatingPasswordEncoder

java 复制代码
@Bean
public PasswordEncoder passwordEncoder() {
    // 支持多种加密方式
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

// 密码格式:{id}encodedPassword
// {bcrypt}$2a$10$...
// {noop}plainTextPassword
// {sha256}...

6.3 自定义 PasswordEncoder

java 复制代码
@Bean
public PasswordEncoder passwordEncoder() {
    String idForEncode = "bcrypt";
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put("bcrypt", new BCryptPasswordEncoder());
    encoders.put("noop", NoOpPasswordEncoder.getInstance());
    encoders.put("sha256", new StandardPasswordEncoder());
    
    return new DelegatingPasswordEncoder(idForEncode, encoders);
}

7. 自定义登录

7.1 自定义登录页面

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .formLogin(form -> form
            .loginPage("/login")                    // 登录页面
            .loginProcessingUrl("/doLogin")         // 登录处理 URL
            .usernameParameter("username")          // 用户名参数
            .passwordParameter("password")          // 密码参数
            .defaultSuccessUrl("/home", true)       // 登录成功跳转
            .failureUrl("/login?error=true")        // 登录失败跳转
            .successHandler(authenticationSuccessHandler())
            .failureHandler(authenticationFailureHandler())
            .permitAll()
        );
    
    return http.build();
}

7.2 登录成功处理器

java 复制代码
@Component
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        Authentication authentication) throws IOException {
        // 记录登录日志
        String username = authentication.getName();
        String ip = request.getRemoteAddr();
        System.out.println("用户登录成功: " + username + ", IP: " + ip);
        
        // 根据角色跳转不同页面
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        
        if (authorities.stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
            response.sendRedirect("/admin/dashboard");
        } else {
            response.sendRedirect("/user/home");
        }
    }
}

// API 登录成功返回 JSON
@Component
public class JsonAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        Authentication authentication) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 200);
        result.put("message", "登录成功");
        result.put("username", authentication.getName());
        
        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

7.3 登录失败处理器

java 复制代码
@Component
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        AuthenticationException exception) throws IOException {
        String errorMessage = "登录失败";
        
        if (exception instanceof BadCredentialsException) {
            errorMessage = "用户名或密码错误";
        } else if (exception instanceof DisabledException) {
            errorMessage = "账户已被禁用";
        } else if (exception instanceof LockedException) {
            errorMessage = "账户已被锁定";
        } else if (exception instanceof AccountExpiredException) {
            errorMessage = "账户已过期";
        }
        
        response.sendRedirect("/login?error=" + URLEncoder.encode(errorMessage, "UTF-8"));
    }
}

// API 登录失败返回 JSON
@Component
public class JsonAuthenticationFailureHandler implements AuthenticationFailureHandler {
    
    @Autowired
    private ObjectMapper objectMapper;
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        AuthenticationException exception) throws IOException {
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        
        Map<String, Object> result = new HashMap<>();
        result.put("code", 401);
        result.put("message", exception.getMessage());
        
        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

7.4 登录页面示例

html 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>登录</title>
</head>
<body>
    <div class="login-container">
        <h2>用户登录</h2>
        
        <div th:if="${param.error}" class="error">
            用户名或密码错误
        </div>
        
        <div th:if="${param.logout}" class="success">
            已成功退出
        </div>
        
        <form th:action="@{/doLogin}" method="post">
            <div>
                <label>用户名:</label>
                <input type="text" name="username" required/>
            </div>
            <div>
                <label>密码:</label>
                <input type="password" name="password" required/>
            </div>
            <div>
                <label>
                    <input type="checkbox" name="remember-me"/> 记住我
                </label>
            </div>
            <button type="submit">登录</button>
        </form>
    </div>
</body>
</html>

8. 记住我功能

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .rememberMe(remember -> remember
            .key("uniqueAndSecret")              // 加密 key
            .tokenValiditySeconds(86400 * 7)    // 有效期 7 天
            .rememberMeParameter("remember-me") // 表单参数名
            .rememberMeCookieName("remember-me") // Cookie 名
            .userDetailsService(userDetailsService)
        );
    
    return http.build();
}

8.2 持久化 Token

java 复制代码
// 创建数据库表
CREATE TABLE persistent_logins (
    username VARCHAR(64) NOT NULL,
    series VARCHAR(64) PRIMARY KEY,
    token VARCHAR(64) NOT NULL,
    last_used TIMESTAMP NOT NULL
);
java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Autowired
    private DataSource dataSource;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 启动时创建表(仅首次)
        // tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .rememberMe(remember -> remember
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(86400 * 7)
                .userDetailsService(userDetailsService)
            );
        
        return http.build();
    }
}

9. 会话管理

9.1 会话配置

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            // 会话创建策略
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            
            // 会话固定攻击防护
            .sessionFixation().migrateSession()
            
            // 最大会话数
            .maximumSessions(1)
            
            // 达到最大会话数时阻止新登录
            .maxSessionsPreventsLogin(true)
            
            // 会话过期跳转
            .expiredUrl("/login?expired")
        );
    
    return http.build();
}

9.2 会话创建策略

java 复制代码
// ALWAYS - 总是创建会话
// IF_REQUIRED - 需要时创建(默认)
// NEVER - 不创建,但使用已存在的
// STATELESS - 不创建也不使用(适合 REST API)

.sessionManagement(session -> session
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)

9.3 并发会话控制

java 复制代码
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
            .maxSessionsPreventsLogin(false)  // 踢掉旧会话
            .expiredSessionStrategy(event -> {
                HttpServletResponse response = event.getResponse();
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write("{\"code\":401,\"message\":\"会话已过期\"}");
            })
        );
    
    return http.build();
}

10. CSRF 防护

10.1 默认 CSRF 配置

java 复制代码
// CSRF 默认开启,表单需要包含 CSRF Token
<form method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    <!-- 或使用 Thymeleaf -->
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
</form>

10.2 禁用 CSRF

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        // 禁用 CSRF(REST API 通常禁用)
        .csrf(csrf -> csrf.disable());
    
    return http.build();
}

10.3 自定义 CSRF 配置

java 复制代码
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf
            // 忽略特定路径
            .ignoringRequestMatchers("/api/**", "/webhook/**")
            
            // 自定义 Token 仓库
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        );
    
    return http.build();
}

11. JWT 集成

11.1 添加依赖

xml 复制代码
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

11.2 JWT 工具类

java 复制代码
@Component
public class JwtUtils {
    
    @Value("${jwt.secret}")
    private String secret;
    
    @Value("${jwt.expiration}")
    private long expiration;
    
    private Key getSigningKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    
    // 生成 Token
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", userDetails.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList()));
        
        return Jwts.builder()
            .setClaims(claims)
            .setSubject(userDetails.getUsername())
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + expiration))
            .signWith(getSigningKey(), SignatureAlgorithm.HS256)
            .compact();
    }
    
    // 解析 Token
    public Claims parseToken(String token) {
        return Jwts.parserBuilder()
            .setSigningKey(getSigningKey())
            .build()
            .parseClaimsJws(token)
            .getBody();
    }
    
    // 获取用户名
    public String getUsername(String token) {
        return parseToken(token).getSubject();
    }
    
    // 验证 Token
    public boolean validateToken(String token, UserDetails userDetails) {
        try {
            Claims claims = parseToken(token);
            String username = claims.getSubject();
            Date expiration = claims.getExpiration();
            
            return username.equals(userDetails.getUsername()) 
                && !expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
    
    // 判断是否过期
    public boolean isTokenExpired(String token) {
        try {
            return parseToken(token).getExpiration().before(new Date());
        } catch (ExpiredJwtException e) {
            return true;
        }
    }
}

11.3 JWT 认证过滤器

java 复制代码
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtUtils jwtUtils;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) 
            throws ServletException, IOException {
        
        String authHeader = request.getHeader("Authorization");
        
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        
        String token = authHeader.substring(7);
        
        try {
            String username = jwtUtils.getUsername(token);
            
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                
                if (jwtUtils.validateToken(token, userDetails)) {
                    UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(
                            userDetails, null, userDetails.getAuthorities());
                    
                    authentication.setDetails(
                        new WebAuthenticationDetailsSource().buildDetails(request));
                    
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        } catch (ExpiredJwtException e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"message\":\"Token已过期\"}");
            return;
        } catch (Exception e) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"message\":\"无效的Token\"}");
            return;
        }
        
        filterChain.doFilter(request, response);
    }
}

11.4 JWT 安全配置

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter, 
                UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

11.5 登录接口

java 复制代码
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private JwtUtils jwtUtils;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        try {
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getUsername(), request.getPassword())
            );
            
            SecurityContextHolder.getContext().setAuthentication(authentication);
            
            UserDetails userDetails = userDetailsService.loadUserByUsername(request.getUsername());
            String token = jwtUtils.generateToken(userDetails);
            
            Map<String, Object> response = new HashMap<>();
            response.put("code", 200);
            response.put("message", "登录成功");
            response.put("token", token);
            response.put("username", userDetails.getUsername());
            
            return ResponseEntity.ok(response);
        } catch (AuthenticationException e) {
            return ResponseEntity.status(401)
                .body(Map.of("code", 401, "message", "用户名或密码错误"));
        }
    }
}

// 登录请求 DTO
public class LoginRequest {
    private String username;
    private String password;
    // getter/setter
}

12. OAuth2 集成

12.1 添加依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

12.2 配置 OAuth2

yaml 复制代码
# application.yml
spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: your-client-id
            client-secret: your-client-secret
            scope: user:email,read:user
          google:
            client-id: your-client-id
            client-secret: your-client-secret
            scope: openid,profile,email
        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: login

12.3 安全配置

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/login/**", "/error").permitAll()
                .anyRequest().authenticated()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .defaultSuccessUrl("/home")
                .failureUrl("/login?error")
                .userInfoEndpoint(userInfo -> userInfo
                    .userService(customOAuth2UserService())
                )
            );
        
        return http.build();
    }
    
    @Bean
    public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
        return new CustomOAuth2UserService();
    }
}

12.4 自定义 OAuth2UserService

java 复制代码
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oauth2User = super.loadUser(userRequest);
        
        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String email = oauth2User.getAttribute("email");
        String name = oauth2User.getAttribute("name");
        
        // 查找或创建用户
        User user = userRepository.findByEmail(email)
            .orElseGet(() -> {
                User newUser = new User();
                newUser.setEmail(email);
                newUser.setName(name);
                newUser.setProvider(registrationId);
                return userRepository.save(newUser);
            });
        
        return new CustomOAuth2User(oauth2User, user);
    }
}

13. 方法级安全

13.1 启用方法安全

java 复制代码
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig {
}

13.2 @PreAuthorize

java 复制代码
@Service
public class UserService {
    
    // 需要 ADMIN 角色
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long id) {
        // 删除用户
    }
    
    // 需要特定权限
    @PreAuthorize("hasAuthority('USER_READ')")
    public User getUser(Long id) {
        // 获取用户
    }
    
    // 多条件
    @PreAuthorize("hasRole('ADMIN') or hasRole('MANAGER')")
    public List<User> getAllUsers() {
        // 获取所有用户
    }
    
    // 使用参数
    @PreAuthorize("#username == authentication.name or hasRole('ADMIN')")
    public User getUserByUsername(String username) {
        // 获取用户
    }
    
    // 使用 SpEL
    @PreAuthorize("@securityService.canAccess(#id)")
    public void updateUser(Long id, User user) {
        // 更新用户
    }
}

13.3 @PostAuthorize

java 复制代码
@Service
public class DocumentService {
    
    // 返回后检查
    @PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
    public Document getDocument(Long id) {
        return documentRepository.findById(id).orElse(null);
    }
}

13.4 @Secured

java 复制代码
@Service
public class OrderService {
    
    @Secured("ROLE_ADMIN")
    public void deleteOrder(Long id) {
        // 删除订单
    }
    
    @Secured({"ROLE_USER", "ROLE_ADMIN"})
    public Order getOrder(Long id) {
        // 获取订单
    }
}

13.5 自定义安全表达式

java 复制代码
@Component("securityService")
public class SecurityService {
    
    @Autowired
    private UserRepository userRepository;
    
    public boolean canAccess(Long userId) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null) return false;
        
        String currentUsername = auth.getName();
        User user = userRepository.findById(userId).orElse(null);
        
        if (user == null) return false;
        
        // 检查是否是本人或管理员
        return user.getUsername().equals(currentUsername) 
            || auth.getAuthorities().stream()
                .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
    }
}

// 使用
@PreAuthorize("@securityService.canAccess(#id)")
public void updateUser(Long id, UserDTO dto) {
    // 更新用户
}

14. 实战案例

14.1 完整的 JWT 认证系统

java 复制代码
// 1. 配置文件
// application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/security_demo
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update

jwt:
  secret: dGhpcyBpcyBhIHZlcnkgbG9uZyBzZWNyZXQga2V5IGZvciBqd3QgdG9rZW4=
  expiration: 86400000  # 24小时
java 复制代码
// 2. 用户实体
@Entity
@Table(name = "users")
@Data
public class User implements UserDetails {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(unique = true, nullable = false)
    private String username;
    
    @Column(nullable = false)
    private String password;
    
    @Column(unique = true)
    private String email;
    
    private boolean enabled = true;
    
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
    @Column(name = "role")
    private Set<String> roles = new HashSet<>();
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return roles.stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList());
    }
    
    @Override
    public boolean isAccountNonExpired() { return true; }
    
    @Override
    public boolean isAccountNonLocked() { return true; }
    
    @Override
    public boolean isCredentialsNonExpired() { return true; }
}
java 复制代码
// 3. Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
}
java 复制代码
// 4. DTO
@Data
public class LoginRequest {
    @NotBlank(message = "用户名不能为空")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    private String password;
}

@Data
public class RegisterRequest {
    @NotBlank(message = "用户名不能为空")
    @Size(min = 3, max = 20, message = "用户名长度3-20")
    private String username;
    
    @NotBlank(message = "密码不能为空")
    @Size(min = 6, max = 40, message = "密码长度6-40")
    private String password;
    
    @Email(message = "邮箱格式不正确")
    private String email;
}

@Data
@AllArgsConstructor
public class JwtResponse {
    private String token;
    private String type = "Bearer";
    private Long id;
    private String username;
    private List<String> roles;
    
    public JwtResponse(String token, Long id, String username, List<String> roles) {
        this.token = token;
        this.id = id;
        this.username = username;
        this.roles = roles;
    }
}
java 复制代码
// 5. Service
@Service
public class UserService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
    }
    
    public User register(RegisterRequest request) {
        if (userRepository.existsByUsername(request.getUsername())) {
            throw new RuntimeException("用户名已存在");
        }
        
        if (request.getEmail() != null && userRepository.existsByEmail(request.getEmail())) {
            throw new RuntimeException("邮箱已被使用");
        }
        
        User user = new User();
        user.setUsername(request.getUsername());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        user.setEmail(request.getEmail());
        user.setRoles(Set.of("ROLE_USER"));
        
        return userRepository.save(user);
    }
}
java 复制代码
// 6. Controller
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private JwtUtils jwtUtils;
    
    @PostMapping("/login")
    public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
        );
        
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        User user = (User) authentication.getPrincipal();
        String token = jwtUtils.generateToken(user);
        
        List<String> roles = user.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.toList());
        
        return ResponseEntity.ok(new JwtResponse(token, user.getId(), user.getUsername(), roles));
    }
    
    @PostMapping("/register")
    public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
        try {
            User user = userService.register(request);
            return ResponseEntity.ok(Map.of(
                "message", "注册成功",
                "username", user.getUsername()
            ));
        } catch (RuntimeException e) {
            return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
        }
    }
    
    @GetMapping("/me")
    public ResponseEntity<?> getCurrentUser(@AuthenticationPrincipal User user) {
        return ResponseEntity.ok(Map.of(
            "id", user.getId(),
            "username", user.getUsername(),
            "email", user.getEmail(),
            "roles", user.getRoles()
        ));
    }
}
java 复制代码
// 7. 安全配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    
    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Autowired
    private UserService userService;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .exceptionHandling(exception -> exception
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType("application/json;charset=UTF-8");
                    response.setStatus(401);
                    response.getWriter().write("{\"code\":401,\"message\":\"未认证\"}");
                })
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setContentType("application/json;charset=UTF-8");
                    response.setStatus(403);
                    response.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
                })
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:3000"));
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setAllowCredentials(true);
        
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
    
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

15. 最佳实践

15.1 密码安全

java 复制代码
// ✅ 使用 BCrypt 加密
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(12);  // 强度 12
}

// ✅ 密码复杂度验证
public boolean isValidPassword(String password) {
    // 至少8位,包含大小写字母和数字
    return password.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]{8,}$");
}

15.2 Token 安全

java 复制代码
// ✅ 使用足够长的密钥
jwt:
  secret: dGhpcyBpcyBhIHZlcnkgbG9uZyBzZWNyZXQga2V5IGZvciBqd3QgdG9rZW4gYXQgbGVhc3QgMjU2IGJpdHM=

// ✅ 设置合理的过期时间
jwt:
  expiration: 3600000  # 1小时

// ✅ 实现 Token 刷新机制
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String token) {
    // 验证并刷新 Token
}

15.3 异常处理

java 复制代码
@ControllerAdvice
public class SecurityExceptionHandler {
    
    @ExceptionHandler(AccessDeniedException.class)
    public ResponseEntity<?> handleAccessDeniedException(AccessDeniedException e) {
        return ResponseEntity.status(403)
            .body(Map.of("code", 403, "message", "权限不足"));
    }
    
    @ExceptionHandler(AuthenticationException.class)
    public ResponseEntity<?> handleAuthenticationException(AuthenticationException e) {
        return ResponseEntity.status(401)
            .body(Map.of("code", 401, "message", "认证失败"));
    }
}

15.4 日志审计

java 复制代码
@Component
public class SecurityAuditListener {
    
    private static final Logger logger = LoggerFactory.getLogger(SecurityAuditListener.class);
    
    @EventListener
    public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
        String username = event.getAuthentication().getName();
        logger.info("用户登录成功: {}", username);
    }
    
    @EventListener
    public void onAuthenticationFailure(AbstractAuthenticationFailureEvent event) {
        String username = event.getAuthentication().getName();
        logger.warn("用户登录失败: {}, 原因: {}", username, event.getException().getMessage());
    }
}

15.5 安全配置检查清单

复制代码
✅ 使用 HTTPS
✅ 启用 CSRF 防护(Web 应用)
✅ 配置 CORS
✅ 使用强密码加密
✅ 设置合理的会话超时
✅ 限制登录尝试次数
✅ 记录安全日志
✅ 定期更新依赖
✅ 最小权限原则
✅ 输入验证

附录:常用注解速查

注解 说明
@EnableWebSecurity 启用 Web 安全
@EnableMethodSecurity 启用方法级安全
@PreAuthorize 方法执行前检查
@PostAuthorize 方法执行后检查
@Secured 角色检查
@AuthenticationPrincipal 注入当前用户
@CurrentSecurityContext 注入安全上下文
相关推荐
PRINT!2 小时前
RabbitMQ实战项目(含代码仓库地址+视频教程地址)基本篇已更新完结,高级篇持续更新中
java·分布式·后端·微服务·rabbitmq
gAlAxy...3 小时前
MyBatis-Plus 核心 CRUD 操作全解析:BaseMapper 与通用 Service 实战
java·开发语言·mybatis
开开心心就好3 小时前
一键加密隐藏视频,专属格式播放工具
java·linux·开发语言·网络·人工智能·macos
Amarantine、沐风倩✨3 小时前
列表接口严禁嵌套 LISTAGG + REGEXP:一次 mission_label 性能事故复盘
java·数据库·sql
m***06683 小时前
Java进阶(ElasticSearch的安装与使用)
java·elasticsearch·jenkins
Anastasiozzzz4 小时前
Java异步编程:CompletableFuture从入门到底层实现
java·开发语言
xiaomin-Michael4 小时前
netty学习
java
上海合宙LuatOS4 小时前
LuatOS核心库API——【fft 】 快速傅里叶变换
java·前端·人工智能·单片机·嵌入式硬件·物联网·机器学习
爱敲代码的小鱼4 小时前
web后端开发SpringBootWeb的入门:
java·spring boot·spring