JWT全称为 JSON Web Token(https://jwt.io/)。JWT定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
JWT令牌由三部分组成:
Header和Payload会通过Base64编码生成。Signature部分会通过Header指定的签名算法,以Header、Payload以及指定的秘钥作为签名算法输入计算得到。这三部分共同组成了JWT令牌。
Java代码生成与校验令牌需要引入JWT的相关依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
生成JWT令牌需要准备四个数据:签名算法、秘钥、有效时间和载荷(自定义内容)。
签名算法有很多种,可以在JWT官网查看。
下面通过一个简单的例子感受一下:
生成JWT令牌:
@Test
public void testJWT(){
// 准备Map集合载荷
Map<String, Object> claims = new HashMap<>();
claims.put("id", 1);
claims.put("name", "Tom");
String jwt = Jwts.builder() // 使用Jwts中的builder()方法构造
.signWith(SignatureAlgorithm.HS256, "wrj-web") // 指定签名算法和秘钥
.setClaims(claims) // 加入载荷
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 设置有效时间:当前时间 + 有效时长,单位为ms
.compact();
System.out.println(jwt);
}
// 运行输出:
// eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTczMzU0MTk2MX0.AgNXthmTMBJHKIeSaacBf-wThVNwsPi1F63sAqsuJkY
解析JWT令牌:
@Test void testParseJWT(){
Claims claims = Jwts.parser() // 使用Jwts中的parser()方法开始解析
.setSigningKey("wrj-wrb") // 指定秘钥
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiaWQiOjEsImV4cCI6MTczMzU0MTk2MX0.AgNXthmTMBJHKIeSaacBf-wThVNwsPi1F63sAqsuJkY") // 输入JWT令牌,此处直接输入上述代码生成的JWT令牌
.getBody(); // 获取载荷
System.out.println(claims);
}
// 运行输出:
// {name=Tom, id=1, exp=1733541961}
通常会将JWT生成与解析的代码封装成JWT工具类来使用,JwtUtil工具类中只有两个成员方法,一个用于生成JWT令牌,一个用于解析JWT令牌:
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT有效时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();
}
/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
}
对于一般项目而言,JWT令牌需要的签名秘钥、有效时长以及前端传递过来的令牌名称是较为固定的,可以在配置到配置文件中,以便修改。
步骤如下:
web:
jwt:
# 设置jwt签名加密时使用的秘钥
secret-key: itcast
# 设置jwt过期时间 单位ms
ttl: 7200000
# 设置前端传递过来的令牌名称
token-name: token
@Component // 注册成Bean
@ConfigurationProperties(prefix = "web.jwt") // 关联到配置文件中web->jwt下的配置信息
@Data // Lombok注解,生成get/set等方法
public class JwtProperties {
// 签名秘钥,与配置文件中secret-key对应
private String secretKey;
// jwt过期时间,与配置文件中ttl对应
private long ttl;
// 前端传递过来的令牌名称,与配置文件中token-name对应
private String tokenName;
}
在需要使用这些信息时,只需要将JwtProperties 对象注入,然后通过get/set方法获取即可。
过滤器能够把对资源的请求拦截下来,从而实现一些特殊的功能,例如登录校验、敏感字符处理等。
Filter过滤器使用分两步:
@WebFilter(urlPatterns = "/*") // 通过WebFilter注解表示这是一个web过滤器组件,通过urlPatterns属性配置拦截路径
public class DemoFilter implements Filter {
// 初始化方法、Web服务器启动,创建Filter时调用,只调用一次。通常在此方法中完成资源和环境的准备操作
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
// 每次拦截到请求时会调用
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("拦截请求...放行前逻辑");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("拦截请求...放行后逻辑");
}
// 销毁方法,Web服务器关闭时调用,只调用一次。通常完成资源的释放等操作
@Override
public void destroy() {
Filter.super.destroy();
}
}
Filter过滤器的拦截路径通过 @WebFilter 注解中的 urlPatterns 属性设置,有三种方式:
拦截路径 | urlPatterns | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问 /login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问 /emps 下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
Filter接口中的三个方法有各自的执行时机和作用。
在使用Filter过滤器时需要注意几点:
下面改造doFilter()方法,实现登录校验功能。
场景:用户进行登录操作不进行拦截,其他操作进行拦截。也就是不拦截"/login"路径,拦截其他所有路径。
拦截逻辑分为以下几步:
1.获取请求路径;
2.判断路径中是否包含"login",包含则直接放行,不包含则需拦截,进行登录校验;Filter放行是通过执行filterChain调用doFilter()方法;
3.进行登录校验,首先获取JWT令牌;
4.判断JWT令牌是否存在,或是否为空。不存在或为空则校验失败,不放行,直接return结束。
5.解析JWT令牌,不成功则不放行;
6.解析JWT令牌成功则放行。
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
// 1.获取请求的URL
String url = req.getRequestURL().toString();
// 2.判断login是否存在,存在则放行,不存在则拦截
if(url.contains("login")) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
// 3.获取JWT令牌
String jwt = req.getHeader("token");
// 4.判断JWT是否存在
if(!StringUtils.hasLength(jwt)) {
Result error = Result.error("NOT_LOGIN");
String s = JSONObject.toJSONString(error);
resp.getWriter().write(s);
return;
}
// 5.判断JWT是否能解析成功
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
Result error = Result.error("NOT_LOGIN");
String s = JSONObject.toJSONString(error);
resp.getWriter().write(s);
return;
}
// 6.解析成功,放行
filterChain.doFilter(servletRequest, servletResponse);
}
这部分登录校验的代码是可以进行优化的。因为实际业务中是不会对 /login 登录请求进行拦截的。
一个Web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
由于存在多个过滤器,过滤器的执行顺序会按照过滤器类名(字符串)的自然顺序。
拦截器是一种动态拦截方法调用的机制,类似与过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。拦截器通常用来拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
Interceptor拦截器的使用分两步:
@Component // 注册成Bean,交给IOC容器管理
public class DemoInterceptor implements HandlerInterceptor {
// 目标资源方法执行前执行,返回true:放行,返回false:不放行
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("preHandle...");
return true;
}
// 目标资源方法执行后执行
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("postHandle...");
}
// 视图渲染完毕后执行,最后执行
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("afterCompletion...");
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private DemoInterceptor demoInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor) // 注册拦截器
.addPathPatterns("/**") // 配置拦截资源
.excludePathPatterns("/login"); // 配置不拦截的资源
}
}
除了通过实现WebMvcConfigurer接口来注册配置拦截器,Spring还提供了继承WebMvcConfigurationSupport配置类来实现注册配置拦截器。定义一个类,继承WebMvcConfigurationSupport配置类,重写其中的addInterceptors()方法。
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private DemoInterceptor demoInterceptor;
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(demoInterceptor) // 注册拦截器
.addPathPatterns("/**") // 配置拦截资源
.excludePathPatterns("/login"); // 配置不拦截的资源
}
}
Interceptor拦截器的拦截路径是在配置类中进行配置的。可借鉴上述配置类的代码。
下面介绍Interceptor的拦截路径:
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配 /depts、/emps、/login,不能匹配 /depts/1 |
/** | 任意级路径 | 能匹配 /depts、/depts/1、/depts/1/2 |
/depts/* | /depts下的一级路径 | 能匹配 /depts/1、不能匹配 /depts/1/2,/depts |
/depts/** | /depts下的任意级路径 | 能匹配 /depts、/depts/1、/depts/1/2、不能匹配 /emps/1 |
Interceptor接口中的三个方法有各自的执行时机和作用。
Interceptor拦截器进行登录校验与Filter过滤器实现登录校验的逻辑相同,只是部分实现细节不同。这里提供一个简单的实现。
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取请求路径
String url = request.getRequestURL().toString();
// 2. 判断请求路径是否包含 /login
if(url.contains("login")) {
return true;
}
// 3. 从请求头获取token,一般把JWT令牌的key名设置为token
String jwt = request.getHeader("token");
// 4. 判断是否获取到JWT令牌
if(!StringUtils.hasLength(jwt)) {
Result error = Result.error("NOT_LOGIN");
String s = JSONObject.toJSONString(error);
response.getWriter().write(s);
return false;
}
// 5. 解析JWT令牌
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
Result error = Result.error("NOT_LOGIN");
String s = JSONObject.toJSONString(error);
response.getWriter().write(s);
return false;
}
return true;
}