JSON Web Token
Header(头部)描述 JWT 的元数据,由Base64URL算法转成字符串
1 2 3 4
| { "alg": "HS256", "typ": "JWT" }
|
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;
public static String secret = "10wen_petRescue";
public static String tokenPrefix = "Bearer ";
public void setExpireTime(int expireTime) { JwtUtil.expireTime = expireTime; }
public void setSecret(String secret) { JwtUtil.secret = secret; }
public static String createToken(Map<String,String> map) { JWTCreator.Builder builder = JWT.create(); map.forEach((key,value)->{ builder.withClaim(key,value); }); String TOKEN = builder.withExpiresAt(new Date(System.currentTimeMillis() + expireTime)) .sign(Algorithm.HMAC256(secret));
return tokenPrefix + TOKEN; }
public static DecodedJWT validateToken(String token) throws Exception { JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build(); String noPrefixToken = token.replace(tokenPrefix, ""); 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<>(); 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); 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); 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加了拦截器后出现的跨域问题解析
问题描述:
- CROS复杂请求时会首先发送一个OPTIONS请求做嗅探,来测试服务器是否支持本次请求,请求成功后才会发送真实的请求
- OPTIONS请求不会携带任何数据,导致这个请求不符合我们拦截器的校验规则被拦截了
- 响应头中也没携带解决跨域需要的头部信息,进而出现了跨域问题
- 所有的拦截器的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
|
if("OPTIONS".equals(request.getMethod().toUpperCase())) { System.out.println("Method:OPTIONS预检请求"); return true; }
|
方案二:利用过滤器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
| @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. 安装:jsonwebtoken
、express-jwt
- jsonwebtoken 用于生成 JWT 字符串
- express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
1
| npm install jsonwebtoken express-jwt
|
2. 定义密钥:secret
1 2 3 4 5
| const jwt = require('jsonwebtoken') const expressJWT = require("express-jwt")
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 const tokenStr = jwt.sign({username: userInfo.username}, secretKey, {expiresIn: '30s'}) res.send({ status: 200, message: '登录成功!', token: tokenStr }) })
|
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
|
const expressJWT = require('express-jwt') const config = require('./config') app.use( expressJWT({ secret: config.jwtSecretKey, algorithms: ["HS256"], credentialsRequired: false, }).unless({ path: [ "/login", "/register", "/index", { url: /^\/articles\/.*/, methods: ["GET"] }, { url: /^\/content\/.*/, methods: ["GET"] }, ], }) );
|
5. 获取用户信息
- 当 express-jwt 中间件配置成功后,即可在那些有权限的接口中,使用
req.user
对象,来访问从 JWT 字符串中解析出来的用户信息
1 2 3 4 5 6 7 8 9
| app.get('/admin/getinfo', function (req, res) { 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' }) })
|