主流的安全设计
概述
WEB系统的安全设计没有唯一标准,更多是根据系统形态选择合适的方案。传统网页、前后端分离、App、小程序、内部服务,它们面对的客户端不同,登录态保存方式也不一样。
我在项目里一般先判断三个问题:客户端是什么、服务端是否保存会话、权限是否需要细到资源级别。把这三个问题弄清楚,安全方案基本就能定下来。
Cookie和Session
Cookie加Session是传统WEB应用最常见的方案。用户登录成功以后,服务器创建Session,把Session ID写到浏览器Cookie里。浏览器后续请求会自动带上Cookie,服务器根据Session ID找回用户。
这个方案的优点是成熟、简单、和浏览器天然配合。缺点也很明显,服务器需要保存会话,集群部署时要考虑Session共享。
常见处理方式有三种:
- 使用单机Session,适合小系统。
- 使用Redis集中保存Session,适合集群部署。
- 使用粘性Session,让同一用户请求固定落到同一台机器。
Spring Security默认就很适合这种方式。只要使用表单登录,登录成功以后会自动维护SecurityContext和Session。
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/login", "/assets/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.build();
}
如果你做的是服务端渲染后台,这种方案仍然很值得使用。
Token
Token方案常见于前后端分离、App和小程序。用户登录成功以后,服务端返回一个Token,客户端自己保存。后续请求通过HTTP Header携带Token。
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
服务端收到请求以后,先解析Token,再把用户身份放进SecurityContext。这种方式不依赖Cookie,也不要求服务端保存Session。
它适合这些场景:
- 后端只提供API。
- 客户端不只有浏览器。
- 服务需要水平扩展。
- 登录态不希望和服务器内存绑定。
Token方案也不是没有代价。服务端不保存会话以后,Token一旦签发,在过期前通常都有效。如果要支持主动退出、封禁用户、踢下线,就要额外设计黑名单、版本号或者短Token加刷新Token。
JWT
JWT是Token方案里最常见的一种格式。它把Header、Payload、Signature组合成一个字符串,服务端通过签名判断内容是否可信。
JWT适合保存少量非敏感信息,比如用户ID、租户ID、权限版本、过期时间。不要把手机号、身份证、密码、微信session_key这类敏感信息放进去,因为JWT默认只是签名,不是加密。
比较稳妥的Payload可以像这样:
{
"sub": "10001",
"tenant_id": "zhouzhou",
"role": "USER",
"token_version": 3,
"iat": 1710000000,
"exp": 1710003600
}
sub用来表示用户主体,exp表示过期时间,token_version可以用来做强制失效。用户修改密码、管理员封禁账号时,只要提升数据库里的版本号,旧Token就会失效。
OAuth2
OAuth2解决的是第三方授权问题。它不是单纯的登录协议,而是让一个应用在用户授权后,代表用户访问另一个系统的资源。
比如“使用微信登录”经常会被叫成OAuth2登录,但微信小程序登录流程并不是标准网页OAuth2。它更像是客户端拿code,服务端用code向微信换openid和session_key,再由自己的系统签发登录态。
标准OAuth2更常见的场景是:
- 使用GitHub登录。
- 使用企业微信或飞书登录后台。
- 自己的系统给第三方应用开放API。
- 微服务之间通过授权服务器发放访问令牌。
如果只是自己的App和自己的后端通讯,不一定要引入完整OAuth2。JWT加自己的登录接口通常就够了。
API Key
API Key适合服务端到服务端的访问,比如开放平台、内部系统回调、第三方系统推送消息。
客户端请求时带上固定Key:
X-API-Key: app_xxx
服务端根据Key识别调用方,再判断调用方能访问哪些接口。
API Key实现简单,但安全性依赖管理方式。Key要能轮换,要能禁用,最好不要长期暴露在前端代码里。对于重要接口,可以再加上时间戳和签名,避免请求被截获后重复使用。
RBAC
RBAC是基于角色的访问控制。它的核心是用户拥有角色,角色拥有权限。
用户 -> 角色 -> 权限
比如:
张三 -> 管理员 -> 用户查看、用户编辑、订单查看
李四 -> 运营 -> 订单查看、商品编辑
RBAC适合大多数后台系统。角色让权限分配更方便,权限让系统控制更细。
Spring Security里可以直接使用角色和权限:
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long userId) {
// 删除用户
}
@PreAuthorize("hasAuthority('order:read')")
public OrderDetail getOrder(Long orderId) {
return orderService.getOrder(orderId);
}
角色和权限不要混在数据库里随便写。我的习惯是角色用ROLE_前缀,权限用资源:动作格式,比如user:read、order:refund。
ABAC
ABAC是基于属性的访问控制。它不仅看用户角色,还会看用户属性、资源属性和环境属性。
比如:
- 只能查看自己创建的订单。
- 只能操作同一租户的数据。
- 只能在工作时间发起审批。
- 只能下载自己部门的文件。
这种规则很难只靠角色表达。角色只说明用户是谁,属性才能说明当前资源和当前场景是否匹配。
在Spring项目里,可以把粗粒度权限放在注解上,把资源级判断放到业务服务里。
@PreAuthorize("hasAuthority('order:read')")
public OrderDetail getOrder(Long orderId) {
OrderDetail order = orderService.getOrder(orderId);
Long currentUserId = SecurityUtils.currentUserId();
if (!order.ownerId().equals(currentUserId)) {
throw new AccessDeniedException("不能访问其他人的订单");
}
return order;
}
这样做的好处是边界清楚。接口入口先判断有没有读订单的权限,业务内部再判断是不是这条订单的拥有者。
常见组合
实际项目一般不会只用一种方案,而是组合使用。
| 项目形态 | 常见方案 | 说明 |
|---|---|---|
| 服务端后台 | Cookie + Session + RBAC | 开发成本低,适合传统管理后台 |
| 前后端分离 | JWT + RBAC | 后端无状态,前端自己保存Token |
| App和小程序 | 第三方登录 + JWT | 平台负责身份入口,业务系统签发自己的Token |
| 开放平台 | API Key + 签名 | 识别调用方,同时防止请求被篡改 |
| 多租户系统 | JWT + RBAC + ABAC | Token保存租户,权限和数据范围分开判断 |
我的建议是先用最少的方案覆盖主要入口。不要为了“标准”引入一套很重的授权服务器,也不要为了方便把所有接口都只写一个ROLE_USER。方案要跟着业务复杂度一起长大。
作者: 舟哥
链接:https://www.b919p4.com
来源: B919P4
本文原创发布于B919P4,©著作权归作者所有,转载请注明出处,谢谢合作!