API安全加固:OAuth2.0、JWT与防注入实战
008、API安全加固:OAuth2.0、JWT与防注入实战
一、从一次深夜告警说起
上周三凌晨两点,手机突然狂震——监控显示某个内部管理接口的调用量在十分钟内暴涨了500倍。登录服务器一看,日志里全是同一个token在重复调用用户列表接口,请求频率高得离谱。显然,这不是正常业务行为。
紧急排查发现:这个接口虽然用了OAuth2.0做认证,却漏了权限细粒度控制;token是长期有效的,且客户端把token硬编码在了前端代码里。攻击者通过浏览器调试工具轻松拿到token后,直接写脚本疯狂爬取数据。
这件事让我再次意识到:用了安全框架不等于安全。今天我们就聊聊那些在真实企业场景里,让API真正“硬”起来的实战技巧。
二、OAuth2.0:别只停留在“能用”层面
很多团队实现OAuth2.0时,只关心“能不能跑通授权流程”,却忽略了企业场景下的特殊需求。看看这段典型的配置代码:
// 常见的、但问题多多的配置
@Bean
public AuthorizationServerConfigurerAdapter authConfig() {
return new AuthorizationServerConfigurerAdapter() {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("web_app")
.secret("{noop}client_secret_123") // 问题1:弱密码还明文存
.authorizedGrantTypes("password", "refresh_token") // 问题2:password模式直接暴露
.accessTokenValiditySeconds(30 * 24 * 3600); // 问题3:token一个月不过期
}
};
}
这段代码至少有三个坑:
第一坑:password模式滥用
在企业内部系统,除非绝对必要,否则别用password模式。它要求客户端收集用户密码,增加了密码泄露风险。建议用authorization_code模式,或者针对内部服务间调用用client_credentials模式。
第二坑:token生命周期过长
access_token给30天,refresh_token给90天——这是给自己埋雷。我们现在的策略是:access_token最长4小时,refresh_token最多用一次就失效(必须配合refresh_token轮换机制)。
第三坑:客户端凭证管理随意
用inMemory存client_secret、用弱密码、不加密存储……这些低级错误在真实项目里真不少见。至少得这样:
// 改进后的配置片段
.withClient("internal_service")
.secret(passwordEncoder.encode(strongRandomSecret)) // BCrypt加密
.autoApprove(false) // 必须显式授权
.redirectUris("https://internal.company.com/callback") // 严格限制回调地址
.scopes("read_profile", "write_log") // 按需最小化scope
.accessTokenValiditySeconds(2 * 3600); // 2小时过期
三、JWT:别把token当数据库用
JWT最大的优点是“自包含”,但这也是最容易被滥用的地方。见过有人在JWT的payload里塞几十个字段,包括用户权限列表、部门结构、甚至个人偏好设置——token长度直接突破2KB。
// 反面教材:JWT payload过大
{
"sub": "user123",
"name": "张三",
"department": "技术部-后端组-微服务团队",
"roles": ["admin", "editor", "viewer", "auditor", "approver"],
"permissions": ["user:read", "user:write", "log:read", "report:generate", ...],
"preferences": {"theme": "dark", "language": "zh-CN"},
// ... 还有十几个字段
}
问题在哪?
每次请求都携带这么大的token,浪费带宽不说,更关键的是权限信息更新延迟。用户权限变了,得等token过期才能生效,除非你实现一套复杂的token强制失效机制。
我们的经验是:JWT只放身份标识和必要元数据。权限信息走单独的API实时查询,或者用短缓存的权限码。比如:
// 精简后的payload设计
public class JwtPayload {
private String sub; // 用户ID
private String jti; // token唯一标识(用于强制失效)
private Long iat; // 签发时间
private Long exp; // 过期时间
private String aud; // 受众(哪个服务)
private String scope; // 核心权限范围,如"api:read"
// 不!要!塞!权!限!列!表!
}
签名算法选择
别再用HS256(对称加密)了,一旦secret泄露,攻击者可以伪造任意token。用RS256或ES256(非对称加密),私钥签名、公钥验证,私钥严格保存在服务端安全存储里。
四、防注入:参数校验别只靠框架
Spring Validation用起来很顺手,但容易让人产生虚假安全感。看这个例子:
@PostMapping("/users")
public User createUser(@Valid @RequestBody UserCreateRequest request) {
// 看起来安全,其实还有漏洞
return userService.create(request);
}
@Data
public class UserCreateRequest {
@NotBlank
@Size(min=2, max=20)
private String username; // 好像校验了长度
@Email
private String email; // 格式校验有了
}
漏了什么?
- 没过滤特殊字符(比如username里带HTML标签)
- 没做业务逻辑校验(比如username是否已被占用)
- 没防批量注册(单个IP短时间内创建大量用户)
我们现在的做法是三层校验链:
// 第一层:基础格式校验(用注解)
// 第二层:业务规则校验(在Service里)
public User createUser(UserCreateRequest request) {
// 防脚本注入
if (containsScriptTags(request.getUsername())) {
throw new IllegalArgument("用户名包含非法字符");
}
// 防业务滥用
rateLimiter.check("user_create", currentIp, 10, 3600); // 1小时最多10次
// 防逻辑漏洞
if (userRepo.existsByUsername(request.getUsername())) {
throw new ConflictException("用户名已存在"); // 别返回"用户已存在",避免信息泄露
}
// 第三层:数据库层面约束(唯一索引、外键等)
return userRepo.save(request.toEntity());
}
特别注意:错误信息别太详细
返回“用户名或密码错误”而不是“密码错误”,返回“请求参数无效”而不是“邮箱格式错误”。避免给攻击者提供信息便利。
五、企业场景的特殊加固技巧
1. 内部API也要认证
别以为内网就安全。我们要求所有内部服务间调用必须带service-account的JWT token,哪怕它们在同一台物理机上。零信任网络,从内部开始。
2. 动态scope机制
标准OAuth2.0的scope是静态配置的,但我们扩展了一套动态scope:根据用户风险等级、登录设备、访问时间等因素,动态调整token的权限范围。比如下班时间访问敏感接口,scope自动缩减。
3. Token绑定策略
重要操作(如转账、删除)的token必须绑定设备指纹或IP段。我们实现了一个简单的token绑定中间件:
@Component
public class TokenBindingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
String token = extractToken(request);
String deviceFingerprint = request.getHeader("X-Device-Fingerprint");
// 从Redis查这个token应该绑定的设备指纹
String expectedFingerprint = redis.get("token_bind:" + token);
if (expectedFingerprint != null &&
!expectedFingerprint.equals(deviceFingerprint)) {
// 设备不匹配,强制token失效
redis.delete("token:" + token); // 加入黑名单
throw new UnauthorizedException("检测到异常设备");
}
chain.doFilter(request, response);
}
}
4. 审计日志必须独立通道
别把操作日志和业务日志混在一起。我们专门有个带独立认证的审计服务,所有敏感操作(登录、删数据、改权限)通过同步调用记录,确保日志不被篡改。
六、个人经验与建议
API安全不是一次性项目,而是持续对抗的过程。几个血泪教训:
关于token存储:前端别存localStorage,用httpOnly的cookie,至少加个secure和sameSite属性。见过太多XSS直接偷localStorage里的token。
关于密钥管理:别把任何密钥、密码硬编码在代码里。用KMS或至少是环境变量。我们吃过亏——某同事把测试环境数据库密码提交到了GitHub公开仓库,虽然半小时内发现并删除,但已经被爬虫抓走了。
关于依赖更新:定期更新安全相关的依赖库。去年那个Spring Security的绕过漏洞(CVE-2022-22978),我们因为及时升级到补丁版本,躲过一劫。隔壁团队晚了三天升级,被扫出漏洞扣了安全分。
关于压力测试:安全测试别只做功能验证。上压力工具模拟高并发场景,有时候认证系统在正常流量下没问题,一到高压就出现token重复使用、缓存击穿导致权限校验绕过。
最后说个心态问题:别追求“绝对安全”,那不存在。我们的目标是让攻击成本高于攻击收益。就像你家防盗门——防不了专业大盗,但能让小偷觉得撬你家不如撬隔壁的。
每次设计API安全方案时,问自己三个问题:
- 这个方案被攻破后,影响范围有多大?(最小化爆炸半径)
- 攻击者需要付出多少成本?(提高攻击门槛)
- 我能否在1小时内发现异常?(加强监控能力)
安全是道高一尺魔高一丈的持久战,保持警惕,持续迭代。共勉。
下一篇预告:009、高并发下的API性能优化:从缓存策略到异步化改造
更多推荐




所有评论(0)