从零开始用Spring Security保护你的Java应用

程序员的日常信号
2025-12-21 06:06
阅读 1311

大家好,我是一名从中文系转行做后端开发的程序员。当初学Java的时候,面对各种框架简直头大,尤其是Spring Security这种听起来就很高大上的安全框架。记得第一次看到Authentication、Authorization这些英文单词时,我还以为要学密码学了!但其实,Spring Security并没有想象中那么难。今天我就用最通俗的语言,手把手带你搭建一个基础的安全认证系统。

为什么要学Spring Security?

在我们开发Web应用时,总会遇到这样的需求:有些页面只有登录用户才能访问,管理员能看到普通用户看不到的功能,用户的密码需要安全存储等等。这些都是应用安全的范畴。

Spring Security就是Spring生态中专门用来处理这些安全问题的框架。它能帮你轻松实现:

  • 用户登录认证
  • 权限控制(谁能访问什么资源)
  • 密码加密存储
  • 防止常见的安全攻击(比如CSRF)

虽然现在区块链技术很火,很多同学都想直接学区块链开发,但我建议先打好Web开发的基础。因为即使是区块链应用的后台管理系统,也需要用到这些基础的安全机制。技术学习要循序渐进,先把地基打牢!

开发环境准备

必备工具清单

工具 版本要求 说明
JDK 11或更高 Java开发环境
Maven 3.6+ 项目依赖管理
IDE IntelliJ IDEA或VS Code 代码编辑器
Spring Boot 2.7.x或3.x 快速开发框架

创建项目

我推荐使用Spring Initializr来快速创建项目。访问 https://start.spring.io,选择以下配置:

  • Project: Maven Project
  • Language: Java
  • Spring Boot: 2.7.18 (稳定版本)
  • Dependencies:
    • Spring Web
    • Spring Security
    • Spring Boot DevTools (开发时自动重启)

点击"Generate"下载项目压缩包,解压后用IDE打开即可。

如果你喜欢命令行,也可以这样创建:

# 使用Spring Boot CLI(如果已安装)
spring init --dependencies=web,security my-security-app

# 或者手动创建pom.xml

项目结构预览

创建完成后,你会看到这样的基本结构:

my-security-app/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/example/demo/
│       │       └── DemoApplication.java
│       └── resources/
│           └── application.properties
└── pom.xml

核心概念扫盲

在我刚开始学的时候,最困惑的就是这些专业术语。让我用大白话解释一下:

认证(Authentication)vs 授权(Authorization)

  • 认证 = "你是谁?" → 验证用户名密码是否正确
  • 授权 = "你能做什么?" → 验证你有没有权限访问某个功能

比如你去银行:

  • 柜员让你出示身份证和银行卡 → 这是认证
  • 柜员确认你能取自己的钱但不能取别人的 → 这是授权

Security的核心组件

  1. SecurityFilterChain: 安全过滤器链,所有请求都要经过它检查
  2. UserDetailsService: 负责从数据库或其他地方加载用户信息
  3. PasswordEncoder: 密码加密器,确保密码安全存储
  4. AuthenticationManager: 认证管理器,负责验证用户身份

默认的安全行为

有趣的是,只要你添加了Spring Security依赖,什么都不配置也会有安全保护!启动应用后访问任何页面,都会跳转到一个自动生成的登录页。

默认用户名是 user,密码会在控制台输出,类似这样:

Using generated security password: 8d9e3a1b-4c5d-4e6f-7g8h-9i0j1k2l3m4n

这个特性让我当初吓了一跳,以为程序出bug了,后来才知道这是Spring Security的"约定优于配置"哲学。

实战:搭建基础认证系统

现在让我们一步步构建一个简单的用户认证系统。

第一步:基础配置

创建一个配置类 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.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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @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();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("alice")
            .password("password123")
            .roles("USER")
            .build();
        
        UserDetails admin = User.withDefaultPasswordEncoder()
            .username("bob")
            .password("admin123")
            .roles("ADMIN", "USER")
            .build();

        return new InMemoryUserDetailsManager(user, admin);
    }
}

关键点解释:

  • @EnableWebSecurity 启用Web安全功能
  • authorizeHttpRequests() 配置哪些路径需要认证
  • InMemoryUserDetailsManager 在内存中存储用户(仅用于演示!)

第二步:创建控制器

创建 HomeController.java

package com.example.demo.controller;

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

@Controller
public class HomeController {

    @GetMapping("/public/home")
    @ResponseBody
    public String publicHome() {
        return "这是公共主页,无需登录";
    }

    @GetMapping("/user/dashboard")
    @ResponseBody
    public String userDashboard() {
        return "欢迎来到用户仪表板!";
    }

    @GetMapping("/admin/panel")
    @ResponseBody
    public String adminPanel() {
        return "管理员专属面板";
    }

    @GetMapping("/login")
    public String login() {
        return "login"; // 返回登录页面模板名
    }
}

第三步:创建登录页面

src/main/resources/templates/ 目录下创建 login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>登录</title>
    <meta charset="UTF-8">
</head>
<body>
    <h2>用户登录</h2>
    
    <!-- 登录失败时显示错误信息 -->
    <div th:if="${param.error}">
        <p style="color:red;">用户名或密码错误!</p>
    </div>
    
    <!-- 登录成功后的提示 -->
    <div th:if="${param.logout}">
        <p>您已成功登出!</p>
    </div>

    <form th:action="@{/login}" method="post">
        <div>
            <label>用户名:</label>
            <input type="text" name="username" required/>
        </div>
        <div>
            <label>密码:</label>
            <input type="password" name="password" required/>
        </div>
        <div>
            <button type="submit">登录</button>
        </div>
    </form>
</body>
</html>

别忘了在 pom.xml 中添加Thymeleaf依赖:

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

第四步:测试我们的系统

启动应用后,按以下顺序测试:

  1. 访问 http://localhost:8080/public/home → 应该直接显示内容
  2. 访问 http://localhost:8080/user/dashboard → 跳转到登录页
  3. 用用户名 alice,密码 password123 登录 → 成功进入仪表板
  4. 访问 http://localhost:8080/admin/panel → 应该被拒绝(因为我们还没配置权限)

第五步:添加基于角色的权限控制

修改 SecurityConfig.java 中的 filterChain 方法:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authz -> authz
            .requestMatchers("/public/**").permitAll()
            .requestMatchers("/admin/**").hasRole("ADMIN")    // 只有ADMIN角色能访问
            .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") // USER或ADMIN都能访问
            .anyRequest().authenticated()
        )
        .formLogin(form -> form
            .loginPage("/login")
            .permitAll()
        )
        .logout(logout -> logout
            .permitAll()
        );
    return http.build();
}

注意: Spring Security会自动给角色加前缀 ROLE_,所以配置时写 hasRole("ADMIN"),实际检查的是 ROLE_ADMIN

现在用 alice 用户尝试访问 /admin/panel 会被拒绝,而用 bob 用户就可以正常访问。

常见问题与解决方案

问题1:为什么登录总是失败?

可能原因:

  • 密码没有正确加密
  • 用户名/密码输入错误
  • CSRF令牌问题

解决方案: 确保使用 PasswordEncoder。上面的例子用了 User.withDefaultPasswordEncoder(),但在生产环境中应该显式配置:

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

// 创建用户时
UserDetails user = User.builder()
    .username("alice")
    .password(passwordEncoder().encode("password123"))
    .roles("USER")
    .build();

问题2:如何禁用CSRF保护?

在开发API时,可能需要禁用CSRF(但生产环境不建议):

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable()) // 禁用CSRF
        // ... 其他配置
    return http.build();
}

问题3:如何自定义用户来源(比如从数据库)?

创建自定义的 UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        
        return User.builder()
            .username(user.getUsername())
            .password(user.getPassword()) // 应该是已加密的密码
            .roles(user.getRoles().toArray(new String[0]))
            .build();
    }
}

然后在配置类中注入:

@Autowired
private CustomUserDetailsService customUserDetailsService;

@Bean
public UserDetailsService userDetailsService() {
    return customUserDetailsService;
}

问题4:静态资源(CSS/JS)被拦截怎么办?

authorizeHttpRequests 中添加:

.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()

或者更通用的方式:

.requestMatchers(StaticResourceRequest.toCommonLocations()).permitAll()

学习建议与下一步

我踩过的坑

  1. 不要在生产环境使用内存用户存储 - 我当初为了省事在demo里用内存存储,结果上线后用户数据全丢了!
  2. 密码一定要加密 - 明文存储密码是重大安全漏洞
  3. 理解过滤器链的顺序 - Spring Security是基于Filter的,了解执行顺序很重要

推荐学习路径

  1. 基础巩固:先掌握Spring Boot和基本的Web开发
  2. 深入Security:学习JWT认证、OAuth2、方法级安全等高级特性
  3. 实战项目:做一个完整的博客系统,包含用户注册、登录、权限管理
  4. 安全最佳实践:学习HTTPS、CORS配置、安全头设置等

关于区块链的思考

虽然这篇文章主要讲Spring Security,但我想说:安全是所有应用的基础,包括区块链。即使你未来想从事区块链开发,也需要理解传统的Web安全机制。很多区块链项目的管理后台、钱包服务都离不开这些基础的安全框架。

而且,区块链本身解决的是分布式信任问题,但应用层的安全(比如用户认证、API保护)仍然需要传统安全技术来保障。所以不要觉得学这些"老技术"没用,它们是构建可靠系统的基石。

扩展阅读建议

  • Spring Security官方文档(虽然是英文,但例子很清晰)
  • 《Spring Security实战》这本书(适合入门后深入学习)
  • OWASP Top 10(了解常见的Web安全漏洞)

结语

回想起我当初从文科生转码的经历,最大的感悟就是:技术没有想象中那么可怕,关键是要动手实践。Spring Security看起来复杂,但只要按照本文的步骤一步步来,你很快就能掌握基础用法。

记住,每个优秀的程序员都是从"Hello World"开始的。不要害怕犯错,遇到问题就查文档、看源码、问社区。编程的世界很友好,只要你愿意学习。

现在就去创建你的第一个安全应用吧!如果有任何问题,欢迎在评论区讨论。技术分享的意义就在于互相帮助,共同进步。

祝你编码愉快!

评论 0

最热最新
暂无评论
程序员的日常信号Lv.1
0
影响力
0
文章
0
粉丝