登录分析,以及其他页面登录信息验证。
以Ant Design Pro为例
思路分析
这里是使用async来实现
const handleSubmit = async (values: API.LoginParams) => {
const msg = await login({ ...values, type });
}
这里的Login登录函数存储在/src/services/ant-design-pro/api.ts
中
所有的登录函数都进行了统一封装,如下
// /src/service/ant-design-pro/api.ts
import { request } from 'umi';
/** 登录接口 POST /api/login/account */
export async function login(body: API.LoginParams, options?: { [key: string]: any }) {
return request<API.LoginResult>('/api/login/account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
这里的API.LoginParams
和API.LoginResult
是参数结构,参数结构存储在和api.ts同层级的typings.d.ts
中
declare namespace API{
type LoginParams = {
username?: string;
password?: string;
autoLogin?: boolean;
type?: string;
};
type LoginResult = {
status?: string;
type?: string;
currentAuthority?: string;
};
}
然后根据返回参数msg
const defaultLoginSuccessMessage = intl.formatMessage({
id: 'pages.login.success',
defaultMessage: '登录成功!',
});
message.success(defaultLoginSuccessMessage);//展示信息成功
await fetchUserInfo();
/** 此方法会跳转到 redirect 参数所在的位置 */
这里发现,你会发现成功后直接去获取个人信息了,并没有存储token,这是为什么呢?
因为本地mock的数据并没有token这种东西,所以这里并没有执行判断,而是直接去获取用户信息
那么浏览器是如何判断用户是否真正登录的呢?
我们跟着去找fetchUserInfo
// /src/pages/user/Login/index.ts
import { useIntl, history, FormattedMessage, SelectLang, useModel } from 'umi';
const Login: React.FC = () => {
const { initialState, setInitialState } = useModel('@@initialState');
const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
await setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
}
};
}
这里我们看到了一个userModel
,这是一个用于进行初始化的地方,具体的用法可见官方文档
这里的useModel
是在src/app.tsx
更新的,我们现在去看看
// src/app.tsx
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
const fetchUserInfo = async () => {
try {
const msg = await queryCurrentUser();
return msg.data;
} catch (error) {
history.push(loginPath);
}
return undefined;
};
// 如果不是登录页面,执行
if (history.location.pathname !== loginPath) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
currentUser,
settings: defaultSettings,
};
}
return {
fetchUserInfo,
settings: defaultSettings,
};
}
看着特别难以理解但是解释一下,getInitialState
函数后会返回一个Promise对象,Promise对象的返回结果如图。
getInitialState
函数是在项目初始化执行的,如果你第一次登录该页面,这里并没有任何数据,则开始初始化数据。
fetchUserInfo
则会去获取用户信息,如果返回信息成功,则证明能够查询用户信息
这里有一个queryCurrentUser
,这个接口又是干什么的呢?
这个接口就是简单的查询用户信息的,也没有做任何身份判断…
好像很难理解,但总结一下就是这里是产生初始化信息(全局状态),不过看起来好像并没有进行token的存储之类的,这个我们可以放到之后说。
下面的代码则是,如果我们之前从其他界面跳转过来,则会跳转回之前的页面
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const { query } = history.location;
const { redirect } = query as { redirect: string };
history.push(redirect || '/');
return;
然后就是提示登录失败的错误信息
console.log(msg);
// 如果失败去设置用户错误信息
setUserLoginState(msg);
还有捕获异常并提示
const defaultLoginFailureMessage = intl.formatMessage({
id: 'pages.login.failure',
defaultMessage: '登录失败,请重试!',
});
message.error(defaultLoginFailureMessage);
然后我们再看全局的提交函数
const handleSubmit = async (values: API.LoginParams) => {
try {
// 登录
const msg = await login({ ...values, type });
if (msg.status === 'ok') {
const defaultLoginSuccessMessage = intl.formatMessage({
id: 'pages.login.success',
defaultMessage: '登录成功!',
});
message.success(defaultLoginSuccessMessage);
await fetchUserInfo();
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const { query } = history.location;
const { redirect } = query as { redirect: string };
history.push(redirect || '/');
return;
}
console.log(msg);
// 如果失败去设置用户错误信息
setUserLoginState(msg);
} catch (error) {
const defaultLoginFailureMessage = intl.formatMessage({
id: 'pages.login.failure',
defaultMessage: '登录失败,请重试!',
});
message.error(defaultLoginFailureMessage);
}
};
好像除了 fetchUserInfo以外,其他都很好理解。
不过好像有一个关键问题并没有解决,如果我没有登录用户,直接去访问内容,是如何做到跳转到登录页的呢?
我们不妨先去退出登录接口看看,退出登录接口更改了什么导致出现了这个问题
// /src/components/RogjtCpmtent/AvatarDropdown.tsx
const loginOut = async () => {
await outLogin();
const { query = {}, search, pathname } = history.location;
console.log(history.location)
const { redirect } = query;
// Note: There may be security issues, please note
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: stringify({
redirect: pathname + search,
}),
});
}
};
const onMenuClick = useCallback(
(event: MenuInfo) => {
const { key } = event;
if (key === 'logout') {
setInitialState((s) => ({ ...s, currentUser: undefined }));
loginOut();
return;
}
history.push(`/account/${key}`);
},
[setInitialState],
);
很显然,从上面的案例可以确定,是根据initialState中的currentUser来判断是否登录用户的。
那么是在哪个地方判断的呢。
我们全局找到了,在mock数据中的user.ts,如果获取失败了则会返回请先登录。下一步,找在哪里调用了这个接口
export default {
// 支持值为 Object 和 Array
'GET /api/currentUser': (req: Request, res: Response) => {
if (!getAccess()) {
res.status(401).send({
data: {
isLogin: false,
},
errorCode: '401',
errorMessage: '请先登录!',
success: true,
});
return;
}
}
}
那么在哪里获取的msg信息和跳转呢?经过全局的搜索和排查,最后又回到了那个组件src/app.tsx
// src/app.tsx
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
const fetchUserInfo = async () => {
try {
const msg = await queryCurrentUser();
return msg.data;
} catch (error) {
//在这里进行的路由跳转
history.push(loginPath);
}
return undefined;
};
// 如果不是登录页面,执行
if (history.location.pathname !== loginPath) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
currentUser,
settings: defaultSettings,
};
}
return {
fetchUserInfo,
settings: defaultSettings,
};
}
当queryCurrentUser发送请求时,因为没有身份验证,所以返回401错误,然后在这里被捕获,更改数据。
那么又有一个问题,msg是在哪里提示的呢?
全局查找之后会发现,其实umi自己封装了请求,如果报错,会帮我们全局提示。
总结
整个项目的登录验证逻辑如下,因为是一个示例项目,mock存储了一个access,用来存储用户的状态,用户是管理员还是用户。当用户使用浏览器地址来请求的时候,因为配置了 @umijs/plugin-initial-state
插件,就会执行app.tsx
中的初始化数据,初始化数据会执行fetchUserInfo
(获取用户信息)请求,这个接口会在后端判断access,如果access为空(即之前并没有登录过),则会返回401错误,即在初始化的时候就执行了history.push('user/Login')
,并且借用umi封装的请求来提示登录失败。
这个是一个示例项目,在实战中一般使用token来验证,这其实也比较简单,把token存到localstorage中,然后每次请求的时候封装一个获取token的方法,进行返回即可。
Q.E.D.