安全的基本概念
概述
安全不是一个单独的功能点,而是一套围绕身份、权限、数据和边界建立起来的系统。写WEB项目的时候,很多问题看起来是业务问题,最后都会落到安全上,比如用户是谁、能访问什么、数据有没有被篡改、接口是不是被别人反复调用。
在Spring Security里,最常接触的两个词是Authentication和Authorization。前者解决“你是谁”,后者解决“你能做什么”。这两个概念分清以后,后面的JWT、小程序登录、接口鉴权都会顺很多。
Authentication
Authentication通常翻译成认证,也有人叫鉴权。它的重点是识别当前请求背后的用户身份。
常见认证方式有很多:
- 用户名和密码登录。
- 手机号加验证码登录。
- 微信、支付宝等第三方登录。
- HTTP Basic Auth。
- Cookie和Session。
- Bearer Token。
这些方式看起来差别很大,本质上都是把一段用户提交的凭证,转换成一个应用内部能理解的用户身份。
在Spring Security里,这个身份通常会被包装成Authentication对象。它里面会包含用户主体、凭证、权限和认证状态。
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
boolean authenticated = authentication.isAuthenticated();
这段代码就是从当前线程上下文中取出已经认证过的用户。只要请求进入业务代码之前完成了认证,后面的Controller、Service就可以从这里拿到用户身份。
Authorization
Authorization通常翻译成授权或访问控制。用户已经知道是谁了,接下来就要判断这个用户能不能访问某个资源。
比如同样是/admin/users这个接口:
- 游客不能访问。
- 普通用户不能访问。
- 管理员可以访问。
- 超级管理员可以删除用户。
这就是访问控制。
Spring Security里的权限一般用GrantedAuthority表示。最常见的写法是角色和权限混合使用:
List<GrantedAuthority> authorities = List.of(
new SimpleGrantedAuthority("ROLE_ADMIN"),
new SimpleGrantedAuthority("user:read"),
new SimpleGrantedAuthority("user:write")
);
ROLE_ADMIN更像是身份标签,user:read、user:write更像是具体动作。项目小的时候只用角色也可以,项目复杂以后,角色负责分组,权限负责控制具体资源,会更容易维护。
Principal和Credential
Principal是用户主体,Credential是凭证。
以用户名密码登录为例:
- Principal是用户名,或者根据用户名查出来的用户对象。
- Credential是密码。
以JWT访问接口为例:
- Principal可以是JWT里的用户ID。
- Credential是HTTP Header里的那段Token字符串。
凭证只应该用来完成认证,不应该长期保留在上下文中。认证完成以后,系统真正需要的是用户身份和权限,而不是密码、短信验证码或者第三方平台返回的临时code。
SecurityContext
SecurityContext是Spring Security保存当前用户状态的地方。默认情况下,它会和当前请求线程绑定。
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null, authorities);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
这段代码表示:我已经确认当前请求属于这个用户,并且给他分配了对应权限。后面的过滤器和业务代码就会认为当前请求已经通过认证。
在JWT这种无状态系统里,每个请求都要重新解析Token,然后重新放入SecurityContext。请求结束以后,上下文会被清理掉。
Session和Token
Session和Token是WEB系统最常见的两种登录态设计。
Session的思路是服务器保存登录态,浏览器只保存一个Session ID。浏览器每次请求带上这个ID,服务器就能找到对应用户。
Token的思路是客户端保存一段令牌,服务器每次校验这段令牌,校验成功以后就认为用户已经登录。JWT就是Token的一种常见格式。
| 方式 | 登录态保存位置 | 服务端压力 | 适合场景 |
|---|---|---|---|
| Session | 服务端 | 需要保存会话 | 后台管理系统、传统网页 |
| Token | 客户端 | 不保存会话 | 前后端分离、App、小程序、纯API服务 |
如果项目主要提供API,我一般会优先考虑Token。如果项目是传统服务端页面,Session仍然是简单且稳定的方案。
密码和加密
密码不能明文保存,也不要自己发明加密算法。Spring Security默认推荐使用BCryptPasswordEncoder,它会自动处理盐值和多轮哈希。
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
注册时保存密码摘要:
String encodedPassword = passwordEncoder.encode(rawPassword);
登录时比较密码:
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
这里要注意,密码摘要不是加密后的密码,它不能被解密回原文。系统只需要判断用户输入的密码和数据库里的摘要是否匹配。
安全边界
WEB项目里最容易忽略的是边界。很多人只在Controller上做权限判断,但实际系统里还有很多入口。
常见边界有这些:
- HTTP接口。
- 静态资源。
- 定时任务。
- MQ消费者。
- 管理后台。
- 内部RPC接口。
- 文件上传和下载。
Spring Security主要处理HTTP请求链路,其他入口要结合业务自己处理。比如MQ消息里带了用户ID,也不能默认可信,至少要确认消息来源和消息内容没有被篡改。
我的建议
安全模块不要一开始就写得很复杂。先把认证、授权、密码、Token、异常返回这几块跑通,再逐步补上刷新Token、权限模型、限流、防重放这些内容。
系统越复杂,安全越要保持清晰。能在过滤器里做完的事情,不要散落到每个Controller里;能用统一异常返回的地方,不要让每个接口自己拼错误信息。
作者: 舟哥
链接:https://www.b919p4.com
来源: B919P4
本文原创发布于B919P4,©著作权归作者所有,转载请注明出处,谢谢合作!