Spring Security基础:快速搭建安全认证系统
凌晨1点27分,窗外的县城已经彻底安静下来,只有偶尔传来几声狗叫。我泡了杯速溶咖啡(别笑,小镇做题家哪有手冲的条件),盯着屏幕上的Spring Boot项目,心想:这玩意儿要是能像Go写HTTP服务那样简单就好了。
在这家公司待了三年多,从一个只会System.out.println的菜鸟,到现在能被叫一声“后端主力”,说实话,成长了不少。但最近总觉得有点卡住——技术栈太稳了,稳到我都快睡着了。产品需求永远是“加个按钮”、“改个字段”,连运维都懒得搭理我们这种小县城远程团队,反正“你们自己搞定就行”。
上周五晚上,产品经理突然在企业微信上@我:“下个月要上线新模块,得加登录和权限控制,最好下周给个Demo。” 我盯着消息看了三秒,心里默默翻了个白眼:你当我是AI吗?不过转念一想,也好,正好趁这个机会把拖了半年的Spring Security捡起来。毕竟,跳槽简历上总不能只写“熟练使用MyBatis”吧?
为啥不用Go?或者…区块链?
先说清楚,这篇文章讲的是Spring Security,不是Go,也不是区块链。但既然要求提到,那我就坦白交代一下我的内心戏。
前阵子确实折腾过用Go写认证服务——Gin + JWT,跑得飞快,内存占用不到Java的三分之一。但问题来了:公司老系统全是Spring全家桶,MySQL + Redis + RabbitMQ那一套,突然插个Go服务进去,运维大哥直接发语音吼我:“你让K8s怎么管?日志怎么统一?监控告警谁对接?” 得,打住。
至于区块链……咳,去年双11期间,领导不知从哪听来的风,说“用户数据要上链才安全”。我当时差点把键盘砸了——我们做的就是一个后台管理系统,用户量还没小区业主群人多,上什么链?最后不了了之。不过这事让我意识到:安全不是堆概念,而是解决真实问题。比如——别让实习生删了生产库。
所以,回归现实:用Spring Security,稳,熟,团队都能维护。
需求场景:一个再普通不过的后台系统
我们要做的,其实就三件事:
- 用户登录(用户名+密码)
- 登录后访问受保护接口
- 不同角色看到不同菜单(RBAC)
听起来简单?但上次我手写过滤器+Token验证,结果因为没处理CSRF,被测试小妹提了个高危漏洞。那次线上事故后,我发誓:认证授权这种事,别自己造轮子。
Spring Security虽然配置有点“魔法”,但人家是经过千锤百炼的。而且,它和Spring Boot整合得贼丝滑,自动配置能省掉80%的样板代码。
实战:从零搭建,不踩坑是不可能的
第一步:依赖安排上
<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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
注意:别忘了加spring-boot-starter-web!我第一次就漏了,启动直接报错,还以为Security有问题。
第二步:最简配置(内存用户)
为了快速验证,先搞个内存用户:
@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.builder()
.username("admin")
.password("{bcrypt}$2a$10$...") // 这里用BCrypt加密后的密码
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
}
这时候访问 /,会自动跳转到 /login 页面——Spring Security自带的登录页虽然丑,但能用!
血泪教训:密码一定要用
{bcrypt}前缀!否则默认用DelegatingPasswordEncoder会报There is no PasswordEncoder mapped for the id "null"。我第一次直接写明文密码,调试半小时才发现是加密格式问题。
第三步:接入数据库(这才是生产该有的样子)
内存用户只能玩玩,真上生产必须用数据库。表结构建议如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| username | VARCHAR(50) | 唯一 |
| password | VARCHAR(100) | BCrypt加密后存储 |
| enabled | TINYINT | 是否启用(防逻辑删除) |
对应的User实体:
@Entity
@Table(name = "sys_user")
public class SysUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private Boolean enabled = true;
}
然后自定义UserDetailsService:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 数据库里已经是BCrypt加密过的
.roles("USER") // 这里可以查role表动态赋值
.build();
}
}
注意:不要在数据库存明文密码!注册或修改密码时,记得用BCryptPasswordEncoder加密:
@Autowired
private PasswordEncoder passwordEncoder;
// 注册时
user.setPassword(passwordEncoder.encode(rawPassword));
接口设计 & Token方案
虽然Spring Security默认用Session,但前后端分离项目一般用JWT。这里我选择保留Session——原因很简单:我们前端是Vue+ElementUI,部署在同一域名下,Cookie自动携带,省心。
但如果你们是纯API服务(比如给App提供接口),那就要上JWT了。思路是:
- 登录接口返回Token
- 后续请求Header带
Authorization: Bearer <token> - 自定义
OncePerRequestFilter解析Token并设置SecurityContext
不过这次项目没这个需求,我就没折腾。稳定压倒一切,这是我在这小县城悟出的道理。
权限控制:方法级注解真香
光有登录不够,还得控制谁能干啥。Spring Security的方法级安全简直神器:
@RestController
public class OrderController {
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/orders/{id}")
public Result deleteOrder(@PathVariable Long id) {
// 只有ADMIN能删订单
orderService.delete(id);
return Result.success();
}
@PreAuthorize("authentication.name == #userId")
@GetMapping("/users/{userId}/profile")
public UserProfile getProfile(@PathVariable String userId) {
// 只能看自己的资料
return userService.getProfile(userId);
}
}
开启方法安全只需一行注解:
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
}
吐槽时间:测试小妹第一次看到
@PreAuthorize("authentication.name == #userId")时,惊呼“这还能这么写?!” 其实Spring EL表达式支持很多操作,甚至能调用自定义Bean方法,灵活到飞起。
性能 & 运维经验
虽然Spring Security很重,但在我们这种日活几百的小系统里,完全无感。不过还是有几个生产经验分享:
- 缓存UserDetails:如果用户信息不常变,可以用
@Cacheable缓存loadUserByUsername的结果,减少DB压力。 - 日志打全:登录失败、权限拒绝等事件,一定要记录日志,方便审计。可以用
AuthenticationFailureHandler和AccessDeniedHandler定制。 - CSRF别关:除非你是纯API服务,否则千万别关CSRF防护!我们之前关了,结果被扫出XSS+CSRF组合拳漏洞。
- 密码策略:至少8位,包含大小写+数字。别让用户设
123456!
最后:小镇做题家的思考
折腾完这套认证系统,花了两个晚上。虽然不算复杂,但比手写过滤器靠谱多了。更重要的是,我不再怕“安全”这个词了。
有人说,县城程序员接触不到高并发、大数据,技术会落后。但我觉得,能把基础打牢,把安全、事务、异常处理这些细节做好,比盲目追新更重要。Go再快,也得有人维护;区块链再火,也得解决实际问题。
下周我要去面试一家远程公司,JD里写着“熟悉Spring Security优先”。呵,这下稳了。
对了,如果你也在小城市coding,欢迎留言交流。说不定哪天,我们能在GitHub上互相点个star呢。
附:常用配置速查表
| 配置项 | 说明 | 示例 |
|---|---|---|
permitAll() |
允许匿名访问 | .requestMatchers("/api/public/**").permitAll() |
authenticated() |
需要认证 | .anyRequest().authenticated() |
hasRole('ADMIN') |
需要ADMIN角色 | @PreAuthorize("hasRole('ADMIN')") |
hasAuthority('order:delete') |
需要具体权限 | 更细粒度控制 |
rememberMe() |
记住我功能 | .rememberMe().tokenValiditySeconds(86400) |
好了,咖啡凉了,该睡觉了。明天还要改产品提的“紧急需求”——他说要把登录页背景换成渐变色。

评论 0