快速上手 Spring Security:国企程序员的认证实战笔记
上周五下午,阳光正好,咖啡刚续上,我正盘算着周末要不要去城郊新开的露营基地——结果产品经理小王带着他标志性的“就改一点点”表情走过来:“咱们后台系统得加个登录权限控制,下周上线。”
我当时差点把 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 走起
- POST
/api/auth/loginwith{ "username": "admin", "password": "123456" }→ 返回 200 - 再访问
/api/admin/users→ 成功返回数据 - 换个浏览器或清除 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