前言

之前写过对于next.js鉴权操作cookie的文章,Next.js-cookie鉴权+续期1和Next.js-cookie鉴权+续期2,最近chatgpt十分火,于是我问它next.js鉴权最佳实际,上面是它的答复:

  1. 设置正当的 cookie 有效期:正当的 cookie 有效期应该依据网站的特点来确定,通常倡议将其设置为肯定工夫内(如 30 分钟)。
    援用
  2. 定时刷新 cookie 的有效期:能够通过服务器端的 HTTP 拦截器来监测每个申请的 cookie 是否行将过期,当发现其行将过期时,立刻从新设置一个新的 cookie,并在响应头中返回给客户端。
    援用
  3. 前端优化提醒弹窗:通过前端 JavaScript 实现 cookie 弹窗过期揭示,将其联合到用户体验优化计划之中,提供清晰的提醒,帮忙用户解决 cookie 续期或是否敞开要求。
    援用
  4. 风控,管制 cookie 的数据权限:依据用户的操作行为等危险因素,为 cookie 设置不同的权限级别,限度 cookie 的数据权限。
    援用
  5. 应用 HTTPS 协定:采纳 HTTPS 协定,可保障传输过程中传输平安。 在实践中,续期策略须要依据不同网站的业务场景,以及用户的应用习惯,思考相应的细节状况。

于是我又问,cookie轮询操作是不是影响服务器性能,于是它答复我:

在用户沉闷期间,页面会始终更新并续期 cookie。为了查看 cookie 是否过期并续期它,能够在页面中应用 useEffect 监听 window 对象的 focus 事件,当用户回到页面时查看 cookie 是否过期。如果 cookie 快要过期,就续期它。

于是我又问,应用 useEffect 监听 window 对象的 focus 事件好还是轮询好,上面是它的答复:

应用 window.addEventListener("focus", handleFocus) 更好,因为它可能响应用户的交互行为,缩小不必要的拜访和资源耗费。
如果应用轮询,每隔固定的工夫距离就会向服务器发送申请,这可能会节约用户的工夫和网络带宽,对服务器也会造成额定的压力。而应用 window.addEventListener("focus", handleFocus) 到用户呈现交互行为时才会进行操作,这是更高效和精确的形式。

于是,我就又把之前的鉴权颠覆了,应用了chatgpt举荐的形式,分享给大家。

操作

前端操作

首先在布局页面监听用户的动作,而后调用验证cookie的操作,如果快要过期则返回以set-cookie的形式返回给前端浏览器中保留,否则不做解决,这样比轮询操作既简略又不便,又不会频繁发动申请耗费服务器性能。

layout.js

// 监听用户动作,如果页面被点击就申请cookie是否将要过期,如果是则返回新cookie,否则不做anything  useEffect(() => {    setMounted(true)    // 判断是否是客户端    if (process.browser && isLogin){      window.addEventListener("focus", handleFocus);      return () => {        window.removeEventListener("focus", handleFocus);      };    }  }, [])  // 验证cookie是否将要过期,如果是返回新cookie写入到浏览器  async function handleFocus(){    const res = await dispatch(refreshCookie())    if (res.payload.status === 40001){      confirm({        title: '登录已过期',        icon: <ExclamationCircleFilled />,        content: '您的登录已过期,请从新登录!',        okText: '确定',        cancelText: '勾销',        onOk() {          // 从新登录          location.href = '/login'        },        onCancel() {            // 刷新以后页面          location.reload()        },      });    }  }

咱们把之前操作中的axiosInstance.interceptors.response.use(function (response)代码全副移除掉,只剩下上面的代码:

axios.js

import axios from 'axios';axios.defaults.withCredentials = true;const axiosInstance = axios.create({  baseURL: `${process.env.NEXT_PUBLIC_API_HOST}`,  withCredentials: true,});export default axiosInstance;

这样所有页面每次在服务端执行getServerSideProps办法时,只须要传递cookie到axios的申请头中即可。
page.js

export const getServerSideProps = wrapper.getServerSideProps(store => async (ctx) => {  axios.defaults.headers.cookie = ctx.req.headers.cookie || null  // 判断申请头中是否有set-cookie,如果有,则保留并同步到浏览器中  // if(axios.defaults.headers.setCookie){  //   ctx.res.setHeader('set-cookie', axios.defaults.headers.setCookie)  //   delete axios.defaults.headers.setCookie  // }  return {    props: {          }  };});

后盾操作

首先是springgateway的代码,如下所示:

@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {    ServerHttpRequest request = exchange.getRequest();    ServerHttpResponse response = exchange.getResponse();    HttpHeaders headers = request.getHeaders();    Flux<DataBuffer> body = request.getBody();    MultiValueMap<String, HttpCookie> cookies = request.getCookies();    MultiValueMap<String, String> queryParams = request.getQueryParams();    logger.info("request cookie2={}", com.alibaba.fastjson.JSONObject.toJSON(request.getCookies()));    // 设置全局跟踪id    if (isCorrelationIdPresent(headers)) {        logger.debug("correlation-id found in tracking filter: {}. ", filterUtils.getCorrelationId(headers));    } else {        String correlationID = generateCorrelationId();        exchange = filterUtils.setCorrelationId(exchange, correlationID);        logger.debug("correlation-id generated in tracking filter: {}.", correlationID);    }    // 获取申请的URI    String url = request.getPath().pathWithinApplication().value();    logger.info("申请URL:" + url);    // 这些前缀的url不须要验证cookie    if (url.startsWith("/info") || url.startsWith("/websocket") || url.startsWith("/web/login") || url.startsWith("/web/refreshToken") || url.startsWith("/web/logout")) {        // 放行        return chain.filter(exchange);    }    logger.info("cookie ={}", cookies);    HttpCookie cookieSession = cookies.getFirst(SESSION_KEY);    if (cookieSession != null) {        logger.info("session id ={}", cookieSession.getValue());        String session = cookieSession.getValue();        // redis中保留cookie,格局:key: session_jti,value:xxxxxxx        // 从redis中获取过期工夫        long sessionExpire = globalCache.getExpire(session);        logger.info("redis key={} expire = {}", session, sessionExpire);        if (sessionExpire > 1) {            // 从redis中获取token信息            Map<Object, Object> result = globalCache.hmget(session);            String accessToken = result.get("access_token").toString();            try {                HashMap authinfo = getAuthenticationInfo(accessToken);                ObjectMapper mapper = new ObjectMapper();                String authinfoJson = mapper.writeValueAsString(authinfo);                // 留神:这里保留的key: user,value:userinfo保留到申请头中供上游微服务获取,否则获取用户信息失败                request.mutate().header(FilterUtils.USER, authinfoJson);                // 这个token有名无实了,要不要无所谓                request.mutate().header(FilterUtils.AUTH_TOKEN, accessToken);                return chain.filter(exchange);            } catch (Exception ex) {                logger.info("getAuthenticationName error={}", ex.getMessage());                // 如果获取失败则返回给前端错误信息                return getVoidMono(response);            }        }    }    // cookie不存在或redis中也没找到对应cookie的用户信息(阐明是假的cookie)    // 让cookie生效    setCookie("", 0, response);    // 阐明redis中的token不存在或曾经过期    logger.info("session 不存在或曾经过期");    return getVoidMono(response);}

还有一个就是监听focus事件调用的后盾接口办法,如下所示:

  /**     * 续期cookie过程     * 1、cookie key从新生成,并设置到浏览器     * 2、老的删除,创立新的redis key=xxx并保留token,工夫和cookie工夫雷同     *  留神:浏览器只发送key-name的cookie到后盾,而发送不了对应的过期工夫,我也不晓得为什么!     * @param request     * @param response     * @return     */    @GetMapping("/web/refresh")    public ResponseEntity<?> refresh(HttpServletRequest request, HttpServletResponse response) {        Cookie[] cookies = request.getCookies();        if (cookies != null) {            for (Cookie cookie : cookies) {                if (cookie.getName().equals(SESSION_KEY)) {                    logger.info("request cookie={}", cookie);                    String oldCookieKey = cookie.getValue();                    String newCookieKey = UUID.randomUUID().toString().replace("-", "");                    // redis中保留cookie,格局:key: session_jti,value:xxxxxxx                    // 从redis中获取过期工夫                    // 查问redis中是否有cookie对应的数据                    long sessionExpire = globalCache.getExpire(oldCookieKey);                    logger.info("redis.sessionExpire()={}", sessionExpire);                    // 如果有,则延期redis中的cookie                    // 新cookie:查看redis中是否小于10分钟,如果是,则从新生成新的30分钟的cookie给浏览器                    if (sessionExpire > 1 && sessionExpire < COOKIE_EXPIRE_LT_TIME) {                        logger.info("cookie快要过期了,我来续期一下");                        // 获取redis中保留的用户信息                        Map<Object, Object> result = globalCache.hmget(cookie.getValue());                        logger.info("request redis auth info={}", JSONObject.toJSON(result));                        if (result != null) {                            //cookie未过期,持续应用                            expireCookie(newCookieKey, COOKIE_EXPIRE_TIME, response);                            expireRedis(oldCookieKey, newCookieKey, result);                        }                    }else{                        logger.info("cookie没有过期");                    }                    return ResponseEntity.ok(new ResultSuccess<>(true));                }            }        }        return ResponseEntity.ok(new ResultSuccess<>(ResultStatus.AUTH_ERROR));    }    // 延期cookie    private void expireRedis(String oldCookieKey, String newCookieKey, Map<Object, Object> result) {        // redis设置该key的值立刻过期        //time要大于0 如果time小于等于0 将设置无限期        globalCache.expire(oldCookieKey, 1);        // 转化result        Map<String, Object> newResult = (Map) result;        // 保留到redis中        globalCache.hmset(newCookieKey, newResult, COOKIE_EXPIRE_TIME);    }    // 延期cookie    private void expireCookie(String cookieValue, Integer cookieTime, HttpServletResponse httpServletResponse) {        ResponseCookie cookie = ResponseCookie.from(SESSION_KEY, cookieValue) // key & value                .httpOnly(true)        // 禁止js读取                .secure(true)        // 在http下也传输                .domain(serviceConfig.getDomain())// 域名                .path("/")            // path,过期用秒,不过期用天                .maxAge(Duration.ofSeconds(cookieTime))                .sameSite("Lax")    // 大多数状况也是不发送第三方 Cookie,然而导航到指标网址的 Get 申请除外                .build();        httpServletResponse.setHeader(HttpHeaders.SET_COOKIE, cookie.toString());    }

退出登录

之前两篇文章都忘了写了,这里补充一下退出操作吧,上面是具体的思路:
1、调用服务器端接口,接口中删除cookie,其实就是返回的set-cookie中时效为0
2、后盾接口返回之后,浏览器中的cookie即可删除,这时页面跳转到登录页面即可

具体代码如下所示:

前端js代码:

// 只有服务器端能力革除httponly的cookieawait dispatch(logout())// 革除完之后立马跳转到登录页面location.href = '/login'

后盾java代码:

    /**     * 退出登录     *     * @param request     * @param response     */    @PostMapping("/web/logout")    public void refreshToken(HttpServletRequest request, HttpServletResponse response) {        Cookie[] cookies = request.getCookies();        if (cookies.length > 0) {            // 遍历数组            for (Cookie cookie : cookies) {                if (cookie.getName().equals("session_jti")) {                    String value = cookie.getValue();                    logger.info("cookie session_jti={}", value);                    if (StringUtils.hasLength(value)) {                        // 从redis中删除                        globalCache.del(value);                        ResponseCookie clearCookie = ResponseCookie.from("session_jti", "") // key & value                                .httpOnly(true)        // 禁止js读取                                .secure(true)        // 在http下也传输                                .domain(serviceConfig.getDomain())// 域名                                .path("/")            // path                                .maxAge(0)    // 1个小时候过期                                .sameSite("None")    // 大多数状况也是不发送第三方 Cookie,然而导航到指标网址的 Get 申请除外                                .build();                        // 设置Cookie到返回头Header中                        response.setHeader(HttpHeaders.SET_COOKIE, clearCookie.toString());                    }                }            }        }    }

这样就实现了Next.js的鉴权、cookie续期和退出的所有操作了!

留神

1、当客户端浏览器应用axios申请接口时,会主动把cookie带到后盾
2、当客户端浏览器应用axios申请接口时,主动把后盾返回的set-cookie保留到浏览器中
3、前端浏览器js不能操作httponly的相干cookie,只有服务端才行
4、设置成securecookie只能本地localhosthttps协定能力应用
5、在getServerSideProps办法中应用axios时,axios申请头中是不存在cookie的,所以须要将context中的cookie手动设置到axios的申请头中,如下:

axios.defaults.headers.cookie = ctx.req.headers.cookie || null

6、在getServerSideProps办法中应用axios后,保留在axios申请头中的set-cookie不会主动写入到浏览器中,须要取出来放到context中,如下:

ctx.res.setHeader('set-cookie', axios.defaults.headers.setCookie)

总结

1、之前的文章是在axiosInstance.interceptors.response.use(function (response)中拼接cookie,然而没有下面的不便,可能有的人会放心这个focus会不会反复调用接口影响性能?我能够释怀跟大家讲,这个focus只有第一次才失效,当你切换到其它利用再回来了才从新调用。
2、这里页面刷新的时候调用getServerSideProps办法可能会有三种后果:

a、没有认证的cookie,
b、有认证的cookie,
c、处于有和没有之间。

a和b没啥好说的,c的状况比拟非凡,比方getServerSideProps之中有三个接口,当执行第1个接口时安然无恙,因为处于有效期内,当执行第2的接口时,发现认证的cookie生效了,这个概率十分之小,所以也能够放心使用,然而还是有人感觉不行,必定会报错,是啊,就算真的产生也会报错的,前端解决报错退出以后页面跳转到登录页面即可。