主流的安全设计

概述

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向微信换openidsession_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:readorder: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。方案要跟着业务复杂度一起长大。

© 舟哥 all right reserved,powered by Gitbook文件修订时间: 2026-07-16 12:50:59
作者: 舟哥
链接:https://www.b919p4.com
来源: B919P4
本文原创发布于B919P4,©著作权归作者所有,转载请注明出处,谢谢合作!

undefined

results matching ""

    No results matching ""