关于next.js:NextjsCookie鉴权续期退出登录

6次阅读

共计 8715 个字符,预计需要花费 22 分钟才能阅读完成。

前言

之前写过对于 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 的代码,如下所示:

@Override
public 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 的 cookie
await 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 生效了,这个概率十分之小,所以也能够放心使用,然而还是有人感觉不行,必定会报错,是啊,就算真的产生也会报错的,前端解决报错退出以后页面跳转到登录页面即可。

正文完
 0