从网上摘录了一些关于 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是相对独立的。

附上一个流程图

java前后端分离说明算法 前后端分离 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在前后端分离中的运用。还有很多细节有待完善。