1. 项目概述
在Java Web开发中,登录认证是一个基础但至关重要的功能模块。这次我们要实现的是一个基于JWT令牌和Filter的完整认证方案,这也是目前企业级应用中最常见的认证方式之一。不同于传统的Session认证,JWT(JSON Web Token)是一种无状态的认证机制,特别适合分布式系统和前后端分离架构。
我在实际项目中多次使用过这种方案,发现它既能解决跨域认证问题,又能减轻服务器存储压力。但同时也遇到过不少坑,比如令牌刷新、安全性处理等问题。下面就把这套方案的完整实现过程和经验总结分享给大家。
2. 核心组件解析
2.1 JWT令牌机制
JWT由三部分组成:Header(头部)、Payload(负载)和Signature(签名)。它的工作原理是这样的:
- 用户登录成功后,服务器生成一个包含用户信息的JWT
- 客户端保存这个JWT(通常放在localStorage或cookie中)
- 后续请求都在Authorization头中携带这个JWT
- 服务器验证JWT的有效性并处理请求
生成JWT的典型代码示例:
java复制public String generateToken(User user) {
// 设置过期时间
Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder()
.setHeaderParam("typ", "JWT") // 设置头部
.setSubject(user.getId()) // 设置主体
.setIssuedAt(new Date()) // 签发时间
.setExpiration(expireDate) // 过期时间
.claim("role", user.getRole()) // 自定义声明
.signWith(SignatureAlgorithm.HS512, SECRET_KEY) // 签名算法和密钥
.compact();
}
注意:SECRET_KEY一定要足够复杂且妥善保管,这是保证JWT安全的关键
2.2 Filter过滤器的作用
Filter是Java Web中的拦截器,可以在请求到达Servlet前进行预处理。在认证流程中,我们主要用它来做:
- 拦截需要认证的请求
- 从请求头中提取JWT
- 验证JWT的有效性
- 将用户信息存入请求上下文
Filter的执行顺序可以通过web.xml中的
3. 完整实现步骤
3.1 环境准备
首先确保你的项目包含以下依赖(Maven配置):
xml复制<!-- JWT支持 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
3.2 JWT工具类实现
创建一个JwtUtil工具类,封装常用的JWT操作:
java复制public class JwtUtil {
private static final String SECRET_KEY = "your-256-bit-secret"; // 实际项目应从配置读取
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // 7天
// 生成Token
public static String generateToken(String userId, String role) {
// ...实现同上...
}
// 解析Token
public static Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
// 验证Token
public static boolean verifyToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
return false;
}
}
}
3.3 认证Filter实现
创建一个JwtAuthenticationFilter:
java复制public class JwtAuthenticationFilter implements Filter {
private static final List<String> WHITE_LIST = Arrays.asList(
"/api/login",
"/api/register",
"/swagger-ui.html"
);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 检查白名单
if (isWhiteList(httpRequest.getRequestURI())) {
chain.doFilter(request, response);
return;
}
// 获取Token
String token = httpRequest.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
sendError(httpResponse, "缺少有效的认证信息");
return;
}
token = token.substring(7); // 去掉Bearer前缀
try {
// 验证Token
Claims claims = JwtUtil.parseToken(token);
// 将用户信息存入请求属性
request.setAttribute("userId", claims.getSubject());
request.setAttribute("userRole", claims.get("role", String.class));
chain.doFilter(request, response);
} catch (ExpiredJwtException e) {
sendError(httpResponse, "令牌已过期");
} catch (Exception e) {
sendError(httpResponse, "无效的令牌");
}
}
private boolean isWhiteList(String uri) {
return WHITE_LIST.stream().anyMatch(uri::startsWith);
}
private void sendError(HttpServletResponse response, String message) throws IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Map<String, String> error = new HashMap<>();
error.put("error", "Unauthorized");
error.put("message", message);
response.getWriter().write(new ObjectMapper().writeValueAsString(error));
}
}
3.4 注册Filter
在Spring Boot中注册Filter:
java复制@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<JwtAuthenticationFilter> jwtFilter() {
FilterRegistrationBean<JwtAuthenticationFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new JwtAuthenticationFilter());
registration.addUrlPatterns("/api/*");
registration.setOrder(1); // 设置过滤器顺序
return registration;
}
}
4. 关键问题与解决方案
4.1 令牌刷新机制
JWT的一个缺点是过期后需要重新登录。好的做法是实现令牌刷新机制:
- 发放两个Token:access_token(短有效期)和refresh_token(长有效期)
- access_token过期后,用refresh_token获取新的access_token
- refresh_token也过期时才要求重新登录
实现代码示例:
java复制public Map<String, String> refreshToken(String refreshToken) {
if (!JwtUtil.verifyToken(refreshToken)) {
throw new RuntimeException("无效的刷新令牌");
}
Claims claims = JwtUtil.parseToken(refreshToken);
String userId = claims.getSubject();
String role = claims.get("role", String.class);
Map<String, String> tokens = new HashMap<>();
tokens.put("access_token", JwtUtil.generateToken(userId, role));
tokens.put("refresh_token", JwtUtil.generateRefreshToken(userId, role));
return tokens;
}
4.2 安全性增强
- 防止CSRF攻击:建议将JWT存储在HttpOnly的Cookie中,而不是localStorage
- 密钥轮换:定期更换SECRET_KEY,使旧令牌失效
- 黑名单机制:对于需要提前失效的令牌,维护一个短期的黑名单缓存
4.3 性能优化
- 使用非对称加密算法(如RS256)替代HS256,减轻服务器负担
- 对频繁访问的接口添加缓存层,减少JWT验证次数
- 使用连接池处理数据库验证操作
5. 测试与验证
5.1 登录接口测试
使用Postman测试登录流程:
- 调用/login接口获取JWT
- 在后续请求的Header中添加Authorization: Bearer
- 验证受保护接口的访问
5.2 异常场景测试
需要特别测试的场景包括:
- 过期令牌
- 篡改后的令牌
- 不携带令牌的请求
- 不同角色权限的访问控制
6. 实际项目中的经验
经过多个项目的实践,我总结了以下几点经验:
- 令牌有效期设置:生产环境中access_token建议2小时,refresh_token建议7天
- 敏感操作二次验证:即使有有效JWT,关键操作(如修改密码)也应要求重新验证
- 分布式环境处理:在微服务架构中,可以考虑将JWT验证逻辑放到API网关层
- 监控与日志:记录异常的JWT验证请求,有助于发现潜在的安全问题
一个常见的坑是时区问题。JWT的过期时间是基于UTC的,而服务器可能有自己的时区设置。我曾经遇到过本地测试正常,但部署到服务器后令牌立即过期的情况。解决方案是在生成和验证时都明确指定时区:
java复制// 生成时
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME), TimeZone.getTimeZone("UTC"))
// 验证时
Jwts.parser().setAllowedClockSkewSeconds(30) // 允许30秒时钟偏移
另一个实际问题是移动端的令牌存储。iOS和Android有不同的安全存储方案,不能简单使用Web那套。在混合开发中,需要与客户端同事密切配合确定最佳实践。