共计 8715 个字符,预计需要花费 22 分钟才能阅读完成。
前言
之前写过对于 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 的代码,如下所示:
@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、设置成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 生效了,这个概率十分之小,所以也能够放心使用,然而还是有人感觉不行,必定会报错,是啊,就算真的产生也会报错的,前端解决报错退出以后页面跳转到登录页面即可。