欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JWT在前后端分离中的运用实例

程序员文章站 2022-06-13 20:13:27
...

从网上摘录了一些关于 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是相对独立的。
附上一个流程图
JWT在前后端分离中的运用实例
JWT详细说明,参考博客
https://blog.csdn.net/why15732625998/article/details/78534711

下面就是运用实例基于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核心代码
参考博客https://blog.csdn.net/baidu_38532123/article/details/79211042

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字段
参考博客https://blog.csdn.net/cc1314_/article/details/78272329

//登录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在前后端分离中的运用。还有很多细节有待完善。

相关标签: jwt