文章目录
- 前置知识
- Authorization头部和 Cookie
- CRSF攻击
- JWT概念
- JWT认证流程
- 使用
- Springboot整合JWT
- JwtUtil
- JWT案例
- 控制器
- JWT拦截器
- 注册拦截器
- 结果
- session VS Jwt
前置知识
Authorization头部和 Cookie
Authorization
头部和 Cookie
是 HTTP 协议中两种不同的身份认证 / 信息传递机制.
-
Authorization
头部
是 HTTP 协议中专门用于传递认证信息的请求头,通常格式为:Authorization: <认证方案> <凭证>
常见的认证方案有
Bearer
(用于 JWT、OAuth2.0 令牌)、Basic
(基础认证)等。
核心用途:向服务器证明客户端的身份(如 “我是已登录的用户 XXX”),仅在需要身份验证的请求中使用。 -
Cookie
是服务器通过Set-Cookie
响应头下发给客户端的小型数据片段,客户端(如浏览器)会在后续请求中自动附加到请求头中发送给服务器。
核心用途:不仅用于身份认证(如 Session ID),还可存储其他状态信息(如用户偏好、购物车数据等),是客户端与服务器之间维护 “状态” 的主要方式。
- 自动性:
Cookie
由客户端自动携带,Authorization
需手动设置,这是两者最根本的区别,也导致了 CSRF 风险的差异。 - 安全性:
Cookie
有内置安全属性(HttpOnly
等),Authorization
依赖令牌本身和传输层安全(HTTPS)。 - 灵活性:
Authorization
更适合跨域、长令牌场景,Cookie
适合传统状态维护和轻量数据存储。
CRSF攻击
- 用户登录目标网站 A:用户在浏览器中登录网站 A(如银行网站),网站 A 验证通过后,会在用户的浏览器中生成并存储身份凭证(通常是 Cookie),用于后续请求的身份识别。
- 攻击者诱导用户访问恶意网站 B:用户在未退出网站 A 的情况下,被诱导点击了攻击者精心构造的恶意链接(如邮件、聊天消息中的链接),访问了攻击者控制的网站 B。
- 恶意网站 B 发送伪造请求:网站 B 的页面中包含一段代码(如 JavaScript),会自动向网站 A 的服务器发送一个请求(如转账请求)。由于用户的浏览器中仍保存着网站 A 的登录 Cookie,这个请求会自动携带该 Cookie,让网站 A 误以为是用户本人发起的操作。
- 网站 A 执行未授权操作:网站 A 验证请求中的 Cookie 有效,且未对请求的来源进行严格校验,从而执行了攻击者伪造的操作(如转账给攻击者的账户)。
CSRF 攻击的关键条件
- 用户必须已登录目标网站(即浏览器中存在有效的身份凭证,如 Cookie)。
- 攻击者必须诱导用户在登录状态下访问恶意网站。
- 目标网站未对请求的合法性进行严格校验(如未验证请求来源、未使用 CSRF Token 等防御机制)。****
JWT概念
-
概念:
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
-
构成:
第一部分我们称它为头部(header),
第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),
第三部分是签证(signature).
-
header
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
-
playload
载荷就是存放有效信息的地方。这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
-
signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用
.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。如对称加密:
# 伪代码 signature = HMAC-SHA256(key=secret,message=header_encoded + "." + payload_encoded )
-
注意事项
将这三部分用
.
连接成一个完整的字符串,构成了最终的jwt注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,**在任何场景都不应该流露出去。**一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
JWT认证流程
- 用户登录:
用户提交用户名和密码 → 服务器验证成功 → 服务器生成 JWT(包含用户信息和签名) → 返回 JWT 给客户端。 - 客户端存储:
客户端(如浏览器)将 JWT 存储在localStorage
或Cookie
等中。 - 后续请求:
客户端每次请求时,在请求头中携带 JWT(如Authorization: Bearer <token>
) → 服务器验证签名有效性 → 解析用户信息 → 处理请求并返回结果。
使用
Springboot整合JWT
-
JWT的引入:
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt --> <dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version> </dependency>
-
具体说明:
-
token是通过荷载Claims或playholder两者之一构造的,两者不能同时提供,否则compact()将报异常。
-
token构建是荷载一般必须两个属性:sub和exp
- sub:即token中保存的必要信息。
- exp:即token的过期时间,当token过了过期时间时,解析token时会抛出
ExpiredJwtException
异常。
-
将token转换成Claims的方法是:Jwts类中的如下方法完成。
public static JwtParser parser() {return new DefaultJwtParser();}
具体实现如下:
public Claims getClaimsByToken(String token) throws ExpiredJwtException{if(null == token)return null;return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}
-
-
解析token。
-
解析过程:
- 第一步:将token转换成Claims对象
- 第二步:通过Claims对象的getSubject()方法获取token中保存的信息。
-
将token转换成Claims的方法是:Jwts类中的如下方法完成。
public static JwtParser parser() {return new DefaultJwtParser();}
-
具体实现如下:
public Claims getClaimsByToken(String token) throws ExpiredJwtException{if(null == token)return null;return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}
-
将token转换成Claims过程中将抛出如下异常
UnsupportedJwtException
:当token不是通过Claims对象构建的token时。ExpiredJwtException
:当token已过期时。MalformedJwtException
:当token不是有效的Claims对象构建的token时。SignatureException
:当token的Signature验证失败时。IllegalArgumentException
:当token为null或token是空字符串或token中只有空字符时。
-
通过Claims的方法获取token中保存的信息。
-
具体实现如下:
/*2.通过token获取构建时的信息*/public String getUserNameFromToken(String token) throws Exception{Claims claims = getClaimsByToken(token);return claims.getSubject();}
-
-
JwtUtil
/*** JwtToken生成的工具类* JWT token的格式:header.payload.signature* header的格式(算法、token的类型):* {"alg": "HS512","typ": "JWT"}* payload的格式(用户名、创建时间、生成时间):* {"sub":"wang","created":1489079981393,"exp":1489684781}* signature的生成算法:* HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)* Created on 2018/4/26.*/
@Component
@Getter
public class JwtTokenUtil {private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);private static final String CLAIM_KEY_USERNAME = "user_name";private static final String CLAIM_KEY_CREATED = "created";@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private Long expiration;@Value("${jwt.tokenHead}")private String tokenHead;/*** 根据负责生成JWT的token*/private String generateToken(Map<String, Object> claims) {return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate()).signWith(SignatureAlgorithm.HS512, secret).compact();}/*** 从token中获取JWT中的负载*/private Claims getClaimsFromToken(String token) {Claims claims = null;try {claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();} catch (Exception e) {LOGGER.info("JWT格式验证失败:{}", token);}return claims;}/*** 生成token的过期时间*/private Date generateExpirationDate() {return new Date(System.currentTimeMillis() + expiration * 1000);}/*** 解密:从token中获取登录用户名(项目使用)*/public String getUserNameFromToken(String token) {String username;try {Claims claims = getClaimsFromToken(token);username = claims.get(CLAIM_KEY_USERNAME,String.class);} catch (Exception e) {username = null;}return username;}/*** 加密: 根据用户名生成token(项目使用)*/public String generateUserNameStr(String username) {Map<String, Object> claims = new HashMap<>();claims.put(CLAIM_KEY_USERNAME,username);claims.put(CLAIM_KEY_CREATED, new Date());return generateToken(claims);}/*** 判断token是否已经失效*/private boolean isTokenExpired(String token) {Date expiredDate = getExpiredDateFromToken(token);return expiredDate.before(new Date());}/*** 从token中获取过期时间*/private Date getExpiredDateFromToken(String token) {Claims claims = getClaimsFromToken(token);return claims.getExpiration();}/*** 当原来的token没过期时是可以刷新的** @param oldToken 带tokenHead的token*/public String refreshHeadToken(String oldToken) {if(StrUtil.isEmpty(oldToken)){return null;}String token = oldToken.substring(tokenHead.length());if(StrUtil.isEmpty(token)){return null;}//token校验不通过Claims claims = getClaimsFromToken(token);if(claims==null){return null;}//如果token已经过期,不支持刷新if(isTokenExpired(token)){return null;}//如果token在30分钟之内刚刷新过,返回原tokenif(tokenRefreshJustBefore(token,30*60)){return token;}else{claims.put(CLAIM_KEY_CREATED, new Date());return generateToken(claims);}}/*** 判断token在指定时间内是否刚刚刷新过* @param token 原token* @param time 指定时间(秒)*/private boolean tokenRefreshJustBefore(String token, int time) {Claims claims = getClaimsFromToken(token);Date created = claims.get(CLAIM_KEY_CREATED, Date.class);Date refreshDate = new Date();//刷新时间在创建时间的指定时间内if(refreshDate.after(created)&&refreshDate.before(DateUtil.offsetSecond(created,time))){return true;}return false;}// 验证令牌有效性//没有使用 Spring Security,就不需要依赖其提供的 UserDetails 类,也无需遵循它的用户认证流程。// 此时验证 JWT 令牌并获取用户信息的思路会更简洁,核心是从 JWT 中解析出用户标识(如用户名、ID 等),// 再通过自己的业务逻辑验证用户合法性。public Boolean validateToken(String token) {// 解析令牌(自动验证签名和过期时间)Jwts.parser().setSigningKey(secret).parseClaimsJws(token);return true;}}
JWT案例
这里将在springboot中展示jwt,通过拦截器的方式。(只展示后端代码,可以使用postman测试,如果token是本地存储localStorage,会存到postman的相应地方,和浏览器一样)
大体流程如下:
- 第一次登录,传入用户名和密码
- 数据库检验用户名和密码是否正确,如果正确,根据用户信息(这里使用用户名)生成token。封装token和其他信息返回给前端。
- 前端拿到token,并且存到本地客户端(如localstorage)。以后每次发送请求都会在请求头中带上
Authorization: Bearer <token>
- 再次向服务端发送请求时就会经过Jwt的拦截器,拿到请求头中的token,解析token是否合法,如果合法即可证明用户身份并放行。
控制器
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@Autowiredprivate JwtTokenUtil jwtTokenUtil;//简单登录接口@PostMapping("/login")public Result<LoginVO> login(@RequestBody LoginDTO loginDTO) {boolean auth = userService.authenticate(loginDTO);if (!auth){return Result.error(40101, "用户名或密码错误");}// 生成tokenString token = jwtTokenUtil.generateUserNameStr(loginDTO.getUsername());String authToken = "Bearer " + token;return Result.success(new LoginVO(loginDTO.getUsername(), authToken,jwtTokenUtil.getExpiration()*1000 + System.currentTimeMillis()));}//测试接口@GetMapping("/info")public Result<String> info() {return Result.success("info");}
}
JWT拦截器
public class JwtInceptor implements HandlerInterceptor {private final JwtTokenUtil jwtTokenUtil;public JwtInceptor(JwtTokenUtil jwtTokenUtil) {this.jwtTokenUtil = jwtTokenUtil;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//获取请求头中令牌String token = request.getHeader("Authorization");if(token != null && token.startsWith("Bearer ")){token = token.substring(7);//验证令牌if(jwtTokenUtil.validateToken(token)){//验证通过,放行,提取用户信息病传递到后续处理String username = jwtTokenUtil.getUserNameFromToken(token);request.setAttribute("username",username);return true;}}//验证失败,返回401response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT");return false;}
}
注册拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Autowiredprivate JwtTokenUtil jwtTokenUtil;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(jwtInceptor()).addPathPatterns("/**").excludePathPatterns("/user/login");}@Beanpublic JwtInceptor jwtInceptor() {return new JwtInceptor(jwtTokenUtil);}
}
结果
登录:http://localhost:8080/user/login
{"code": 200,"msg": "操作成功","data": {"username": "saber","token": "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ1c2VyX25hbWUiOiJzYWJlciIsImNyZWF0ZWQiOjE3NTEyNzE4NjY5NTEsImV4cCI6MTc1MTM1ODI2Nn0.TF2_XwhON3H-Jn_fdG5xacATxu_4lvJqY19C1JUCKkQjWYeW2vRdh5lwlgoCjw-nqTEPbJyRrC7Hg3TBOyuCjw","expireTime": 1751358267094}
}
测试:http://localhost:8080/user/info
{"code": 200,"msg": "操作成功","data": "info"
}
session VS Jwt
-
服务器存储压力大,扩展性差,有性能开销:认证信息(用户ID,权限等)存储在服务器端(通常是内存中),客户端通过session ID与服务器端关联
- 当用户量激增时,服务器需要存储大量的session,会占用大量的内存/存储资源
- 在分布式系统中(如多台服务器负载均衡),需要保证session在多台服务器间同步,复杂度高,维护成本大
- 根据 Session ID 在内存中查找对应的 Session 对象,有性能开销,如果存储在redis或数据库,性能开销较高。
- 如果存储在内存中,重启系统后用户需要重新登录,影响用户体验。
-
依赖cookie,存在安全风险:通常依赖 Cookie 传递 Session ID,而 Cookie 存在以下风险:
-
CSRF(跨站请求伪造):攻击者可能利用用户的 Cookie 在其他网站发起恶意请求(如转账),因为 Session ID 会随 Cookie 自动发送。
-
Cookie 劫持:如果 Session ID 通过 HTTP 明文传输,可能被窃听;即使使用 HTTPS,也可能通过 XSS 攻击获取 Cookie 中的 Session ID,进而伪造身份。
-
JWT 对比:
可通过 HTTP Header(如Authorization: Bearer <token>
)传递,不依赖 Cookie,从根源上避免了 CSRF 风险。即使 Token 被存储在 LocalStorage,虽仍可能面临 XSS 攻击,但可通过设置 Token 有效期较短、配合 HTTPS 等方式降低风险,且 JWT 的签名机制能防止 Token 被篡改。
-
-
难以实现跨域认证
- 传统 Session:
由于 Cookie 的同源策略限制,跨域请求(如前后端分离架构中,前端域名与后端 API 域名不同)时,Session ID 可能无法正常传递(需额外配置CORS
和 Cookie 跨域属性),实现复杂且兼容性差。 - JWT 对比:
基于 Header 传递的 JWT 不受同源策略限制,可轻松支持跨域认证(如不同域名的前端应用调用同一后端 API),只需在请求头中携带 Token 即可,更适合前后端分离、微服务等分布式架构。
- 传统 Session:
-
Token 有效期管理灵活度低
-
Session
Session 的有效期通常在服务器端设置(如 30 分钟),若需提前失效 (如用户登出、密码修改、账号异常),需在服务器端主动删除对应的 Session 数据。服务端可控性强。
-
JWT
无法修改已生成的token,其exp无法更新,但可以通过token的刷新机制,但短期 Token(如 15 分钟)配合刷新 Token(Refresh Token)的机制,可兼顾安全性和用户体验,且无需服务器存储全量认证数据。
-
场景 | JWT | 传统 Session |
---|---|---|
分布式 / 微服务架构 | ✅ 无状态,无需共享 Session | ❌ 需要 Session 共享机制 |
跨域认证 | ✅ 支持跨域(Header 传递) | ❌ 依赖 Cookie,需额外配置 |
移动端应用 | ✅ 轻量,易于客户端管理 | ❌ 依赖 Cookie,移动端支持差 |
高并发场景 | ✅ 验证快(本地计算),无需 IO | ❌ 频繁查询存储,性能瓶颈 |
安全性要求极高 | ❌ Token 一旦泄露风险大 | ✅ 服务端可控(如强制登出) |