Spring Security从零开始:手把手搭建你的第一个安全认证系统

周涛
2025-12-26 10:42
阅读 1484

大家好,我是一名开源项目维护者,也经常给团队新人做后端技术培训。在过去的几年里,我发现很多刚接触Java Web开发的同学,在面对Spring Security时总是感到“高不可攀”——文档复杂、概念抽象、配置繁琐。其实,它并没有那么难。

我当初学的时候,也踩过不少坑:明明只是想加个登录功能,结果被一堆AuthenticationManagerUserDetailsServicePasswordEncoder搞得晕头转向。后来我才明白,Spring Security 的核心思想其实非常清晰:保护资源、验证身份、授权访问。

今天这篇教程,就是为完全零基础的你量身打造。我们将用最简单的语言、最实用的代码,一步步搭建一个具备基本用户名密码登录功能的安全系统。你不需要懂加密算法原理,也不需要精通Spring Boot——只要你会写“Hello World”,就能跟上。


为什么我们需要 Spring Security?

想象一下:你开发了一个后台管理系统,任何人都可以直接访问 /admin/users 页面删除用户数据——这显然不行。安全认证(Authentication)和权限控制(Authorization) 是任何真实项目不可或缺的部分。

Spring Security 就是 Spring 生态中专门解决这类问题的框架。它能帮你:

  • 防止未登录用户访问受保护页面
  • 验证用户名和密码是否正确
  • 控制不同角色能访问哪些接口
  • 抵御常见的网络攻击(如 CSRF、会话固定等)

更重要的是,它与 Spring Boot 无缝集成,几行配置就能启动基础保护。


环境准备:5分钟搭好开发环境

在开始编码前,请确保你的电脑已安装以下工具:

工具 版本建议 作用
JDK 17 或 21 Java 运行环境
Maven / Gradle 最新版 项目依赖管理
IDE IntelliJ IDEA / VS Code 代码编辑器

第一步:创建 Spring Boot 项目

打开 start.spring.io,选择:

  • Project: Maven / Gradle(按你习惯)
  • Language: Java
  • Spring Boot: 3.x(推荐 3.2+)
  • Dependencies: 勾选 Spring WebSpring Security

点击 “Generate” 下载 ZIP 包,解压后导入你的 IDE。

💡 提示:如果你使用命令行,也可以通过 Spring Initializr CLI 创建:

curl https://start.spring.io/starter.zip -d dependencies=web,security -d javaVersion=17 -o demo.zip

第二步:验证项目是否运行

src/main/java 下找到主类(如 DemoApplication.java),直接运行。

默认情况下,Spring Security 会自动启用基本认证(Basic Auth)。启动后访问 http://localhost:8080,浏览器会弹出登录框!

用户名是 user,密码会在控制台打印出来,类似:

Using generated security password: a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8

这就是 Spring Security 的“开箱即用”能力——即使你没写一行安全代码,它也已经保护了你的整个应用。


核心概念:三个关键词搞懂 Spring Security

要掌握 Spring Security,只需理解三个核心组件:

1. Authentication(认证)

“你是谁?”

当用户提交用户名和密码,系统需要验证其真实性。Spring Security 通过 AuthenticationManager 来处理这个过程。

2. Authorization(授权)

“你能做什么?”

即使用户身份合法,也要检查他是否有权限访问某个资源。比如普通用户不能删除管理员账户。

3. Security Filter Chain(安全过滤链)

“请求如何被保护?”

所有 HTTP 请求都会经过一系列过滤器(Filter)。Spring Security 在这里拦截请求,决定是否放行。

🧠 类比理解:
把你的应用想象成一栋大楼。

  • 认证 = 门卫查身份证
  • 授权 = 查看你是否有某楼层的门禁卡
  • 过滤链 = 大楼入口的安检通道

实战项目:实现自定义用户名密码登录

现在,我们来替换默认的随机密码,实现自己的用户体系。

步骤一:创建用户实体类

// src/main/java/com/example/demo/User.java
public class User {
    private String username;
    private String password;
    private String role;

    // 构造函数、getter/setter 省略(可用 Lombok 简化)
    public User(String username, String password, String role) {
        this.username = username;
        this.password = password;
        this..role = role;
    }
    
    // getter 方法...
}

步骤二:配置内存中的用户(仅用于演示)

⚠️ 注意:真实项目应从数据库读取用户,但为简化入门,我们先用内存存储。

// src/main/java/com/example/demo/SecurityConfig.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.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        // 创建两个测试用户
        UserDetails admin = org.springframework.security.core.userdetails.User.builder()
            .username("admin")
            .password(passwordEncoder().encode("123456"))
            .roles("ADMIN")
            .build();

        UserDetails user = org.springframework.security.core.userdetails.User.builder()
            .username("user")
            .password(passwordEncoder().encode("123456"))
            .roles("USER")
            .build();

        return new InMemoryUserDetailsManager(admin, user);
    }

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

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/public/**").permitAll()      // 公开路径
                .anyRequest().authenticated()                  // 其他路径需登录
            )
            .formLogin(form -> form                            // 启用表单登录
                .loginPage("/login")                           // 自定义登录页(可选)
                .permitAll()
            )
            .logout(logout -> logout.permitAll());             // 允许登出

        return http.build();
    }
}

关键点解析:

  1. BCryptPasswordEncoder
    Spring Security 不会明文存储密码BCrypt 是一种安全的哈希算法,每次加密结果都不同(因为加了 salt),但能正确验证原密码。这是现代 Web 应用的标准做法。

  2. roles("ADMIN") vs authorities("ROLE_ADMIN")
    Spring Security 内部会自动在角色名前加 ROLE_ 前缀。所以 .roles("ADMIN") 等价于 .authorities("ROLE_ADMIN")

  3. formLogin()
    默认会提供一个朴素的登录页面(路径 /login)。你可以自定义 HTML,但初学者建议先用默认的。

步骤三:创建一个受保护的控制器

// src/main/java/com/example/demo/HelloController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello, authenticated user!";
    }

    @GetMapping("/admin")
    public String admin() {
        return "Welcome, admin!";
    }
}

步骤四:启动并测试

  1. 启动应用
  2. 访问 http://localhost:8080/hello
  3. 跳转到登录页(由 Spring Security 自动生成)
  4. 输入:
    • 用户名:user,密码:123456 → 可访问 /hello
    • 用户名:admin,密码:123456 → 可访问 /hello/admin

✅ 恭喜!你已经实现了基于角色的访问控制。


新手常见问题解答

Q1:为什么密码要用 BCrypt?不能直接存明文吗?

绝对不行! 如果数据库泄露,明文密码会让用户所有账户(很多人多个网站用同一密码)面临风险。BCrypt 是单向哈希算法,无法逆向解密,且计算慢(防暴力破解)。

Q2:登录页太丑了,怎么自定义?

创建 src/main/resources/templates/login.html(需引入 Thymeleaf):

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

然后在 SecurityConfig 中指定:

.formLogin(form -> form.loginPage("/login").permitAll())

Q3:如何从数据库读取用户?

你需要实现自己的 UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) throw new UsernameNotFoundException("User not found");
        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword()) // 已加密
            .roles(user.getRole())
            .build();
    }
}

然后在 SecurityConfig 中注入这个 Bean 即可。

Q4:登录后跳转到哪里?

默认登录成功后跳回最初请求的页面。可通过 .defaultSuccessUrl("/dashboard", true) 强制跳转。


学习建议与下一步

你现在掌握了 Spring Security 的基础骨架。但真实项目远不止于此。我建议你按以下路径深入:

  1. 理解过滤器链
    使用 @EnableWebSecurity(debug = true) 查看请求经过哪些 Filter。

  2. 学习 JWT 无状态认证
    适用于 API 服务(如 Vue/React 前端 + Spring Boot 后端)。

  3. 集成 OAuth2 / 社交登录
    支持微信、GitHub 等第三方登录。

  4. 细粒度权限控制
    使用 @PreAuthorize("hasRole('ADMIN')") 注解控制方法级权限。

  5. 安全加固
    配置 HTTPS、CSP、XSS 防护等。

📌 避坑指南:

  • 不要为了“快速上线”而关闭 CSRF(csrf().disable()),除非你明确知道自己在做什么。
  • 密码永远不要硬编码在代码中,使用配置文件或密钥管理服务。
  • 测试时用内存用户没问题,但上线前务必切换到数据库。

结语

Spring Security 初看复杂,但它的设计哲学非常优雅:通过组合小而专注的组件,构建强大的安全体系。你今天迈出的第一步,正是通往专业后端开发的关键一环。

记住,每一个复杂的系统,都是从一行 HttpSecurity 配置开始的。希望这篇教程能成为你安全开发之路的起点。

如果你觉得有帮助,欢迎在 GitHub 上给我的开源项目点个 Star —— 那里有更多实战案例和最佳实践。Happy coding!

评论 0

最热最新
暂无评论
周涛Lv.1
0
影响力
0
文章
0
粉丝