前言
之前写过对于next.js鉴权操作cookie的文章,Next.js-cookie鉴权+续期1和Next.js-cookie鉴权+续期2,最近chatgpt十分火,于是我问它next.js鉴权最佳实际,上面是它的答复:
- 设置正当的 cookie 有效期:正当的 cookie 有效期应该依据网站的特点来确定,通常倡议将其设置为肯定工夫内(如 30 分钟)。
援用- 定时刷新 cookie 的有效期:能够通过服务器端的 HTTP 拦截器来监测每个申请的 cookie 是否行将过期,当发现其行将过期时,立刻从新设置一个新的 cookie,并在响应头中返回给客户端。
援用- 前端优化提醒弹窗:通过前端 JavaScript 实现 cookie 弹窗过期揭示,将其联合到用户体验优化计划之中,提供清晰的提醒,帮忙用户解决 cookie 续期或是否敞开要求。
援用- 风控,管制 cookie 的数据权限:依据用户的操作行为等危险因素,为 cookie 设置不同的权限级别,限度 cookie 的数据权限。
援用- 应用 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、设置成secure
的cookie
只能本地localhost
和https
协定能力应用
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生效了,这个概率十分之小,所以也能够放心使用,然而还是有人感觉不行,必定会报错,是啊,就算真的产生也会报错的,前端解决报错退出以后页面跳转到登录页面即可。