JWT安全风险及应对策略
- 密钥泄露风险
- 风险:JWT的签名验证依赖于密钥,如果密钥泄露,攻击者可以伪造有效的JWT,从而获取未授权的访问权限。
- 应对策略:妥善保管密钥,避免硬编码在代码中。在生产环境中,建议使用环境变量来存储密钥。
- 代码示例(Node.js with Express):
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
// 从环境变量中获取密钥
const secretKey = process.env.JWT_SECRET_KEY;
app.get('/protected', (req, res) => {
const token = req.headers['authorization'];
if (!token) {
return res.status(401).send('Token is missing');
}
try {
const decoded = jwt.verify(token, secretKey);
res.send('Access granted:'+ decoded);
} catch (err) {
res.status(403).send('Invalid token');
}
});
- 过期时间设置不当风险
- 风险:如果JWT的过期时间设置过长,攻击者获取到JWT后有较长时间可以利用它进行未授权访问;如果设置过短,可能会给用户带来频繁重新认证的不便。
- 应对策略:根据应用场景合理设置过期时间。对于长期用户会话,可以结合刷新令牌(refresh token)机制。
- 代码示例(Python with Flask):
from flask import Flask, request, jsonify
import jwt
import datetime
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
@app.route('/protected', methods=['GET'])
def protected():
token = None
if 'x-access-token' in request.headers:
token = request.headers['x-access-token']
if not token:
return jsonify({'message': 'Token is missing!'}), 401
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
# 检查过期时间
if 'exp' in data and data['exp'] < datetime.datetime.utcnow().timestamp():
return jsonify({'message': 'Token has expired'}), 401
return jsonify({'message': 'Access granted!'}), 200
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token has expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Invalid token'}), 401
- 重放攻击风险
- 风险:攻击者截获一个有效的JWT,并多次使用它来访问受保护资源。
- 应对策略:可以使用一次性使用的JWT,或者结合黑名单机制。在黑名单机制中,当用户登出或者JWT被撤销时,将其加入黑名单。
- 代码示例(Java with Spring Boot):
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import java.util.ArrayList;
import java.util.List;
@RestController
public class ProtectedResourceController {
private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
private static final List<String> blacklist = new ArrayList<>();
@GetMapping("/protected")
public ResponseEntity<String> protectedResource(@RequestHeader("Authorization") String token) {
if (token == null ||!token.startsWith("Bearer ")) {
return new ResponseEntity<>("Token is missing", HttpStatus.UNAUTHORIZED);
}
String jwtToken = token.substring(7);
if (blacklist.contains(jwtToken)) {
return new ResponseEntity<>("Invalid token", HttpStatus.FORBIDDEN);
}
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(jwtToken)
.getBody();
return new ResponseEntity<>("Access granted", HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>("Invalid token", HttpStatus.FORBIDDEN);
}
}
}
- 算法篡改风险
- 风险:攻击者可能篡改JWT头部的签名算法,将其改为无签名算法(如none),从而绕过签名验证。
- 应对策略:在验证JWT时,明确指定预期的签名算法,不使用JWT头部指定的算法,除非进行严格的验证。
- 代码示例(Ruby with Sinatra):
require'sinatra'
require 'jwt'
set :jwt_secret, 'your_secret_key'
get '/protected' do
token = request.env['HTTP_AUTHORIZATION']&.gsub('Bearer ', '')
if token.nil?
halt 401, 'Token is missing'
end
begin
decoded = JWT.decode(token, settings.jwt_secret, true, { algorithm: 'HS256' })
'Access granted'
rescue JWT::VerificationError
halt 403, 'Invalid token'
end
end