Spring Security 教程

Spring 应用程序的安全框架

目录

1. Spring Security 概述

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,是保护基于 Spring 的应用程序的标准。它专注于为 Java 应用程序提供身份验证和授权。

1.1 Spring Security 的主要特性

1.2 Spring Security 的核心组件

小提示

Spring Security 默认采用"安全优先"的设计理念,这意味着默认情况下所有资源都是受保护的,需要明确配置才能访问。

2. 快速入门

2.1 添加依赖

在 Maven 项目中添加 Spring Security 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

在 Gradle 项目中添加依赖:

implementation 'org.springframework.boot:spring-boot-starter-security'

2.2 基本配置

创建一个基本的 Spring Security 配置类:

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((requests) -> requests
                .requestMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin((form) -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout((logout) -> logout.permitAll());

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();

        return new InMemoryUserDetailsManager(user);
    }
}

2.3 测试配置

创建一个简单的控制器来测试安全配置:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

    @GetMapping("/")
    public String home() {
        return "home";
    }

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}
注意

上面的示例使用了内存中的用户存储,这在生产环境中是不推荐的。在实际应用中,应该使用数据库或其他持久化存储来管理用户信息。

3. 认证机制

3.1 认证流程

Spring Security 的认证流程如下:

  1. 用户提交用户名和密码
  2. Spring Security 的过滤器链处理请求
  3. AuthenticationManager 验证用户凭据
  4. 如果验证成功,创建 Authentication 对象并存储在 SecurityContextHolder 中
  5. 如果验证失败,抛出 AuthenticationException

3.2 自定义认证

实现自定义的 UserDetailsService:

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

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

        return org.springframework.security.core.userdetails.User
                .withUsername(user.getUsername())
                .password(user.getPassword())
                .roles(user.getRoles().toArray(new String[0]))
                .build();
    }
}

3.3 密码编码器

配置密码编码器:

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

3.4 自定义登录页面

配置自定义登录页面:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((requests) -> requests
            .requestMatchers("/", "/home", "/css/**", "/js/**").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin((form) -> form
            .loginPage("/login")
            .loginProcessingUrl("/login")
            .defaultSuccessUrl("/dashboard")
            .failureUrl("/login?error=true")
            .permitAll()
        )
        .logout((logout) -> logout
            .logoutUrl("/logout")
            .logoutSuccessUrl("/login?logout=true")
            .permitAll()
        );

    return http.build();
}
小提示

在生产环境中,应该使用 HTTPS 来保护用户凭据的传输。可以通过配置 `http.requiresChannel().anyRequest().requiresSecure()` 来强制使用 HTTPS。

4. 授权机制

4.1 基于角色的访问控制

配置基于角色的访问控制:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((requests) -> requests
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .requestMatchers("/user/**").hasRole("USER")
            .requestMatchers("/", "/home").permitAll()
            .anyRequest().authenticated()
        );

    return http.build();
}

4.2 基于权限的访问控制

配置基于权限的访问控制:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((requests) -> requests
            .requestMatchers("/read/**").hasAuthority("READ")
            .requestMatchers("/write/**").hasAuthority("WRITE")
            .requestMatchers("/", "/home").permitAll()
            .anyRequest().authenticated()
        );

    return http.build();
}

4.3 自定义访问决策

实现自定义的 AccessDecisionVoter:

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class CustomAccessDecisionVoter implements AccessDecisionVoter<Object> {

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return attribute.getAttribute() != null && 
               attribute.getAttribute().startsWith("CUSTOM_");
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        if (authentication == null) {
            return ACCESS_DENIED;
        }

        for (ConfigAttribute attribute : attributes) {
            if (this.supports(attribute)) {
                // 自定义投票逻辑
                for (GrantedAuthority authority : authentication.getAuthorities()) {
                    if (authority.getAuthority().equals(attribute.getAttribute())) {
                        return ACCESS_GRANTED;
                    }
                }
            }
        }

        return ACCESS_ABSTAIN;
    }
}
注意

Spring Security 的授权机制遵循"最小权限原则",即默认情况下拒绝所有访问,然后明确授予必要的权限。

5. Web 安全

5.1 安全过滤器链

Spring Security 使用过滤器链来保护 Web 应用程序:

5.2 自定义过滤器

添加自定义过滤器到过滤器链:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

5.3 会话管理

配置会话管理:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .sessionManagement((session) -> session
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true)
            .expiredUrl("/login?expired")
        );

    return http.build();
}
小提示

对于无状态应用程序,可以将会话创建策略设置为 STATELESS,这样 Spring Security 不会创建或使用 HttpSession。

6. 方法级安全

6.1 启用方法级安全

在配置类上启用方法级安全:

import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
    // 配置内容
}

6.2 使用注解保护方法

使用 @PreAuthorize 注解保护方法:

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long userId) {
        // 删除用户的逻辑
    }

    @PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
    public void updateUserProfile(Long userId, UserProfile profile) {
        // 更新用户资料的逻辑
    }
}

6.3 使用 @PostAuthorize 注解

使用 @PostAuthorize 注解在方法执行后进行授权检查:

import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.stereotype.Service;

@Service
public class DocumentService {

    @PostAuthorize("returnObject.owner == authentication.principal.username")
    public Document getDocument(Long documentId) {
        // 获取文档的逻辑
        return documentRepository.findById(documentId).orElse(null);
    }
}
注意

方法级安全注解需要启用 AOP 支持。在 Spring Boot 应用中,spring-boot-starter-aop 依赖会自动添加。

7. OAuth2 集成

7.1 OAuth2 概述

OAuth2 是一种授权框架,允许第三方应用程序代表资源所有者访问受保护的资源。Spring Security 提供了对 OAuth2 的全面支持。

7.2 添加 OAuth2 依赖

在 Maven 项目中添加 OAuth2 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

7.3 配置 OAuth2 客户端

在 application.properties 中配置 OAuth2 客户端:

spring.security.oauth2.client.registration.google.client-id=your-client-id
spring.security.oauth2.client.registration.google.client-secret=your-client-secret
spring.security.oauth2.client.registration.google.scope=profile,email

7.4 配置 OAuth2 登录

配置 OAuth2 登录:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((requests) -> requests
            .requestMatchers("/", "/home").permitAll()
            .anyRequest().authenticated()
        )
        .oauth2Login((oauth2) -> oauth2
            .loginPage("/login")
            .defaultSuccessUrl("/dashboard")
        );

    return http.build();
}
小提示

Spring Security 支持多种 OAuth2 提供商,包括 Google、GitHub、Facebook 等。只需添加相应的配置即可。

8. JWT 支持

8.1 JWT 概述

JSON Web Token (JWT) 是一种紧凑的、自包含的方式,用于在各方之间安全地传输信息。Spring Security 可以与 JWT 集成,用于无状态认证。

8.2 添加 JWT 依赖

在 Maven 项目中添加 JWT 依赖:

<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>

8.3 创建 JWT 工具类

创建 JWT 工具类用于生成和验证令牌:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtUtil {

    private final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private final long jwtTokenValidity = 5 * 60 * 60 * 1000; // 5 hours

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + jwtTokenValidity))
                .signWith(key)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

8.4 创建 JWT 过滤器

创建 JWT 过滤器用于处理 JWT 认证:

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    private final UserDetailsService userDetailsService;
    private final JwtUtil jwtUtil;

    public JwtRequestFilter(UserDetailsService userDetailsService, JwtUtil jwtUtil) {
        this.userDetailsService = userDetailsService;
        this.jwtUtil = jwtUtil;
    }

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

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(request, response);
    }
}
注意

JWT 令牌一旦签发就无法撤销,除非它们过期。对于需要立即撤销访问权限的场景,可以考虑使用黑名单或短令牌有效期。

9. CSRF 防护

9.1 CSRF 概述

跨站请求伪造 (CSRF) 是一种攻击,其中恶意网站诱使用户的浏览器向用户已认证的网站发送请求。Spring Security 提供了内置的 CSRF 防护机制。

9.2 启用 CSRF 防护

默认情况下,Spring Security 启用 CSRF 防护。如果需要显式配置:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf((csrf) -> csrf
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        );

    return http.build();
}

9.3 在表单中包含 CSRF 令牌

在 Thymeleaf 模板中包含 CSRF 令牌:

<form th:action="@{/login}" method="post">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
    <!-- 其他表单字段 -->
</form>

9.4 禁用 CSRF 防护

对于某些场景(如 REST API),可能需要禁用 CSRF 防护:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .csrf((csrf) -> csrf.disable());

    return http.build();
}
小提示

对于使用 JWT 的无状态 API,通常可以禁用 CSRF 防护,因为每个请求都需要包含有效的 JWT 令牌。

10. 最佳实践

10.1 安全配置最佳实践

10.2 认证最佳实践

10.3 授权最佳实践

10.4 测试安全配置

笔记

安全是一个持续的过程,而不是一次性的任务。定期审查和更新安全配置是保持应用程序安全的关键。

返回首页