前言
之前写过关于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: ,
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 filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = request.getHeaders();
Flux body = request.getBody();
MultiValueMap cookies = request.getCookies();
MultiValueMap 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
还有一个就是监听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 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
退出登录
之前两篇文章都忘了写了,这里补充一下退出操作吧,下面是具体的思路:
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失效了,这个概率非常之小,所以也可以放心使用,但是还是有人觉得不行,肯定会报错,是啊,就算真的发生也会报错的,前端处理报错退出当前页面跳转到登录页面即可。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net