JSON Web Token

JWT 由(.)分隔的三个部分组成 Header.Payload.Signature

  • Header(头部)描述 JWT 的元数据,由Base64URL算法转成字符串

    1
    2
    3
    4
    {
    "alg": "HS256", // 签名的算法(algorithm)
    "typ": "JWT" // 令牌(token)的类型
    }
  • Payload(负载)用来存放实际需要传递的数据,由Base64URL算法转成字符串

    • iss (issuer):签发人
    • exp (expiration time):过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号
  • Signature(签名)对前两部分的签名,防止数据篡改,使用 Header 里面指定的签名算法产生签名

    1
    2
    3
    4
    5
    HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret
    )
  • Token: ${Header}.${Payload}.${Signature} 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔

Spring Boot 实现 JWT

引入JWT依赖

1
2
3
4
5
6
<!---jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>

创建 utils/JwtUtil.java 生成以及验证token的工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class JwtUtil {

/**
* 有效期
*/
public static long expireTime = 1000 * 60 * 60 * 24 * 7; // 7天

/**
* 签名密钥
*/
public static String secret = "10wen_petRescue";

/**
* token 前缀
*/
public static String tokenPrefix = "Bearer ";

/**
* 设置 token 有效时间
* @param expireTime token 有效时间
*/
public void setExpireTime(int expireTime) {
JwtUtil.expireTime = expireTime;
}

/**
* 设置 token 密钥
* @param secret token 密钥
*/
public void setSecret(String secret) {
JwtUtil.secret = secret;
}

/**
* 创建 TOKEN
* JWT 构成: header, payload, signature
* @param map jwt payload Map
* @return token 值
*/
public static String createToken(Map<String,String> map) {
// Header 选择默认
JWTCreator.Builder builder = JWT.create();
// Payload
map.forEach((key,value)->{
builder.withClaim(key,value);
});
String TOKEN = builder.withExpiresAt(new Date(System.currentTimeMillis() + expireTime))
.sign(Algorithm.HMAC256(secret)); // Signature

return tokenPrefix + TOKEN;
}

/**
* 验证 token
* @param token 验证的 token 值
* @return DecodedJWT 解码后的 jwt令牌对象
*/
public static DecodedJWT validateToken(String token) throws Exception {
// 创建验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
// 去除 token 前缀 "Bearer "
String noPrefixToken = token.replace(tokenPrefix, "");
// 得到解码token对象
DecodedJWT decodedJwt = jwtVerifier.verify(noPrefixToken);
return decodedJwt;
}
}

创建interceptors/JWTInterceptor.java请求拦截器验证token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Map<String,Object> map = new HashMap<>();
// 获取请求头 token
String token = request.getHeader("token");
try {
JwtUtil.validateToken(token); // 验证令牌
return true; // 放行请求
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg","无效签名!");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg","token过期!");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg","token算法不一致!");
} catch (Exception e) {
e.printStackTrace();
map.put("msg","token无效!");
}
map.put("state",false);
// 将 map 转换为 json
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}

配置拦截器Config/InterceptorConfig.java

1
2
3
4
5
6
7
8
9
10
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor())
.addPathPatterns("/**") // 拦截所有
.excludePathPatterns("/user/login", "/user/register"); // 放行登录注册
}
}

controller/UserControl.java生成token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@GetMapping("/login")
public Map<String,Object> login(User user){
Map<String,Obejct> map = new HashMap<>();
try {
User user = userService.login(user);
Map<String,String> payloadMap = new HashMap<>();
payloadMap.put("username",username);
String token = JwtUtil.createToken(payloadMap);
// 返回 Token 令牌验证信息
map.put("token",token);
map.put("state", true);
map.put("msg","认证成功");
} catch(Exception e) {
map.put("state", false);
map.put("msg",e.getMessage());
}
return map;
}

SpringBoot加了拦截器后出现的跨域问题解析

问题描述:

  1. CROS复杂请求时会首先发送一个OPTIONS请求做嗅探,来测试服务器是否支持本次请求,请求成功后才会发送真实的请求
  2. OPTIONS请求不会携带任何数据,导致这个请求不符合我们拦截器的校验规则被拦截了
  3. 响应头中也没携带解决跨域需要的头部信息,进而出现了跨域问题
  4. 所有的拦截器的preHandle()方法的执行都在实际跨域处理handler的方法之前,拦截器返回false都会跳过后续所有处理过程,因此预检请求被拦截了

方案一:JWT拦截器interceptors/JWTInterceptor.java把所有的OPTIONS请求放行

  • 适用:跨域用的是@CrosOrigin注解,或者是Config/CorsConfig.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
    .allowedOrigins("*")
    .allowCredentials(true)
    .allowedMethods("GET","POST","DELETE","PUT","PATCH","OPTIONS")
    .maxAge(3600);

    }
    }
    解决:在验证Token之前配置
    1
    2
    3
    4
    5
    6
    7
    8
    // 解决因为拦截器先于跨域解决方案处理函数执行,OPTIONS预检请求不携带token而被拦截器抛出错误
    // 拦截器取到请求先进行判断,如果是OPTIONS请求,则放行
    if("OPTIONS".equals(request.getMethod().toUpperCase())) {
    System.out.println("Method:OPTIONS预检请求");
    return true;
    }
    // ...验证 Token

方案二:利用过滤器CorsFilter解决跨域问题,CorsFilter是定义在Web容器中的过滤器,其执行顺序先于SpringMVC的所有拦截器执行,配置Config/CorsConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//让cors高于拦截器的权限
@Configuration
public class GlobalCorsConfig {

@Bean
public CorsFilter corsFilter() {

CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.addExposedHeader("token");
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
return new CorsFilter(configSource);
}
}

Nodejs 实现JWT

1. 安装:jsonwebtokenexpress-jwt

  • jsonwebtoken 用于生成 JWT 字符串
  • express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
1
npm install jsonwebtoken express-jwt

2. 定义密钥:secret

  • 用户生成和解密 Token
1
2
3
4
5
const jwt = require('jsonwebtoken')
const expressJWT = require("express-jwt")

// TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = '10wen.github.io'

3. 生成 Token

1
2
3
4
5
6
7
8
9
10
11
12
app.post('/api/login', function (req, res) {
const userInfo = req.body
// TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
// (用户信息对象, 加密密钥, 配置对象,可以配置 token 的有效期)
// 不要把 密码 加密到 token 字符串中
const tokenStr = jwt.sign({username: userInfo.username}, secretKey, {expiresIn: '30s'})
res.send({
status: 200,
message: '登录成功!',
token: tokenStr // 要发送给客户端的 token 字符串
})
})

4. JWT 字符串还原为 JSON 对象 (app.js)

  • 客户端访问有权限的接口时,需通过请求头的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证
  • 服务器可以通过 express-jwt 中间件将客户端发送过来的 Token 解析还原成 JSON 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// unless({ path: [/^\/api\//] }) 指定哪些接口无需访问权限
// 在路由之前配置解析 Token 的中间件
const expressJWT = require('express-jwt')
const config = require('./config') // 配置文件取出 secret
app.use(
expressJWT({
secret: config.jwtSecretKey,
algorithms: ["HS256"],
// 无token请求不进行解析,并且抛出异常,给错误中间件处理
credentialsRequired: false,
}).unless({
path: [
"/login",
"/register",
"/index",
{ url: /^\/articles\/.*/, methods: ["GET"] },
{ url: /^\/content\/.*/, methods: ["GET"] },
],
})
);

// app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))

5. 获取用户信息

  • 当 express-jwt 中间件配置成功后,即可在那些有权限的接口中,使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息
1
2
3
4
5
6
7
8
9
app.get('/admin/getinfo', function (req, res) {
// TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.user)
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.user // 要发送给客户端的用户信息
})
})

6. 捕获解析 JWT 失败后产生的错误

  • 当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行
  • 通过 Express 的错误中间件,捕获这个错误并进行相关的处理
1
2
3
4
5
6
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.send({ status: 401, message: 'Invalid token' })
}
res.send({ status: 500, message: 'Unknown error' })
})