个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang -
Overview
前言
最近在做项目前端,使用的https://doc.vben.pro/,在登录过期时出现了无法再次登录的问题,在此记录一下。
项目前面那些直接略过,如果感兴趣直接看官方文档就可以
,以下会根据解决过程附带部分官网说明。
分析原因
梳理流程
文档前面的都已按照要求配置修改好了,所有的认证接口也都完成了,登录也是没有问题的,就是在accessToken
过期的时候无法回到登录页面,就算直接路由到/auth/login
也永远在转圈圈。
在本项目中,在登录完成后将accessToken
使用pinia
加持久化插件存储到localStorage中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
export async function initStores(app: App, options: InitStoreOptions) { const { createPersistedState } = await import('pinia-plugin-persistedstate'); pinia = createPinia(); const { namespace } = options; pinia.use( createPersistedState({ key: (storeKey) => `${namespace}-${storeKey}`, storage: localStorage, }), ); app.use(pinia); return pinia; }
...
persist: { pick: ['accessToken', 'refreshToken', 'accessCodes'], }, state: (): AccessState => ({ accessCodes: [], accessMenus: [], accessRoutes: [], accessToken: null, isAccessChecked: false, loginExpired: false, refreshToken: null, }), });
|
然后在每次请求时使用自定义配置了请求拦截器和响应拦截器的axios
中带上accessToken
,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function formatToken(token: null | string) { return token ? `Bearer ${token}` : null; }
client.addRequestInterceptor({ fulfilled: async (config) => { const accessStore = useAccessStore();
config.headers.Authorization = formatToken(accessStore.accessToken); config.headers['Accept-Language'] = preferences.app.locale; return config; }, });
client.addResponseInterceptor<HttpResponse>({ fulfilled: (response) => { const { data: responseData, status } = response;
const { code, data } = responseData; if (status >= 200 && status < 400 && code === 0) { return data; }
throw Object.assign({}, response, { response }); }, });
|
在此都没问题,因为使用登录和其他接口请求和响应都没问题,问题到底在哪?
后端命名也是正常接收到accessToken
了呀,为什么永远在转圈圈,回不去了呢?
那既然这样,只能使出我并没把握的前端debug
了。
debug
根据前面的[Vue Router warn]: Unexpected error when starting the router: {code: 401, msg: '账号未登录'}
的输出和错误栈,大概可以知道是在bootstrap.ts:33
出问题了,那么先在这断一下,结合代码判断,这一步就是设置路由啊,可知一定是初始化路由出错了,导致所有路由都出问题了。
那么在创建路由再加上断点,在export
之前操作router
都点一下
发现可以到下面的创建路由守卫处,说明前面都还没错,那么就继续分析路由守卫
在继续debug
中发现,在到达96
行之前就提示错误,开始转圈圈了,那么错误一定是89-96
之间的。
最后发现是在fetchUserInfo
后出错的,更准确的讲是getUserInfoApi
出错的。
1 2 3 4 5 6 7 8 9 10 11 12 13
| async function fetchUserInfo() { let userInfo: null | UserInfo = null; userInfo = await getUserInfoApi(); userStore.setUserInfo(userInfo); return userInfo; }
export async function getUserInfoApi() { return requestClient.get<UserInfo>('/user/info'); }
|
???不是已经正常返回了吗?我也提示账号未登录了啊,那一定是response
的问题,继续看axios
配置,这里设置三个拦截器,看其中token
过期的处理响应拦截器,正常应该走doReAuthenticate
重新认证逻辑啊,怎么不行呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| client.addResponseInterceptor<HttpResponse>({ fulfilled: (response) => { const { data: responseData, status } = response;
const { code, data } = responseData; if (status >= 200 && status < 400 && code === 0) { return data; }
throw Object.assign({}, response, { response }); }, });
client.addResponseInterceptor( authenticateResponseInterceptor({ client, doReAuthenticate, doRefreshToken, enableRefreshToken: preferences.app.enableRefreshToken, formatToken, }), );
client.addResponseInterceptor( errorMessageResponseInterceptor((msg: string, error) => { const responseData = error?.response?.data ?? {}; const errorMessage = responseData?.error ?? responseData?.msg ?? ''; message.error(errorMessage || msg); }), );
|
authenticateResponseInterceptor
响应拦截器细看,终于抓到你了,没有进入重新认证的逻辑是因为在
response?.status !== 401
为true
直接抛异常了。
啊啊啊啊,响应码不就是401
吗?好吧,还真不是。
根因
根因就是我使用了后端的统一响应处理,将所有异常都以正常的响应返回,也就是说并没有使用http
的响应码,所有能被后端处理的异常都会以响应体里的code
来表示。
1 2 3 4 5 6 7 8 9
|
@ExceptionHandler(NotLoginException.class) public CommonResult<Void> handleNotLoginException(NotLoginException e, HttpServletRequest request) { String requestUri = request.getRequestURI(); log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestUri, e.getMessage()); return CommonResult.error(UNAUTHORIZED); }
|
可是这个项目中使用了http
状态码,也是axios
的status
1 2 3 4 5 6 7 8
| export interface AxiosResponse<T = any, D = any> { data: T; status: number; statusText: string; headers: RawAxiosResponseHeaders | AxiosResponseHeaders; config: InternalAxiosRequestConfig<D>; request?: any; }
|
解决
解决起来也很简单,无非改造前端响应体或改在后端使用http
响应,我选择了后者。
像下面这样使用org.springframework.http.ResponseEntity;
再包装一下就可以了,
这样本次请求状态码就不是200
了,正常走认证逻辑了。
进度
最后提一下进度吧!忘了上次提到的进度是哪里了,那就从指标版本控制之后讲吧。
1、【重要】策略集和规则的版本控制,还没做完
2、【重要】调整了项目结构,优化组件和上下文,新增规则action
(发消息、打tag
、加名单等)
3、【一般】新增节点、字段逻辑等通用接口,修复一些问题
4、【重要】策略的权重模式,顺序&最坏&投票&权重,在计算权重和动态字段时使用QLExpress
5、【重要】放弃逻辑删除,新增数据接入elasticsearch
,增加docker-compose
配置
这些是后端的,前端也要开始,有三大挑战:1、条件组合组件;2、公式类输入时联想词;3、LiteFlow编排。
写在最后
拙作艰辛,字句心血,望诸君垂青,多予支持,不胜感激。
个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang -
Overview