一、核心概念

1.1 登录验证(Authentication)

确认用户身份的过程,通常通过用户名和密码完成。

  • Principal(主体):用户标识(如用户名)。
  • Credentials(凭证):用于验证身份的信息(如密码、Token)。
  • Authentication(认证对象):封装了主体、凭证和权限信息。

1.2 角色控制(Authorization)

在用户通过身份验证后,决定其可以访问哪些资源或执行哪些操作。

  • Role(角色):用户被赋予的权限组(如 ROLE_ADMIN, ROLE_USER)。
  • Authority(权限):更细粒度的操作权限(如 READ_USER, DELETE_POST)。
  • Access Decision(访问决策):基于角色或权限判断是否允许访问。

1.3 关键组件

组件 作用
SecurityFilterChain 定义请求的安全处理流程
UserDetailsService 加载用户信息(用户名、密码、角色)
PasswordEncoder 密码加密与验证
AuthenticationManager 处理认证请求
@EnableWebSecurity 启用 Web 安全配置
@EnableMethodSecurity 启用方法级安全(如 @PreAuthorize

二、详细操作步骤(适合快速实践)

步骤 1:创建 Spring Boot 项目并添加依赖

使用 start.spring.io 创建项目,添加以下依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- 数据库支持(可选) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

步骤 2:创建安全配置类 SecurityConfig.java

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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 // 启用 Spring Security Web 过滤器链
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) // 启用方法级安全注解
public class SecurityConfig {

    /**
     * 配置安全过滤器链
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 配置请求授权规则
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/public/**").permitAll() // 公共资源放行
                .requestMatchers("/admin/**").hasRole("ADMIN") // /admin 路径需要 ADMIN 角色
                .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") // USER 或 ADMIN 可访问
                .requestMatchers("/api/**").authenticated() // API 需要登录
                .anyRequest().authenticated() // 其他所有请求都需要认证
            )
            // 配置表单登录
            .formLogin(form -> form
                .loginPage("/login") // 自定义登录页面路径
                .loginProcessingUrl("/doLogin") // 登录表单提交的 URL(action)
                .defaultSuccessUrl("/dashboard", true) // 登录成功后跳转的默认 URL
                .failureUrl("/login?error") // 登录失败跳转
                .permitAll() // 登录页和登录处理 URL 允许所有人访问
            )
            // 配置注销
            .logout(logout -> logout
                .logoutUrl("/logout") // 注销请求 URL
                .logoutSuccessUrl("/login?logout") // 注销成功后跳转
                .permitAll()
            )
            // 开发阶段可暂时禁用 CSRF(生产环境必须开启)
            .csrf(csrf -> csrf.disable());

        return http.build();
    }

    /**
     * 用户详情服务:使用内存存储用户(仅用于演示)
     * 生产环境应使用数据库实现 UserDetailsService
     */
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
            .username("user")
            .password(passwordEncoder().encode("password123"))
            .roles("USER") // 角色自动添加 ROLE_ 前缀
            .build();

        UserDetails admin = User.builder()
            .username("admin")
            .password(passwordEncoder().encode("admin123"))
            .roles("ADMIN", "USER") // ADMIN 用户也拥有 USER 权限
            .build();

        return new InMemoryUserDetailsManager(user, admin);
    }

    /**
     * 密码编码器:推荐使用 BCrypt
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

步骤 3:创建测试控制器 TestController.java

package com.example.demo.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

    // 公共接口,无需登录
    @GetMapping("/public/info")
    public String publicInfo() {
        return "This is public information.";
    }

    // 需要登录才能访问
    @GetMapping("/user/profile")
    public String userProfile() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return "Hello, " + auth.getName() + "! Your roles: " + auth.getAuthorities();
    }

    // 需要 ADMIN 角色(方法级控制)
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin/stats")
    public String adminStats() {
        return "Admin Dashboard: System Stats";
    }

    // 需要 USER 或 ADMIN 角色
    @GetMapping("/dashboard")
    public String dashboard() {
        return "Welcome to your dashboard!";
    }

    // 演示获取当前用户信息
    @GetMapping("/me")
    public Authentication getCurrentUser() {
        return SecurityContextHolder.getContext().getAuthentication();
    }
}

步骤 4:创建登录页面(可选,使用 Thymeleaf)

添加 Thymeleaf 依赖:

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

创建 src/main/resources/templates/login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
<h2>Login</h2>
<form th:action="@{/doLogin}" method="post">
    <div>
        <label>Username: <input type="text" name="username" required/></label>
    </div>
    <div>
        <label>Password: <input type="password" name="password" required/></label>
    </div>
    <button type="submit">Login</button>
</form>

<!-- 显示错误信息 -->
<div th:if="${param.error}">
    <p style="color:red">Invalid username or password.</p>
</div>
<div th:if="${param.logout}">
    <p style="color:green">You have been logged out.</p>
</div>
</body>
</html>

步骤 5:运行与测试

  1. 启动 Spring Boot 应用。
  2. 访问 http://localhost:8080/public/info → 直接访问成功。
  3. 访问 http://localhost:8080/user/profile → 跳转到 /login
  4. 输入用户名 user,密码 password123 → 登录成功,跳转到 /dashboard
  5. 访问 /admin/stats → 403 Forbidden(user 无 ADMIN 角色)。
  6. 注销后使用 admin/admin123 登录 → 可访问 /admin/stats

三、常见错误与解决方案

错误现象 原因分析 解决方案
提示 There is no PasswordEncoder mapped for the id "null" 密码未使用 {bcrypt} 等前缀,或未配置 PasswordEncoder UserDetailsService 中使用 passwordEncoder().encode() 编码密码
403 Forbidden(权限不足) 角色名称不匹配或缺少权限 检查 hasRole() 参数是否正确;确认用户角色是否包含 ROLE_ 前缀
登录失败但无提示 failureUrl 未配置或前端未处理参数 配置 failureUrl("/login?error") 并在页面显示错误信息
无法访问静态资源(CSS/JS) 安全规则未放行静态资源路径 添加 .requestMatchers("/css/**", "/js/**", "/images/**").permitAll()
@PreAuthorize 注解不生效 未启用 @EnableMethodSecurity 在配置类上添加 @EnableMethodSecurity

四、注意事项

  1. 密码安全
    • 永远不要明文存储密码。
    • 使用 BCryptPasswordEncoder 等强哈希算法。
  2. 角色命名
    • Spring Security 默认要求角色以 ROLE_ 开头。
    • hasRole("ADMIN") 实际检查的是 ROLE_ADMIN
  3. CSRF 保护
    • 生产环境必须开启 CSRF 保护(http.csrf().disable() 仅用于开发)。
    • 表单提交需包含 CSRF Token。
  4. Session 管理
    • 配置合理的 Session 超时时间。
    • 考虑并发登录控制(maximumSessions)。
  5. 内存用户仅用于测试
    • 生产环境应使用数据库、LDAP 或 OAuth2。

五、使用技巧

5.1 获取当前登录用户

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();

5.2 方法级权限控制

// 基于角色
@PreAuthorize("hasRole('ADMIN')")
public void deleteAccount() { ... }

// 基于权限
@PreAuthorize("hasAuthority('DELETE_USER')")
public void deleteUser(Long id) { ... }

// SpEL 表达式
@PreAuthorize("#userId == authentication.principal.id")
public UserData getUserData(Long userId) { ... }

5.3 自定义登录成功处理器

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .formLogin(form -> form
            .successHandler((request, response, authentication) -> {
                // 自定义逻辑:记录登录日志、重定向到个性化页面等
                response.sendRedirect("/home");
            })
        );
    return http.build();
}

六、最佳实践

  1. 分层安全
    • URL 级别控制粗粒度访问。
    • 方法级注解控制细粒度权限。
  2. 最小权限原则
    • 只授予完成任务所需的最小权限。
  3. 使用 HTTPS
    • 所有认证相关通信必须加密。
  4. 审计日志
    • 记录登录、注销、权限变更等关键事件。
  5. 定期更新依赖
    • 及时修复 Spring Security 的安全漏洞。
  6. 避免硬编码
    • 将角色、路径等配置项外化到 application.yml

七、性能优化

  1. 缓存用户信息
    • 对于数据库用户,使用 @Cacheable 缓存 UserDetailsService.loadUserByUsername() 结果。
  2. 减少权限检查开销
    • 避免在循环中进行 @PreAuthorize 检查。
    • 使用角色继承(RoleHierarchy)减少重复配置。
  3. Session 优化
    • 对于 REST API,考虑使用 JWT 实现无状态认证,减少 Session 存储压力。
    • 配置合理的 Session 超时时间。
  4. 异步处理
    • 对于耗时的认证逻辑(如远程调用),可考虑异步处理(需确保线程安全)。

八、扩展:数据库用户认证(JPA)

8.1 创建用户实体

@Entity
@Table(name = "users")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String role; // 如 "ROLE_USER"

    // getters and setters
}

8.2 实现 UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

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

        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRole().replace("ROLE_", "")) // 去除 ROLE_ 前缀
            .build();
    }
}

注意UserRepository 需继承 JpaRepository<UserEntity, Long>


总结

通过以上详细步骤,你已掌握了在 Spring Boot 中配置登录验证和角色控制的核心技能。从基础配置到最佳实践,结合常见问题解决方案,可快速构建安全可靠的应用。

进阶学习建议

  • 集成 Spring Data JPA 实现数据库用户管理。
  • 实现基于 JWT 的无状态认证。
  • 集成 OAuth2 / OpenID Connect(如微信、GitHub 登录)。
  • 学习 Spring Security 的 CSRF、CORS、CSP 等高级安全机制。

本文基于 Spring Boot 3.x + Spring Security 6.x 编写。