快速上手 Spring Security:国企程序员的认证实战笔记

何桂英
2026-05-30 05:43
阅读 3292

上周五下午,阳光正好,咖啡刚续上,我正盘算着周末要不要去城郊新开的露营基地——结果产品经理小王带着他标志性的“就改一点点”表情走过来:“咱们后台系统得加个登录权限控制,下周上线。”

我当时差点把 Mac 上的咖啡打翻。要知道,在我们这种节奏温和、双休雷打不动的国企 IT 部门,突然冒出“下周上线”的需求,简直比让运维老李主动写文档还罕见。

不过吐槽归吐槽,活儿总得干。好在咱也不是第一天搬砖了。三年前刚来的时候,连 Maven 都配不明白;现在嘛,虽然头发少了点,但至少知道该用 Spring Security 而不是自己手搓一套 token 验证逻辑。

说到这个,最近不是在悄悄学 AI 吗?什么 Fine-tuning、LoRA、Lovable 模型这些词天天刷屏。我寻思着,搞安全认证其实也像模型训练——你得先有个基础骨架(Spring Security),再根据业务场景微调(Fine-tuning),最后让它变得“可亲可爱”(Lovable),别动不动就 403 Forbidden 把用户吓跑。

今天这篇,就是我在 deadline 压力下快速搭起一套认证系统的全过程,希望能帮到同样被临时需求砸中的兄弟们。


为什么不用 Shiro?因为 Spring Boot 太香了

公司老项目确实有几套用 Apache Shiro 的,配置文件写得比我毕业论文还长。但新项目基本都上了 Spring Boot,天然和 Spring Security 集成得丝滑如德芙。

而且,咱们开发环境全用 Mac(测试同事才用 Windows 跑 IE11 兼容模式,每次看他开虚拟机我都想笑),Maven + IntelliJ + JDK17 的组合,搭 Security 简直一键起飞。

核心目标很简单:

  • 用户登录后才能访问 /api/admin/**
  • 登录失败要友好提示,别直接白页
  • 支持 JSON 格式登录(前端小妹说她不想拼 form 表单)
  • 别把密码明文存数据库!

第一步:依赖拉满,但别拉垮

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId> <!-- 开发用 -->
</dependency>

别小看这四个依赖,Security 自动配置已经帮你拦住了所有接口。启动项目,访问任意路径,立马跳到 /login 页面——但这是 Thymeleaf 模板,我们前后端分离,不需要它。

所以,第一步就得关掉默认行为。


关键配置:让 Security 听你的话

创建 SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable() // 前后端分离,用 JWT 或 Session 管理,CSRF 可关
            .formLogin().disable() // 不用表单登录
            .httpBasic().disable() // 也不用 Basic Auth
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/auth/login").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(sess -> sess
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint((req, res, ex) -> {
                    res.setStatus(401);
                    res.getWriter().write("{\"error\":\"请先登录\"}");
                })
                .accessDeniedHandler((req, res, ex) -> {
                    res.setStatus(403);
                    res.getWriter().write("{\"error\":\"权限不足\"}");
                })
            );
        return http.build();
    }

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

这段代码干了啥?

  • 关掉 CSRF、Form、Basic —— 我们要自己处理登录
  • 放行 /api/auth/login 和公开接口
  • 所有其他请求必须认证
  • 自定义 401/403 返回 JSON,而不是重定向或空白页
  • 密码用 BCrypt 加密,别再 MD5 了兄弟!

这时候启动项目,访问 /api/admin/users,会返回:

{"error":"请先登录"}

完美!前端终于不用猜是网络错误还是没登录了。


实现登录接口:别让用户等太久

接下来写登录 Controller。这里有个坑:很多人直接用 AuthenticationManager.authenticate(),但如果你没暴露这个 Bean,会报错:

Consider defining a bean of type 'org.springframework.security.authentication.AuthenticationManager'

所以得手动注入:

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final UserService userService;

    public AuthController(AuthenticationManager authenticationManager, UserService userService) {
        this.authenticationManager = authenticationManager;
        this.userService = userService;
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest req) {
        try {
            UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword());
            Authentication auth = authenticationManager.authenticate(token);
            
            // 登录成功,生成 Session 或 Token
            SecurityContextHolder.getContext().setAuthentication(auth);
            
            return ResponseEntity.ok(Map.of("message", "登录成功", "username", auth.getName()));
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(401).body(Map.of("error", "用户名或密码错误"));
        }
    }
}

同时,别忘了配置 AuthenticationManager Bean:

@Bean
public AuthenticationManager authenticationManager(
        AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
}

UserDetailsService 也要实现:

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private UserRepository userRepo;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepo.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
        return org.springframework.security.core.userdetails.User
            .withUsername(user.getUsername())
            .password(user.getPassword()) // 已加密
            .authorities("ROLE_USER") // 简化,实际可查权限表
            .build();
    }
}

数据库设计也很简单:

字段 类型 说明
id BIGINT 主键
username VARCHAR(50) 唯一
password VARCHAR(100) BCrypt 加密后
enabled BOOLEAN 是否启用

插入测试用户时,记得先加密:

User user = new User();
user.setUsername("admin");
user.setPassword(passwordEncoder.encode("123456"));
userRepo.save(user);

测试一把:Postman 走起

  1. POST /api/auth/login with { "username": "admin", "password": "123456" } → 返回 200
  2. 再访问 /api/admin/users → 成功返回数据
  3. 换个浏览器或清除 Cookie → 访问 /api/admin/users → 返回 401

搞定!整个过程不到两小时,周五下班前还能赶上去露营。


性能与生产建议

虽然我们是国企,流量不大,但该注意的还得注意:

  • Session 管理:如果用户量大,考虑用 Redis 存 Session,避免集群 session 丢失
  • 密码策略:上线前务必加复杂度校验(长度、特殊字符等)
  • 日志监控:记录登录失败次数,防暴力破解
  • HTTPS:生产环境必须上,别让密码在公网裸奔

另外,别学我一开始图省事把 @EnableWebSecurity 忘了,结果 Security 压根没生效,测了一晚上以为是前端问题……当时真的想砸电脑。


Fine-tuning 安全策略:让它更“Lovable”

回到开头说的 AI 术语——Security 也需要 Fine-tuning。

比如,默认的 DaoAuthenticationProvider 只支持用户名密码。但业务可能需要手机号+验证码登录、微信扫码登录。这时候你就得自定义 AuthenticationProvider,甚至扩展 Authentication 类型。

又比如,某些接口需要“二级验证”,登录后还要输短信验证码。这可以通过自定义 AccessDecisionVoter 或结合 Spring 的 @PreAuthorize 实现。

而“Lovable”体现在哪儿?用户体验。

  • 登录失败提示“密码错误”而不是“认证失败”(别暴露系统细节)
  • 登出后自动清除客户端 token
  • 会话超时前弹窗提醒“是否继续操作”
  • 权限不足时,引导用户联系管理员开通,而不是冷冰冰的 403

这些细节,才是让一个安全系统从“能用”变成“好用”的关键。

至于 Go?咳,隔壁组新来的小伙天天吹 Go 性能高,还说用 Gin + Casbin 三行代码搞定 RBAC。但我看了看,学习成本不低,而且我们 Java 生态稳如老狗,暂时没必要折腾。不过嘛……既然我在考虑换环境,说不定下家公司真用 Go 呢?先收藏几个教程压压惊。


写在最后

在国企做开发,节奏慢是事实,但技术不能停。这次快速搭起 Security 系统,既满足了业务需求,也巩固了基础知识。更重要的是,让我意识到:安全不是功能,而是体验的一部分

下次再有人问“你们系统安全吗?”,我可以笑着回答:“不仅安全,还很 Lovable。”

对了,露营回来发现 Mac 被雨淋了……还好代码都提交了。程序员的世界,bug 可以有,commit 不能丢。

作者:某双休不加班国企程序员,三年 Java 老兵,正在偷偷学习 AI 和 Go,准备换个战场。
个人信条:代码可以糙,但生活必须精致——咖啡要手冲,键盘要静音,周末绝不回消息。

评论 0

最热最新
暂无评论
何桂英Lv.1
0
影响力
0
文章
0
粉丝