nest实现扫码登录

509 阅读4分钟

nest实现扫码登录

首先初始化一个 nest 项目

nest n qrcode-login

安装下需要使用到的包

pnpm install qrcode @types/qrcode

在前一帖中提到过,实现扫码登录是需要后端提供三个接口

  • 获取二维码key
  • 依据二维码key生成二维码的 url
  • 检测二维码的状态

添加一个路由生成图片返回 key 和二维码的 url

这里为了简单一点就全部返回了

code.png

使用postman 测试返回数据了

PixPin_2025-05-11_15-49-32.png

初始化一个 前端项目,把二维码展示出来

pnpm create vite
import { FC, useEffect, useState } from "react";

import { HomeWrapper } from "./style";
import { getCodeImg } from "@/apis";

const Home: FC = () => {
  const [codeImg, setCodeImg] = useState<string>("");

  const initCodeImg = async () => {
    const { code_url } = await getCodeImg();

    setCodeImg(code_url);
  };

  useEffect(() => {
    initCodeImg();
  }, []);
  return (
    <HomeWrapper>
      <div className="home">
        <img src={codeImg} alt="" />
      </div>
    </HomeWrapper>
  );
};

export default Home;

这里就简单一点直接使用 img 标签展示

PixPin_2025-05-11_16-13-31.png

可以看到成功展示出来了(如果发现发送了两次请求,去到根组件把 react 的严格模式去掉即可解决)

使用手机扫码之后出现的字符就是随机生成的 uuid

微信图片_20250511162331.jpg

其实我们使用二维码解析工具解析出来的是一个网址

PixPin_2025-05-11_16-29-14.png

使用不同的软件扫描出现的界面不一样,本软件是确认登录,浏览器或是微信扫描出来的是软件的下载界面或是提示你使用软件扫描才能登录

这也是正常的不然随便一个软件都能扫描二维码登录了谁还会下载你的软件呢?

微信图片_20250511163428.jpg

微信图片_20250511163437.jpg

那么是怎么做到的呢?

其实就是访问的一个确认登录的页面

我们修改一下前端的页面,添加一个路由

const routes: RouteObject[] = [
  {
    path: "/",
    element: <Layout />,
    children: [
      {
        path: "",
        element: <Home />,
      },
      {
        path: 'confirm',
        element: <Confirm />,
      }
    ],
  },

  {
    path: "*",
    element: <NotFount />,
  },
];

简单的写一下这个页面

import { memo, useEffect } from "react";
import type { FC, ReactNode } from "react";

import { Button, message } from "antd";

import ConfirmWrapper from "./style";
import { codeUpdate, confirmLogin, cancelLogin as cancelLoginApi } from "@/apis";

interface IProps {
  children?: ReactNode;
}

const params = new URLSearchParams(window.location.search.slice(1));

const uid = params.get("uid");

const Confirm: FC<IProps> = () => {
  const [messageApi, contextHolder] = message.useMessage();

  useEffect(() => {
    // 初始进入确认界面说明已经完成扫码
    updateCodeStatus();
  }, []);

  const toLogin = () => {
    confirmLogin(uid);
    messageApi.open({
      content: "登录成功",
      type: "success",
      duration: 1,
    });
  };

  const cancelLogin = () => {
    cancelLoginApi(uid);
    messageApi.open({
      content: "登录已取消",
      type: "error",
      duration: 1,
    });
  };

  const updateCodeStatus = () => codeUpdate(uid);

  return (
    <ConfirmWrapper>
      <div className="flex flex-col justify-end h-screen">
        {contextHolder}
        <div className="flex-1 flex-center text-2xl text-[#1E80FF]">
          确认登录***网站吗?
        </div>
        <div className="px-3">
          <Button type="primary" block onClick={toLogin}>
            确认登录
          </Button>
          <Button block onClick={cancelLogin} className="my-4">
            取消
          </Button>
        </div>
      </div>
    </ConfirmWrapper>
  );
};

export default memo(Confirm);

PixPin_2025-05-11_16-59-04.png

我们在手机上如何预览电脑上的界面呢?

webpack 或是 vite 项目都可以配置一下开发服务器的选项

PixPin_2025-05-12_19-55-27.png

手机扫码效果

微信图片_20250512163556.jpg

我们看下效果

PixPin_2025-05-12_16-49-18.gif

下面就是当我们点击登录的时候,拿到这边的登录状态,取出用户信息

后端 通过 jwt 实现,我们给它加上

安装jwt

pnpm install @nestjs/jwt

app.module.ts 里面导入一下

PixPin_2025-05-12_17-38-35.png

设置一下密钥和 token 的有效时长

然后在 controller 里面加上两个接口,一个用于登录,一个登陆后依据 token 获取用户信息

code3.png

PixPin_2025-05-12_17-49-56.png

PixPin_2025-05-12_17-48-50.png

然后我们在确认界面加上登录逻辑

import { memo, useEffect } from "react";
import type { FC, ReactNode } from "react";
import { useNavigate } from "react-router-dom";

import { Button, message } from "antd";

import ConfirmWrapper from "./style";
import { codeUpdate, confirmLogin, cancelLogin as cancelLoginApi, login } from "@/apis";

interface IProps {
  children?: ReactNode;
}

const params = new URLSearchParams(window.location.search.slice(1));

const uid = params.get("uid");

const Confirm: FC<IProps> = () => {
  const [messageApi, contextHolder] = message.useMessage();
  const navigate = useNavigate();

  useEffect(() => {
    // 初始进入确认界面说明已经完成扫码
    updateCodeStatus();
  }, []);

  const toLogin = async () => {
    const params = {
      username: "admin",
      password: 123456,
    };

    const {
      data: { token },
    } = await login(params);
    // debugger;
    localStorage.setItem("user-token", token);

    navigate("/result");

    confirmLogin(uid);

    messageApi.open({
      content: "登录成功",
      type: "success",
      duration: 1,
    });
  };

  const cancelLogin = () => {
    cancelLoginApi(uid);
    messageApi.open({
      content: "登录已取消",
      type: "error",
      duration: 1,
    });
  };

  const updateCodeStatus = () => codeUpdate(uid);

  return (
    <ConfirmWrapper>
      <div className="flex flex-col justify-end h-screen">
        {contextHolder}
        <div className="flex-1 flex-center text-2xl text-[#1E80FF]">
          确认登录***网站吗?
        </div>
        <div className="px-3">
          <Button type="primary" block onClick={toLogin}>
            确认登录
          </Button>
          <Button block onClick={cancelLogin} className="my-4">
            取消
          </Button>
        </div>
      </div>
    </ConfirmWrapper>
  );
};

export default memo(Confirm);

然后在添加一跳转的结果页面

新建一个result 页面(别忘了,添加路由)

import { memo, useEffect, useState } from "react";
import type { FC, ReactNode } from "react";

interface IProps {
  children?: ReactNode;
}

import ResultWrapper from "./style";
import { getUserInfo } from "@/apis";

const Result: FC<IProps> = () => {
  const [info, setInfo] = useState<any>();

  useEffect(() => {
    getUserInfo().then((res) => {
      const {
        data: { username },
      } = res;

      setInfo(username);
    });
  });

  return (
    <ResultWrapper>
      欢迎回来<br></br>当前用户为:
      <span className="font-900 text-[25px] color-red">{info}</span>
    </ResultWrapper>
  );
};

export default memo(Result);

当我们在点击登录的时候调用 login 的登录,这时候会返回用户的 token ,我们存到本地

然后跳转到结果页面的时候,取出来查询用户信息,展示出来

PixPin_2025-05-12_17-48-50.png 在登录的时候添加上对应用户的 token,这里的 axios 的封装借鉴于 王红元(coderwhy) 老师使用的 class 类进行的封装,这里涉及到了主机的本地地址和局域网下手机的请求,主机上的生成二维码和查看二维码状态的基地址是本地的,而手机上点击确认取消是访问的主机的ipv4地址

使用类来封装的好处就体现出来了,可以对应的 new 两个不同的请求实例,它们分别设置不同的请求基地址

最终效果

PixPin_2025-05-12_21-13-14.gif

face106.gif