从网上摘录了一些关于 JWT 的优点
1. 相比于session,它无需保存在服务器,不占用服务器内存开销。
2. 无状态、可拓展性强:比如有3台机器(A、B、C)组成服务器集群,若session存在机器A上,session只能保存在其中一台服务器,此时你便不能访问机器B、C,因为B、C上没有存放该Session,而使用token就能够验证用户请求合法性,并且我再加几台机器也没事,所以可拓展性好就是这个意思。
JWT的大致思路就是:(感觉不太严谨,仅当参考即可)
后台收到前端的登录验证请求,账号验证成功后,后台创建token并把token返回给前端,前端获取到token后,把token存入cookie中,之后获取数据的请求都要在请求头中加入token。后台会从请求头中解析token,来验证请求的安全性。token不保存在服务端,只保存在前端。后台解析token不需要匹对服务端的数据库或者本地文件等存储介质。所以jwt是相对独立的。
附上一个流程图
下面就是运用实例基于SpringBoot
Maven配置
<!-- https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>5.14</version>
</dependency>
JWT核心代码
import com.aekc.mmall.enums.TokenState;
import com.google.common.collect.Maps;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import net.minidev.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Map;
/**
* JWT组成
* 第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。
*/
public class JwtUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtUtil.class);
/**
* 公共秘钥-保存在服务端,客户端是不知道该秘钥的,防止被攻击。(signature)
*/
private static final byte[] SECRET = "1234567890qwertyuiopasdfghjklzxcvbnm".getBytes();
/**
* 初始化head部分的数据为(第一部分)
* {
* "alg":"HS256",
* "type":"JWT"
* }
*/
private static final JWSHeader HEADER = new JWSHeader(JWSAlgorithm.HS256, JOSEObjectType.JWT, null, null, null, null, null, null, null, null, null, null, null);
/**
* 生成token,该方法只在用户登录成功后调用
* @param payload Map集合,可以存储用户id,token生成时间,token过期时间等自定义字段
* @return token字符串,若失败则返回null
*/
public static String createToken(Map<String, Object> payload) {
String tokenString = null;
// 创建一个JWS Object(第二部分)
JWSObject jwsObject = new JWSObject(HEADER, new Payload(new JSONObject(payload)));
try {
// 将jwsObject进行HMAC签名,相当于加密(第三部分)
jwsObject.sign(new MACSigner(SECRET));
tokenString = jwsObject.serialize();
} catch (JOSEException e) {
LOGGER.error("签名失败: {}", e.getMessage());
e.printStackTrace();
}
return tokenString;
}
/**
* 校验token是否合法,返回Map集合,集合中主要包含 state状态码 data鉴权成功后从token中提取的数据
* 该方法在过滤器中调用,每次请求API时都校验
* @param token token
* @return Map<String, Object>
*/
public static Map<String, Object> validToken(String token) {
Map<String, Object> resultMap = Maps.newHashMap();
try {
JWSObject jwsObject = JWSObject.parse(token);
// palload就是JWT构成的第二部分不过这里自定义的是私有声明(标准中注册的声明, 公共的声明)
Payload payload = jwsObject.getPayload();
JWSVerifier verifier = new MACVerifier(SECRET);
if(jwsObject.verify(verifier)) {
JSONObject jsonObject = payload.toJSONObject();
// token检验成功(此时没有检验是否过期)
resultMap.put("state", TokenState.VALID.toString());
// 若payload包含ext字段,则校验是否过期
if(jsonObject.containsKey("ext")) {
long extTime = Long.valueOf(jsonObject.get("ext").toString());
long curTime = System.currentTimeMillis();
// 过期了
if(curTime > extTime) {
resultMap.clear();
resultMap.put("state", TokenState.EXPIRED.toString());
}
}
resultMap.put("data", jsonObject);
} else {
// 检验失败
resultMap.put("state", TokenState.INVALID.toString());
}
} catch (Exception e) {
e.printStackTrace();
// token格式不合法导致的异常
resultMap.clear();
resultMap.put("state", TokenState.INVALID.toString());
}
return resultMap;
}
}
token的枚举信息
public enum TokenState {
/** 过期 */
EXPIRED("EXPIRED"),
/** 无效(token不合法) */
INVALID("INVALID"),
/** 有效的 */
VALID("VALID");
private String state;
TokenState(String state) {
this.state = state;
}
/**
* 根据状态字符串获取token状态枚举对象
* @param tokenState
* @return TokenState
*/
public static TokenState getTokenState(String tokenState) {
TokenState[] states = TokenState.values();
TokenState ts = null;
for(TokenState state : states) {
if(state.toString().equals(tokenState)) {
ts = state;
break;
}
}
return ts;
}
@Override
public String toString() {
return this.state;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
配置两个拦截器interceptor
一个是拦截所有请求的HttpInterceptor用来设定返回头信息。另一个JwtInterceptor用来拦截除了登录外的请求。
@Component
public class HttpInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 允许跨域
response.setHeader("Access-Control-Allow-Origin", "*");
// 允许自定义请求头token(允许head跨域)
response.setHeader("Access-Control-Allow-Headers", "token, Accept, Origin, X-Requested-With, Content-Type, Last-Modified");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
@Component
public class JwtInterceptor implements HandlerInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtInterceptor.class);
private void output(JsonData jsonData, HttpServletResponse response) throws IOException {
response.setContentType("text/html;charset=UTF-8;");
PrintWriter out = response.getWriter();
out.write(Objects.requireNonNull(JsonUtil.objectToJson(jsonData)));
out.flush();
out.close();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 前段ajax自定义headers字段,会出现了option请求,在GET请求之前。
// 所以应该把他过滤掉,以免影响服务。但是不能返回false,如果返回false会导致后续请求不会继续。
if("OPTIONS".equalsIgnoreCase(request.getMethod())) {
return true;
}
//从请求头中获取token
String token = request.getHeader("token");
Map<String, Object> resultMap = JwtUtil.validToken(token);
TokenState state = TokenState.getTokenState((String) resultMap.get("state"));
switch(state) {
case VALID:
// 取出payload中数据,放到request作用域中
request.setAttribute("data", resultMap.get("data"));
return true;
case EXPIRED:
case INVALID:
LOGGER.warn("无效token");
//JsonData是返回给前端的json格式(不重要)
JsonData jsonData = new JsonData(false);
jsonData.setMsg("您的token不合法或者过期了,请重新登陆");
output(jsonData, response);
break;
default:
break;
}
return false;
}
}
把interceptor注册到spring容器中,并设置拦截的url
import com.aekc.mmall.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SpringBootConfiguration
public class InterceptorConfiguration implements WebMvcConfigurer {
@Autowired
private HttpInterceptor httpInterceptor;
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(httpInterceptor).addPathPatterns("/**");
registry.addInterceptor(jwtInterceptor).addPathPatterns("/sys/**");
}
}
Controller
在登录的时候创建token
@GetMapping(value = "/login")
public JsonData login(HttpServletRequest request, HttpServletResponse response) {
String username = request.getParameter("username");
String password = request.getParameter("password");
//这个步骤就是获取user的全部信息不重要,直接忽略
SysUser sysUser = sysUserService.findByKeyword(username);
sysUser.setPassword(null);
String token = createPayLoad(sysUser.getId());
return JsonData.success(token);
}
/**
* JWT的组成:Header + payload + signature
* Payload(载荷)的组成信息,私有声明(标准中注册的声明和公共的声明并未使用)
* @param userId 用户id
* @return token
*/
private String createPayLoad(Integer userId) {
Map<String, Object> payload = Maps.newHashMap();
Date date = new Date();
// 用户id
payload.put("uid", String.valueOf(userId));
// 生成时间:当前
payload.put("iat", date.getTime());
// 过期时间10分钟(单位毫秒)
payload.put("ext", date.getTime() + 1000*60*10);
return JwtUtil.createToken(payload);
}
下面是前端ajax代码,在罗列代码前说下preflighted request
自定义header字段会导致一种叫做preflighted request的请求。
preflighted request在发送真正的请求前, 会先发送一个方法为OPTIONS的预请求(preflight request), 用于试探服务端是否能接受真正的请求,如果options获得的回应是拒绝性质的,比如404\403\500等http状态,就会停止post、put等请求的发出。
那么, 什么情况下请求会变成preflighted request呢?
1、请求方法不是GET/HEAD/POST
2、POST请求的Content-Type并非application/x-www-form-urlencoded, multipart/form-data, 或text/plain
3、请求设置了自定义的header字段
//登录ajax,登录成功后获取后台返回的token,并把token保存到cookie中
function signIn() {
let username = $("input[name='username']").val();
let password = $("input[name='password']").val();
$.ajax({
url: urlHead + "/user/login",
type: "GET",
dataType: "json",
data: {username: username, password: password},
success: function (result) {
//保存token用来判断用户是否登录,和身份是否属实
$.cookie('token', result.data);
}
})
}
//请求数据的ajax,需要从cookie读取token放入head传给后台。
function loadDeptTree() {
$.ajax({
// 自定义的headers字段,会出现option请求,在GET请求之前,后台要记得做检验。
headers: {
token: $.cookie('token')
},
url: urlHead + "/sys/dept/tree",
type: 'GET',
dataType: 'json',
success : function (result) {
}
})
}
注销账户或退出登录时就把所有cookie清除,不需要向后台验证。
// 注销,清空所有cookie(或者只清空保存着token的Cookie就行)
function logout() {
var keys = document.cookie.match(/[^ =;]+(?=\=)/g);
if(keys) {
for(var i = keys.length; i--;)
document.cookie = keys[i] + '=0;expires=' + new Date(0).toUTCString()
}
//返回登录页面或者主页
window.location.href = "signin.html";
}
以上就是JWT在前后端分离中的运用。还有很多细节有待完善。