登录分析,以及其他页面登录信息验证。
以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.LoginParamsAPI.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.