Skip to content

Spring Security 安全框架详解

1. 概述

1.1 Spring Security简介

Spring Security是一个功能强大且高度可定制的身份认证和访问控制框架。它是保护基于Spring的应用程序的事实标准,提供了全面的安全解决方案,包括认证、授权、防止常见攻击以及与其他安全系统的集成。

1.2 Spring Security的核心特性

  • 强大的认证机制: 支持多种认证方式,如表单认证、HTTP基本认证、OAuth2、LDAP等
  • 细粒度的授权控制: 支持URL级别的访问控制、方法级别的安全、对象级别的安全
  • 全面的安全防护: 提供CSRF保护、会话固定保护、点击劫持防护等安全功能
  • 与Spring框架无缝集成: 利用Spring的依赖注入和AOP机制
  • 丰富的API和扩展点: 提供多种接口和扩展点,便于自定义功能
  • 良好的可测试性: 提供测试工具,便于编写安全相关的单元测试和集成测试

1.3 Spring Security与Apache Shiro对比

特性Spring SecurityApache Shiro
社区活跃度非常活跃,Spring官方支持活跃
学习曲线陡峭,概念较多平缓,API简单直观
与Spring集成完全集成,无缝衔接需要额外配置集成
Web安全功能非常全面,专为Web应用优化通用安全框架,Web只是其中一部分
认证机制丰富,内置多种认证方式相对简单
授权粒度更细,支持方法级、对象级安全相对较粗
会话管理相对较弱强大,独立于容器

2. Spring Security核心架构

2.1 核心组件

SecurityContextHolder

SecurityContextHolder是Spring Security存储认证信息的核心类,它使用ThreadLocal存储当前已认证用户的信息。

java
// 获取当前用户的安全上下文
SecurityContext context = SecurityContextHolder.getContext();

// 获取认证信息
Authentication authentication = context.getAuthentication();

// 检查是否已认证
boolean isAuthenticated = authentication != null && authentication.isAuthenticated() && 
                         !(authentication instanceof AnonymousAuthenticationToken);

// 获取用户名
String username = authentication.getName();

// 获取权限列表
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

Authentication

Authentication接口表示用户的认证信息,包含用户名、密码、权限等信息。

java
public interface Authentication extends Principal, Serializable {
    // 获取权限列表
    Collection<? extends GrantedAuthority> getAuthorities();
    
    // 获取凭证(通常是密码)
    Object getCredentials();
    
    // 获取细节信息(如IP地址、会话ID等)
    Object getDetails();
    
    // 获取主体(通常是用户名)
    Object getPrincipal();
    
    // 检查是否已认证
    boolean isAuthenticated();
    
    // 设置认证状态
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

UserDetailsService

UserDetailsService负责从数据源加载用户信息。

java
public interface UserDetailsService {
    // 根据用户名加载用户信息
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetails

UserDetails接口表示用户的详细信息,是Spring Security内部使用的用户模型。

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

AuthenticationManager

AuthenticationManager是认证的核心接口,负责验证用户的身份。

java
public interface AuthenticationManager {
    // 认证方法,成功返回已认证的Authentication对象,失败抛出异常
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

AccessDecisionManager

AccessDecisionManager负责决定是否允许访问受保护的资源。

java
public interface AccessDecisionManager {
    // 决定是否允许访问
    void decide(Authentication authentication, Object object, 
               Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException;
    
    // 判断是否支持指定的ConfigAttribute
    boolean supports(ConfigAttribute attribute);
    
    // 判断是否支持指定的安全对象类型
    boolean supports(Class<?> clazz);
}

2.2 安全过滤器链

Spring Security通过一系列的过滤器来实现各种安全功能,这些过滤器组成了安全过滤器链。

主要的过滤器包括:

  1. SecurityContextPersistenceFilter: 负责从Session中加载和存储SecurityContext
  2. UsernamePasswordAuthenticationFilter: 处理表单登录认证
  3. BasicAuthenticationFilter: 处理HTTP基本认证
  4. ExceptionTranslationFilter: 处理认证和授权过程中的异常
  5. FilterSecurityInterceptor: 基于URL的访问控制

3. 认证(Authentication)详解

3.1 认证流程

  1. 用户提交认证信息(通常是用户名和密码)
  2. Spring Security创建一个UsernamePasswordAuthenticationToken对象
  3. 将令牌传递给AuthenticationManager进行认证
  4. AuthenticationManager委托给AuthenticationProvider执行具体的认证逻辑
  5. 认证成功后,AuthenticationProvider返回一个已认证的Authentication对象
  6. 将已认证的Authentication对象设置到SecurityContext
  7. SecurityContext保存到SecurityContextHolder和Session中(如果配置了会话管理)

3.2 自定义用户认证

自定义UserDetailsService

java
@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库中查找用户
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
        
        // 创建权限列表
        List<GrantedAuthority> authorities = user.getRoles().stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
            .collect(Collectors.toList());
        
        // 构建并返回UserDetails对象
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            user.isEnabled(),
            user.isAccountNonExpired(),
            user.isCredentialsNonExpired(),
            user.isAccountNonLocked(),
            authorities
        );
    }
}

自定义AuthenticationProvider

java
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    
    @Autowired
    private CustomUserDetailsService userDetailsService;
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();
        
        // 加载用户信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        
        // 验证密码
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("密码错误");
        }
        
        // 创建已认证的Authentication对象
        return new UsernamePasswordAuthenticationToken(
            userDetails, 
            password, 
            userDetails.getAuthorities()
        );
    }
    
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

3.3 密码编码

Spring Security推荐使用加密算法存储密码,提供了多种密码编码器。

java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用BCrypt加密算法
        return new BCryptPasswordEncoder();
        
        // 或使用其他加密算法
        // return new Argon2PasswordEncoder();
        // return new Pbkdf2PasswordEncoder();
        // return new SCryptPasswordEncoder();
    }
}

4. 授权(Authorization)详解

4.1 授权的基本概念

  • 角色(Role): 表示用户的身份,如ADMIN、USER等
  • 权限(Permission): 表示用户可以执行的操作,如READ、WRITE等
  • 安全表达式(Security Expression): 用于表达授权规则的表达式语言

4.2 URL级别的安全配置

java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/", "/home", "/public/**").permitAll() // 允许所有人访问
            .antMatchers("/admin/**").hasRole("ADMIN") // 需要ADMIN角色
            .antMatchers("/user/**").hasAnyRole("ADMIN", "USER") // 需要ADMIN或USER角色
            .antMatchers("/api/**").hasAuthority("API_ACCESS") // 需要API_ACCESS权限
            .anyRequest().authenticated() // 其他所有请求都需要认证
            .and()
            .formLogin() // 配置表单登录
                .loginPage("/login") // 自定义登录页面
                .permitAll() // 允许所有人访问登录页面
            .and()
            .logout() // 配置登出
                .permitAll(); // 允许所有人访问登出功能
    }
}

4.3 方法级别的安全控制

使用注解进行方法级安全控制

java
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class MethodSecurityConfig {
    // 配置方法级安全
}

@Service
public class UserService {
    
    // 使用@PreAuthorize注解,在方法执行前进行授权检查
    @PreAuthorize("hasRole('ADMIN')")
    public List<User> findAllUsers() {
        // 实现代码
    }
    
    // 使用@PostAuthorize注解,在方法执行后进行授权检查
    @PostAuthorize("returnObject.username == authentication.name or hasRole('ADMIN')")
    public User findUserById(Long id) {
        // 实现代码
    }
    
    // 使用@PreFilter注解,过滤集合参数
    @PreFilter("filterObject.owner == authentication.name or hasRole('ADMIN')")
    public void updateUsers(List<User> users) {
        // 实现代码
    }
    
    // 使用@PostFilter注解,过滤返回的集合
    @PostFilter("filterObject.owner == authentication.name or hasRole('ADMIN')")
    public List<User> findUsersByCondition(Condition condition) {
        // 实现代码
    }
    
    // 使用@Secured注解
    @Secured("ROLE_ADMIN")
    public void deleteUser(Long id) {
        // 实现代码
    }
    
    // 使用@RolesAllowed注解(JSR-250)
    @RolesAllowed({"ROLE_ADMIN", "ROLE_MANAGER"})
    public void createUser(User user) {
        // 实现代码
    }
}

4.4 自定义安全表达式

java
@Component("customSecurityExpression")
public class CustomSecurityExpression {
    
    public boolean hasPermission(Authentication authentication, Object target, Object permission) {
        // 自定义权限检查逻辑
        // 参数authentication: 当前用户的认证信息
        // 参数target: 目标资源
        // 参数permission: 权限字符串
        
        // 示例实现
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        String username = userDetails.getUsername();
        
        // 从数据库或其他存储中检查用户是否对目标资源有指定权限
        // ...
        
        return true; // 假设权限检查通过
    }
}

@Service
public class DocumentService {
    
    @PreAuthorize("@customSecurityExpression.hasPermission(authentication, #documentId, 'READ')")
    public Document readDocument(Long documentId) {
        // 实现代码
    }
    
    @PreAuthorize("@customSecurityExpression.hasPermission(authentication, #documentId, 'WRITE')")
    public void updateDocument(Long documentId, Document document) {
        // 实现代码
    }
}

5. Spring Security与Spring Boot集成

5.1 Maven依赖

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

5.2 Spring Boot安全配置

java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private CustomUserDetailsService userDetailsService;
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home", "/css/**", "/js/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .permitAll()
                .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login?logout")
                .permitAll()
                .and()
            .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

5.3 使用Spring Security自带的登录页面

如果不需要自定义登录页面,可以使用Spring Security提供的默认登录页面。

java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin(); // 使用默认的登录页面
    }
}

5.4 自定义登录页面

java
@Controller
public class LoginController {
    
    @GetMapping("/login")
    public String login() {
        return "login";
    }
}

// Thymeleaf模板示例 (login.html)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <h2>Login</h2>
    <div th:if="${param.error}">
        <p>Invalid username and password.</p>
    </div>
    <div th:if="${param.logout}">
        <p>You have been logged out.</p>
    </div>
    <form th:action="@{/login}" method="post">
        <div>
            <label for="username">Username:</label>
            <input type="text" id="username" name="username" required />
        </div>
        <div>
            <label for="password">Password:</label>
            <input type="password" id="password" name="password" required />
        </div>
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
        <button type="submit">Login</button>
    </form>
</body>
</html>

6. CSRF保护

6.1 CSRF保护概述

CSRF(Cross-Site Request Forgery,跨站请求伪造)是一种常见的Web攻击方式,攻击者诱导用户在已认证的Web应用上执行非预期的操作。Spring Security默认启用了CSRF保护。

6.2 在表单中包含CSRF令牌

html
<form th:action="@{/transfer}" method="post">
    <!-- 包含CSRF令牌 -->
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
    
    <!-- 表单字段 -->
    <div>
        <label for="amount">Amount:</label>
        <input type="text" id="amount" name="amount" />
    </div>
    <div>
        <label for="recipient">Recipient:</label>
        <input type="text" id="recipient" name="recipient" />
    </div>
    <button type="submit">Transfer</button>
</form>

6.3 在Ajax请求中包含CSRF令牌

javascript
// 从cookie中获取CSRF令牌
function getCSRFToken() {
    const cookieValue = document.cookie
        .split('; ')  
        .find(row => row.startsWith('XSRF-TOKEN='))
        ?.split('=')[1];
    return cookieValue ? decodeURIComponent(cookieValue) : null;
}

// 发送Ajax请求
$.ajax({
    url: '/api/data',
    type: 'POST',
    beforeSend: function(xhr) {
        // 设置CSRF令牌
        xhr.setRequestHeader('X-XSRF-TOKEN', getCSRFToken());
    },
    data: JSON.stringify({name: 'test'}),
    contentType: 'application/json',
    success: function(response) {
        console.log('Success:', response);
    },
    error: function(xhr, status, error) {
        console.error('Error:', error);
    }
});

6.4 配置CSRF保护

java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 配置CSRF保护
            .csrf()
                // 使用Cookie存储CSRF令牌
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                // 排除某些URL不进行CSRF保护(如API端点)
                .ignoringAntMatchers("/api/public/**");
    }
}

7. 会话管理

7.1 会话超时配置

java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .sessionManagement()
                // 设置会话超时时间(秒)
                .invalidSessionUrl("/login?sessionExpired") // 会话过期后重定向的URL
                .maximumSessions(1) // 每个用户最多允许的会话数
                .expiredUrl("/login?expired"); // 当会话被其他登录过期时重定向的URL
    }
    
    // 配置会话监听
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
}

7.2 会话固定保护

会话固定攻击是一种攻击方式,攻击者预先创建一个会话,然后诱导用户使用该会话登录。Spring Security默认启用了会话固定保护。

java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .sessionManagement()
                // 配置会话固定保护策略
                .sessionFixation()
                    // 登录后更改会话ID
                    .newSession() // 或使用migrateSession()
                    // 或者禁用会话固定保护(不推荐)
                    // .none();
    }
}

8. OAuth2支持

8.1 OAuth2客户端配置

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
java
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/login**", "/error**").permitAll()
                .anyRequest().authenticated()
                .and()
            .oauth2Login()
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard");
    }
}

8.2 应用配置文件

yaml
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: your-google-client-id
            client-secret: your-google-client-secret
            scope:
              - email
              - profile
          github:
            client-id: your-github-client-id
            client-secret: your-github-client-secret
            scope:
              - user:email
              - read:user

9. LDAP集成

9.1 LDAP依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>

9.2 LDAP配置

java
@Configuration
@EnableWebSecurity
public class LdapSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.ldapAuthentication()
            .userDnPatterns("uid={0},ou=people") // 用户DN模式
            .groupSearchBase("ou=groups") // 组搜索基础DN
            .contextSource()
                .url("ldap://localhost:8389/dc=springframework,dc=org") // LDAP服务器URL
                .and()
            .passwordCompare()
                .passwordEncoder(new BCryptPasswordEncoder()) // 密码编码器
                .passwordAttribute("userPassword"); // 密码属性名
    }
}

9.3 应用配置文件

yaml
spring:
  ldap:
    urls: ldap://localhost:8389
    base: dc=springframework,dc=org
    username: cn=admin,dc=springframework,dc=org
    password: secret

10. Spring Security测试

10.1 测试依赖

xml
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

10.2 使用@WithMockUser注解

java
@SpringBootTest
@AutoConfigureMockMvc
public class SecurityTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    @WithMockUser(username = "user", roles = {"USER"})
    public void whenUserAccessUserPage_thenOk() throws Exception {
        mockMvc.perform(get("/user/page"))
            .andExpect(status().isOk());
    }
    
    @Test
    @WithMockUser(username = "user", roles = {"USER"})
    public void whenUserAccessAdminPage_thenForbidden() throws Exception {
        mockMvc.perform(get("/admin/page"))
            .andExpect(status().isForbidden());
    }
    
    @Test
    @WithMockUser(username = "admin", roles = {"ADMIN"})
    public void whenAdminAccessAdminPage_thenOk() throws Exception {
        mockMvc.perform(get("/admin/page"))
            .andExpect(status().isOk());
    }
}

10.3 使用@WithUserDetails注解

java
@SpringBootTest
@AutoConfigureMockMvc
public class UserDetailsTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    @WithUserDetails("admin")
    public void whenUsingRealUserDetails_thenAdminCanAccessAdminPage() throws Exception {
        mockMvc.perform(get("/admin/page"))
            .andExpect(status().isOk());
    }
}

11. 最佳实践

11.1 安全配置建议

  1. 使用强密码策略: 使用安全的密码编码器,如BCrypt、Argon2等
  2. 启用CSRF保护: 特别是对于修改数据的请求
  3. 限制会话数量: 防止会话固定攻击
  4. 配置安全头: 使用Spring Security的安全头配置
  5. 使用HTTPS: 保护传输中的数据
  6. 实现适当的认证机制: 根据应用需求选择合适的认证方式
  7. 使用最小权限原则: 用户只被授予完成任务所需的最小权限
  8. 定期更新依赖: 确保使用的Spring Security版本没有已知的安全漏洞

11.2 性能优化建议

  1. 使用缓存: 缓存用户认证信息和权限数据
  2. 合理配置会话超时: 避免过长的会话超时时间
  3. 优化安全表达式: 避免在安全表达式中执行复杂的计算
  4. 使用异步认证: 对于耗时的认证过程,考虑使用异步处理
  5. 配置适当的并发会话控制: 根据应用需求调整最大并发会话数

12. 常见问题与解决方案

12.1 认证相关问题

  • 认证失败: 检查用户名、密码、账户状态等
  • 无法登录: 检查登录页面配置、CSRF保护、过滤器链等
  • 记住我功能不工作: 检查remember-me配置和Cookie设置

12.2 授权相关问题

  • 权限不足: 检查用户角色和权限配置
  • 安全表达式不生效: 确保已启用方法级安全注解
  • URL访问控制不生效: 检查URL模式配置顺序和精确性

12.3 会话相关问题

  • 会话过期: 检查会话超时配置
  • 会话固定攻击: 确保启用了会话固定保护
  • 并发会话控制问题: 检查并发会话配置和HttpSessionEventPublisher

13. 总结

Spring Security是一个功能全面、高度可定制的安全框架,为基于Spring的应用程序提供了强大的安全保障。它支持多种认证和授权机制,可以与各种安全系统集成,提供了丰富的API和扩展点。通过正确配置和使用Spring Security,开发人员可以构建安全、可靠的Web应用程序,有效防止常见的安全漏洞和攻击。在实际应用中,我们应该根据具体需求,合理配置Spring Security的各项功能,并遵循安全最佳实践,确保应用的安全性。

基于 MIT 许可发布