Spring Security实战-构建安全的Web应用

Spring Security是Spring生态中处理身份验证与授权的核心框架,基于Servlet过滤器链实现灵活安全控制。其核心组件包括UserDetails(用户身份凭证)、UserDetailsService(用户数据加载)、AuthenticationManager(认证管理)及各类过滤器(如登录、授权等)。

作者头像
LumiBee
46 天前 · 121 0
分享

Spring Security 作为 Spring 生态系统中不可或缺的一员,提供了一套全面且可扩展的机制来处理身份验证和授权。本文将结合实际应用场景,深入剖析 Spring Security 的核心组件和工作流程,特别是如何确保用户在各种登录方式下都能获得一致且正确的个性化信息展示,并进一步扩展到框架的整体架构和高级应用,系统地掌握这一强大的安全框架。

架构概览

​ Spring Security 的核心是基于 Servlet 过滤器 (Servlet Filters) 的。当一个请求到达应用时,它会经过一个过滤器链 (Filter Chain)。Spring Security 将其安全特性作为一系列过滤器插入到这个链中。每个过滤器都有特定的职责,例如:

  • 处理身份验证(如 UsernamePasswordAuthenticationFilter 用于表单登录,OAuth2LoginAuthenticationFilter 用于 OAuth2 登录)。
  • 处理授权决策(如 AuthorizationFilter)。
  • 管理安全上下文(如 SecurityContextPersistenceFilter)。
  • 处理“记住我”功能(如 RememberMeAuthenticationFilter)。
  • 处理会话管理(如 SessionManagementFilter)。

这种基于过滤器的架构提供了极大的灵活性和可扩展性。

核心安全概念与组件

UserDetails 接口:Spring Security 的用户“身份证”

​ 在Spring Security中,org.springframework.security.core.userdetails.UserDetails 接口扮演着至关重要的角色。可以把它想象成一张标准化的用户“身份证”,Spring Security通过这张“身份证”来了解用户的核心认证和授权信息。

​ 任何代表应用中用户的类(设为User类),如果想被Spring Security的认证机制所管理和识别,通常都需要实现这个接口。通过让 User 类实现 UserDetails 接口,告诉Spring Security:“嘿,我的 User 对象可以直接用作认证主体(Principal)!你可以直接从它身上获取用户名、密码、权限等信息。”

UserDetails 接口中的关键方法及其在 User 中的实现

  • String getUsername()
    • 作用:返回用于认证的唯一用户名。这个用户名是Spring Security内部识别用户的主要标识。
    • 重要性
      • 登录凭证:当用户通过表单提交用户名/密码时,Spring Security会用提交的用户名来调用 UserDetailsServiceloadUserByUsername() 方法。
      • “记住我”关联persistent_logins 表(用于“记住我”功能)中存储的 username 字段,必须与此方法返回的值完全一致。这样,当“记住我”服务尝试通过token重新认证用户时,才能正确找到对应的用户记录。
      • Principal 对象的核心标识:认证成功后,Authentication 对象中的 Principal(通常是 UserDetails 实例)的 getUsername() 方法返回的就是这个值。
  • String getPassword()
    • 作用:返回用户存储在数据库中的加密后的密码
    • 重要性:Spring Security的 AuthenticationManager(通常通过 DaoAuthenticationProvider)会使用这个密码与用户登录时提交的密码(经过同样的加密算法处理后)进行比较,以验证用户身份。对于OAuth2用户或密码未设置的用户,此字段可能为 null
  • Collection<? extends GrantedAuthority> getAuthorities()
    • 作用:返回授予用户的权限集合(例如角色)。GrantedAuthority 是Spring Security中表示权限的接口。
    • 重要性:这些权限用于授权决策,即判断用户是否有权访问某个资源或执行某个操作。如果您使用基于角色的访问控制(如 @PreAuthorize("hasRole('ADMIN')")),就需要在这里正确返回用户的角色(如 List.of(new SimpleGrantedAuthority("ROLE_USER")))。目前返回空列表意味着用户没有任何特定的声明权限,但仍然是已认证用户。
  • 账户状态方法:
    • boolean isAccountNonExpired(): 账户是否未过期。
    • boolean isAccountNonLocked(): 账户是否未被锁定。
    • boolean isCredentialsNonExpired(): 用户凭证(密码)是否未过期。
    • boolean isEnabled(): 账户是否启用。
    • 作用:这些方法允许Spring Security在认证过程中检查用户的账户状态。如果任何一个方法返回 false,用户将无法通过认证(即使密码正确)。

UserDetailsService:用户的“档案管理员”

org.springframework.security.core.userdetails.UserDetailsService 接口只有一个核心方法:UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

  • 作用:它的职责是根据传入的用户名从持久化存储(如数据库)中查找并加载用户数据,然后返回一个 UserDetails 对象。

  • 与 User 的关系

    // 示例:CustomUserServiceImpl.java
    @Service
    public class CustomUserServiceImpl implements UserDetailsService {
        @Autowired
        private UserRepository userRepository; // 用户数据仓库
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 
            User user = userRepository.findByName(username)
                    .orElseThrow(() -> new UsernameNotFoundException("用户未找到,邮箱: " + username));
            // 这里可以进行额外的用户状态检查,例如用户是否被软删除等
            return user; // 返回实现了 UserDetails 的自定义 User 对象
        }
    }
    
    • CustomUserServiceImpl 中的 loadUserByUsername(String email) 方法会查询数据库,找到对应的 User记录。
    • 关键点:由于User 类现在直接实现了 UserDetails 接口,所以 loadUserByUsername 方法可以直接返回这个从数据库查出来的 User 对象实例。
    • 为什么这很重要? 当这个自定义的 User 对象(它既是一个领域模型,也是一个 UserDetails)被设置为Spring Security认证成功后的 Principal 时,就可以在Thymeleaf模板中通过 #authentication.principal 直接访问到 User 类中定义的所有自定义字段(如 avatarUrl, name, bio 等),而不仅仅是 UserDetails接口定义的标准字段。

Authentication: 用户的“会话通行证”

  • SecurityContextHolder:这是 Spring Security 用来存储当前安全上下文信息的“安全信息保险柜”。默认情况下,它使用 ThreadLocal 策略来为每个请求线程提供独立的存储空间,确保不同用户之间的安全信息互不干扰 。

  • SecurityContext:这是从保险柜(SecurityContextHolder)中取出的“当前用户的档案文件夹” 。它直接包含了 Authentication 对象 。

  • Authentication:这是档案文件夹中那份“已验证的身份凭证” 。它代表了当前已认证用户的信息,通常包含 Principal(用户主体信息)、Credentials(凭证,如密码,在认证成功后通常会被清除)和 Authorities(授予用户的权限列表) 。

AuthenticationManager: 认证逻辑的“安全总管”

  • AuthenticationManager:认证流程的“安全总管”,是认证的核心接口 。它的职责就是验证一份身份凭证(Authentication 对象)是否有效 。

  • ProviderManager:安全总管手下的“认证主管”。作为 AuthenticationManager 最常见的实现,它自己不执行具体的验证工作,而是将任务委托给一个或多个 AuthenticationProvider 实例 。

  • AuthenticationProvider:执行实际认证逻辑的“一线安检员” 。每个安检员都精通一种特定凭证的验证方式。例如,DaoAuthenticationProvider 就像一个专门负责核对用户名和密码的安检员,它会调用 UserDetailsService 来获取用户记录,并使用 PasswordEncoder来比较密码 。也可以添加自定义的安检员来支持其他认证机制(如短信验证码、社交媒体登录等)

认证流程:签发“通行证”的过程

  1. 用户尝试进行需要认证的操作,例如提交登录表单。

  2. 相应的认证过滤器(如 UsernamePasswordAuthenticationFilter)从请求中提取认证信息(如用户名和密码),并创建一个(未认证的)Authentication 对象(创建一个未经过安全总管认证的通行证)(例如 UsernamePasswordAuthenticationToken)。

  3. 过滤器将这个 Authentication 对象传递给 AuthenticationManager 进行认证。

  4. AuthenticationManager(通常是 ProviderManager)遍历其配置的 AuthenticationProvider 列表。

  5. 每个 AuthenticationProvider 检查它是否支持传入的 Authentication 对象类型。

    • 如果支持,它会尝试认证。例如,DaoAuthenticationProvider 会调用 UserDetailsService 加载 UserDetails,然后使用 PasswordEncoder 比较提供的密码和存储的密码。
  6. 如果认证成功,AuthenticationProvider 返回一个完全填充的(已认证的)Authentication 对象,其中包含 Principal、Authorities 等信息。

  7. 这个已认证的 Authentication 对象被设置到 SecurityContextHolder 中的 SecurityContext 里(放到保险柜中)。

  8. 之后,应用程序可以通过 SecurityContextHolder.getContext().getAuthentication() 来获取当前用户的信息。

“记住我”功能

  1. 用户登录时勾选“记住我”,Spring Security将 username(来自 UserDetails.getUsername())和生成的token存入 persistent_logins 表。
  2. 在应用重启或会话过期后,用户再次访问。
  3. RememberMeAuthenticationFilter 检测到浏览器发送的“记住我”cookie。
  4. PersistentTokenBasedRememberMeServices 从cookie中提取 username和token系列号,去 persistent_logins 表中验证token。
  5. 如果token有效,它会使用该 username调用 CustomUserServiceImpl 的 loadUserByUsername(String na me)方法(之前给出的例子)并返回一个实现了UserDetails的User对象。
  6. Spring Security 创建一个新的 RememberMeAuthenticationToken,并将这个 User 对象设置为其 Principal,然后填充到SecurityContextHolder,用户即被视为已认证。
  7. Thymeleaf 渲染页面时,#authentication.principal 就是这个包含所有自定义字段的 User 对象,因此就会正确显示头像、昵称等。

Thymeleaf 与 Spring Security 集成

  • sec:authorize="isAuthenticated()" / sec:authorize="isAnonymous()"
    • 这些属性用于根据当前用户的认证状态条件性地渲染HTML块。isAuthenticated() 表示用户已登录,isAnonymous() 表示用户未登录。
  • #authentication 对象
    • 在Thymeleaf中,可以通过 #authentication 这个表达式对象访问到当前Spring Security的 Authentication 对象。
    • Authentication 对象包含了关于当前认证会话的所有信息。
  • #authentication.principal
    • 这会返回 Authentication 对象中的 Principal(认证主体)。
    • 核心连接点:如果 UserDetailsService (即 CustomUserServiceImpl)(还是之前的例子)返回的是自定义的、实现了 UserDetails 接口的 User对象,那么 #authentication.principal 在模板中就会是这个 User 对象的实例。
    • 因此,可以这样访问自定义属性:
      • th:src="${#authentication.principal.getAvatarUrl()}"
      • th:text="${#authentication.principal.getName()}"

session.user VS #authentication.principal

  • session.user
    • 这是自定义登录成功处理器中手动放入HTTP会话的一个属性。
    • 问题:它只在用户通过表单或OAuth2初次登录,并且执行了自定义成功处理器时才会被设置。当通过“记住我”自动登录时,这些成功处理器不会被调用,因此 session.user不会被设置,也就是会导致自动登录失效。
  • #authentication.principal (通过Spring Security的Thymeleaf集成)
    • 它直接从Spring Security的 SecurityContextHolder 中获取当前的认证主体。
    • 优势:无论用户是通过哪种方式(表单、OAuth2、“记住我”)认证成功的,只要Spring Security成功建立了认证上下文,#authentication.principal 就能反映当前的认证用户。

关键配置

PasswordEncoder

核心职责:PasswordEncoder 的核心使命是确保用户密码的存储安全。它通过复杂的单向加密算法(哈希算法)将用户的明文密码转换成一串看似随机的字符(哈希值)。“单向”意味着从哈希值几乎不可能反推出原始密码。

**配置要点:**必须声明PasswordEncoder Bean,选择强算法:

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

并在用户注册和验证时使用

HttpSecurity

  • 核心职责:HttpSecurity 是 Spring Security 配置的核心入口,提供流畅的 API (DSL) 来声明式地定义应用的整体安全策略。

  • 你能用它做什么

    • URL 授权 (permitAll(), authenticated(), hasRole(), hasAuthority())

    • 登录配置 (自定义登录页、处理URL、成功/失败跳转)

    • 登出配置 (处理URL、成功跳转、清除Cookie、Session失效)

    • “记住我”功能

    • CSRF 保护

    • 会话管理

    • CORS 配置

    • HTTP Header 安全配置

    • 集成自定义过滤器

  • “规则手册”的比喻:详细规定了应用的各项安全规则,如哪些门开放,哪些需要特定凭证等。

  • 现代Lambda DSL:使得配置更简洁易读:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/public/**", "/css/**", "/js/**", "/login").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .loginProcessingUrl("/perform_login")
                .defaultSuccessUrl("/home", true)
                .failureUrl("/login?error=true")
                .permitAll()
            )
            .oauth2Login(oauth2 -> oauth2
                .loginPage("/login")
                .successHandler(customOAuth2SuccessHandler)
             )
            .logout(logout -> logout
                .logoutUrl("/perform_logout")
                .logoutSuccessUrl("/login?logout")
                .deleteCookies("JSESSIONID", "remember-me") // 清除关键Cookie
                .invalidateHttpSession(true) // 使HTTP Session失效
                .permitAll()
            )
            .rememberMe(remember -> remember // 配置“记住我”
                .key("aUniqueAndSecretKeyForRememberMe") // 用于签名和验证令牌的密钥
                .tokenValiditySeconds(86400 * 14) // 令牌有效期,例如14天
                // .userDetailsService(userDetailsService) // 通常Spring会自动发现
            )
          	.csrf(csrf -> csrf        				  	 .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 
// 允许 JavaScript 读取 CSRF token cookie
         		.csrfTokenRequestHandler(new CsrfTokenRequestAttributeNameResolver()) // 或者自定义处理器
          	);
        return http.build();
    }
}

CSRF(Cross-Site Request Forgery)

​ CSRF 是一种常见的 Web 攻击,攻击者诱导受害者在已登录的 Web 应用中执行非本意的操作。

  • 原理:攻击者在恶意网站上放置一个链接或表单,当受害者(已登录目标网站)点击或加载该页面时,浏览器会自动携带目标网站的 Cookie(包含会话信息)向目标网站发起请求,执行恶意操作(如转账、修改密码)。
  • Spring Security 防御机制(同步器令牌模式)
  1. 令牌生成与存储:当用户访问一个包含表单的页面时,Spring Security 生成一个随机的、唯一的 CSRF 令牌。

    • 此令牌存储在服务器端的 HttpSession 中。

    • 同时,此令牌会通过某种方式传递给客户端,通常是作为 HTML 表单中的一个隐藏字段,或者在 AJAX 请求中通过 meta 标签或自定义响应头获取。

  2. 令牌验证:当客户端提交一个会改变状态的请求(如 POST, PUT, DELETE)时,必须将此 CSRF 令牌一并发送回服务器(作为请求参数或请求头)。

  3. 服务器在收到请求后,会比较请求中携带的 CSRF 令牌与 HttpSession 中存储的 CSRF 令牌。

    • 如果两者一致,请求被认为是合法的。

    • 如果不一致或请求中没有令牌,请求被拒绝。

  • 配置 HttpSecurity 中的 CSRF

    具体的配置见上文HttpSecurity部分

    • 默认开启:Spring Security 默认启用 CSRF 保护。

    • 与 Thymeleaf 集成:如果使用 Thymeleaf等模板引擎,并且表单使用 @ ,CSRF 令牌会自动作为隐藏字段添加到表单中。

    • SPA/AJAX 应用

      • 通常需要前端 JavaScript 从某个地方(如初始页面加载时服务器通过 Cookie 或 meta 标签提供的令牌)获取 CSRF 令牌。

      • 然后在每次发送 AJAX 请求(特别是 POST, PUT, DELETE)时,将令牌添加到请求头中(通常是 X-CSRF-TOKEN)。

      • Spring Security 可以配置为从 Cookie (CookieCsrfTokenRepository) 或请求头中读取 CSRF 令牌。

  • 禁用 CSRF (csrf -> csrf.disable()):

    • 不推荐在生产环境中对有状态应用(基于 Session 的应用)禁用 CSRF。

    • 对于某些无状态的 API(例如,完全基于 Token 认证如 JWT,并且不依赖 Cookie 进行会话管理的 API),可以考虑禁用 CSRF,因为这类 API 通常不受传统 CSRF 攻击的影响。但仍需谨慎评估。

阅读量: 121

评论区

登录后发表评论

正在加载评论...
相关阅读

一个草稿箱功能的实现

# **从零到一:一个“自动保存草稿”功能的实现** 在现代Web应用中,用户体验至上。没有什么比用户在精心编辑长篇内容后,因意外关闭浏览器或网络问题而丢失所有心血更令人沮丧的了(一位站友就...

119
1

Mac 快速安装使用 Elasticsearch

在 Mac 上安装并使用 Elasticsearch 主要有两种方法:一种是使用 Homebrew 包管理器,另一种是直接从官网下载压缩包进行手动安装 # 方法一:使用 Homebrew 安装...

115
0