AI 摘要

文章基于 Solon + EasyQuery + ElementPlus 实现后台管理系统的登录、注销与异常处理。后端通过 SaToken 完成认证、Token 生成与 Redis 存储,支持统一异常与参数校验;前端用 ElementPlus 构建登录页与嵌套路由后台首页,Axios 携带 Token 获取用户信息并持久化至 Pinia,实现前后端完整认证闭环。

登录与注销接口

登录接口

核心注意事项:

  • 根据前端传递给后端的账户账号查询用户对象,用户对象为空,即账户账号不存在。
  • 前端传递给后端的密码是明文密码,需要结合数据库中存储的账户盐值进行 MD5Salts 加密,加密结果与数据库中存储的密文账户密码进行判断,一致则密码正确,否则密码错误。
  • 账户状态校验,数据库中存储的账户状态不为 1 时即被禁用,禁止登录。
  • 各项校验通过即登录成功,保存登录日志,调用上下文对象 Context、客户端 IP 地址工具类获取 IP 信息、IP 地址。
  • 由 SaToken 执行登录操作,并生成 Token 返回给前端,前端将 Token 保存下来,后续前端发送请求后端接口的时候需要在请求头上携带 Token 以验证登录状态。
  • 登录成功后,SaToken 会写入 Token 信息到 Redis 数据库中。

编写权限认证控制器(cn.duozai.sadmin.controller.PassportController),提供登录方法:

/**
 * 权限认证控制器
 * @visduo
 */
@Mapping("/passport")
@Controller
public class PassportController {

    @Db
    EasyEntityQuery easyEntityQuery;

    /**
     * 登录
     * @visduo
     *
     * @param ctx 上下文对象
     * @param username 账户账号
     * @param password 账户密码
     * @return 登录结果
     */
    @Post
    @Mapping("/login")
    public ResponseResult login(Context ctx, String username, String password) {
        // 根据账户账号查询用户对象
        UsersEntity usersEntity = easyEntityQuery.queryable(UsersEntity.class)
                .where(u -> {
                    // 条件:u.username = #{username}
                    u.username().eq(username);
                })
                .firstOrNull();

        if (usersEntity == null) {
            // 用户对象不存在,返回失败信息
            return ResponseResult.failure("用户不存在", null);
        }

        // 密码校验
        // 判断条件:前端传递的明文密码+盐值加密结果 ?= 数据库存储的密文密码
        String md5SaltsPassword = MD5SaltsUtil.md5(password, usersEntity.getSalts());
        if (!md5SaltsPassword.equals(usersEntity.getPassword())) {
            // 密码错误,返回失败信息
            return ResponseResult.failure("密码错误", null);
        }

        // 账户状态校验,status = 1正常/0禁用
        if (usersEntity.getStatus() != 1) {
            return ResponseResult.failure("用户被禁用", null);
        }

        // 全部校验通过,登录成功
        // 保存登录日志
        LoginlogEntity loginlogEntity = new LoginlogEntity();
        loginlogEntity.setUserId(usersEntity.getId());
        loginlogEntity.setIp(ctx.realIp());
        // 调用客户端IP地址工具类根据IP信息获取IP地址
        loginlogEntity.setAddress(ClientipUtil.parse(ctx.realIp()));
        // 调用HuTool工具类获取当前时间戳
        loginlogEntity.setTimestamp(DateUtil.currentSeconds());
        // 插入登录日志
        easyEntityQuery.insertable(loginlogEntity).executeRows();

        // SaToken执行登录并生成Token
        StpUtil.login(usersEntity.getId());
        String token = StpUtil.getTokenValue();

        // 返回结果,并将Token返回给前端
        return ResponseResult.success("登录成功", token);
    }

}

在 Apifox 中测试:

Redis 中写入 SaToken 数据:

注销接口

核心注意事项:

  • 由 SaToken 执行注销操作,注销操作执行后,Token 失效。
  • 在 Apifox 中测试时,请求头 Headers 需要携带 Token 参数,参数名与在 Solon 项目配置文件(classpath:/app.yml)中配置的 Token 名字一致,参数值为登录后后端返回的 Token。
  • 在 Apifox 中,需要关闭全局 Cookie 以模拟前后端分离操作。

编写权限认证控制器(cn.duozai.sadmin.controller.PassportController),提供注销方法:

/**
 * 权限认证控制器
 * @visduo
 */
@Mapping("/passport")
@Controller
public class PassportController {

    // ...

    /**
     * 注销
     * @visduo
     *
     * @return 注销结果
     */
    @Post
    @Mapping("/logout")
    public ResponseResult logout() {
        // 注销
        StpUtil.logout();
        return ResponseResult.success("注销成功", null);
    }

}

在 Apifox 中测试:

登录状态认证

原则上,未登录的情况下不能访问其他要求登录后才能访问的后端接口(如注销接口、获取数据接口等),可以借助 SaToken 提供的路由拦截鉴权能力实现相关需求。

核心注意事项:

  • 参考文档:https://solon.noear.org/article/110
  • 在 SaToken 配置类中配置权限认证拦截器对象。
  • 设置拦截全部路径,并排除登录接口的路径,登录接口不需要登录状态认证。
  • 未登录时 SaToken 会抛出异常,根据异常类型封装不同的状态和结果返回给前端。
  • 在 SaToken 的前置拦截中,可以配置允许跨域。

编写 SaToken 配置类(cn.duozai.sadmin.config.SaTokenConfig),提供权限认证拦截方法:

/**
 * SaToken配置类
 * @visduo
 */
@Configuration
public class SaTokenConfig {

    // ...

    /**
     * 构建权限认证拦截器对象
     * @visduo
     *
     * 参考文档:https://solon.noear.org/article/110
     * 参考文档:https://sa-token.cc/doc.html#/use/route-check
     *
     * @return 权限认证拦截器对象
     */
    @Bean
    public SaTokenInterceptor saTokenInterceptor() {
        return new SaTokenInterceptor()
                // 设置拦截路径
                .addInclude("/**")
                // 认证函数:每次访问进入
                .setAuth(r -> {
                    // 登录认证
                    SaRouter
                            // 设置需要登录认证的路径
                            .match("/**")
                            // 设置不需要登录认证的路径
                            .notMatch("/passport/login")
                            // 认证方式 StpUtil::checkLogin 未登录会报错
                            .check(StpUtil::checkLogin);
                })
                // 异常处理函数
                .setError(e -> {
                    // 判断异常类型
                    if(e instanceof NotLoginException) {
                        // 错误类型是未登录NotLoginException,封装返回401状态
                        return new ResponseResult(401, "未登录", null);
                    } else if(e instanceof NotRoleException || e instanceof NotPermissionException) {
                        // 错误类型是无权限NotRoleException/NotPermissionException,封装返回403状态
                        return new ResponseResult(403, "暂无权限", null);
                    }

                    // 异常类型无法匹配,直接返回异常对象
                    return e;
                })
                // 前置函数:在路由之前调用,配置跨域
                .setBeforeAuth(obj -> {
                    SaHolder.getResponse()
                            .setHeader("Access-Control-Allow-Origin", "*")
                            .setHeader("Access-Control-Allow-Methods", "*")
                            .setHeader("Access-Control-Allow-Headers", "*");
                });
    }

}

在 Apifox 中测试:

获取登录用户信息

核心注意事项:

  • 根据当前登录的用户 ID 获取用户信息实体,包含关联部门实体和关联角色实体。
  • 当前登录的用户信息实体,可以存储在 SaToken 会话(Session)中,需要使用时直接从 SaToken 会话中取出。
  • 在登录方法中,可以把用户信息实体连带关联实体查询出来,登录成功后存入 SaToken 会话中。由于前置操作中,SaToken 整合了 Redis,会话数据会被存入 Redis 中。
  • 返回用户信息实体给前端时,需要忽略账户密码、账户盐值这两个属性,使用 Solon 的序列化插件 Snack3 中的 @ONodeAttr 注解忽略序列化属性。

编写权限认证控制器(cn.duozai.sadmin.controller.PassportController),完善登录方法:

/**
 * 权限认证控制器
 * @visduo
 */
@Mapping("/passport")
@Controller
public class PassportController {

    @Db
    EasyEntityQuery easyEntityQuery;

    /**
     * 登录
     * @visduo
     *
     * @param ctx 上下文对象
     * @param username 账户账号
     * @param password 账户密码
     * @return 登录结果
     */
    @Post
    @Mapping("/login")
    public ResponseResult login(Context ctx, String username, String password) {
        // 根据账户账号查询用户对象
        UsersEntity usersEntity = easyEntityQuery.queryable(UsersEntity.class)
                .where(u -> {
                    // 条件:u.username = #{username}
                    u.username().eq(username);
                })
                // 关联查询部门、角色实体
                .include(UsersEntityProxy::dept)
                .include(UsersEntityProxy::role)
                .firstOrNull();

        // ...

        // SaToken执行登录并生成Token
        StpUtil.login(usersEntity.getId());
        String token = StpUtil.getTokenValue();

        // 将用户对象存储在SaToken的会话Session中
        StpUtil.getSession().set("currentUser", usersEntity);

        // 返回结果,并将Token返回给前端
        return ResponseResult.success("登录成功", token);
    }

    // ...

}

编写用户表实体类(cn.duozai.sadmin.repository.UsersEntity),忽略属性:

/**
 * 账户密码
 */
@ONodeAttr(ignore = true)   // 忽略序列化
private String password;

/**
 * 账户盐值
*/
@ONodeAttr(ignore = true)   // 忽略序列化
private String salts;

编写权限认证控制器(cn.duozai.sadmin.controller.PassportController),提供获取登录用户信息方法:

/**
 * 权限认证控制器
 * @visduo
 */
@Mapping("/passport")
@Controller
public class PassportController {

    // ...

    /**
     * 获取登录用户信息
     * @visduo
     *
     * @return 登录用户信息实体
     */
    @Get
    @Mapping("/currentUser")
    public ResponseResult currentUser() {
        // 获取当前登录用户对象
        UsersEntity usersEntity = (UsersEntity) StpUtil.getSession().get("currentUser");
        return ResponseResult.success("查询成功", usersEntity);
    }

}

在 Apifox 中测试:

Redis 中写入 SaToken 数据:

后端优化进阶

请求参数校验

在业务的实现过程中,尤其是对外接口开发,我们需要对请求进行大量的验证并返回错误状态码和描述。

后端做请求参数校验是保障系统稳定性、安全性、数据一致性的核心环节,前端校验仅能提升用户体验,无法替代后端校验,因为前端逻辑可被绕过(如直接调用接口、篡改请求等),后端必须作为最后一道防线。

核心注意事项:

  • 参考文档:https://solon.noear.org/article/49
  • 控制器上添加 @Valid 注解,以开启参数校验能力。
  • 控制器方法接收的参数前,根据需要添加校验注解。
  • 参数校验未通过,会抛出 ValidatorException 异常,需要自行捕获异常并进行友好返回。

编写权限认证控制器(cn.duozai.sadmin.controller.PassportController),完善请求参数校验:

/**
 * 权限认证控制器
 * @visduo
 *
 * @Valid:请求参数校验
 * 参考文档:https://solon.noear.org/article/49
 */
@Valid
@Mapping("/passport")
@Controller
public class PassportController {

    @Db
    EasyEntityQuery easyEntityQuery;

    /**
     * 登录
     * @visduo
     *
     * @param ctx 上下文对象
     * @param username 账户账号
     * @param password 账户密码
     * @return 登录结果
     */
    @Post
    @Mapping("/login")
    public ResponseResult login(Context ctx,
                                @NotBlank(message = "账户账号不能为空") String username,
                                @NotBlank(message = "账户密码不能为空") String password) {
        // ...
    }

}

在 Apifox 中测试:

统一异常处理

统一异常处理是后端开发中规范错误反馈、降低系统风险、提升可维护性的关键机制,核心是将分散在各层(如控制器、DAO 层、参数校验异常)的异常、未知的异常进行捕获、处理逻辑集中管理,避免 到处 try-catch 的混乱,同时解决异常处理不一致、错误信息泄露、排查困难等问题。

核心注意事项:

编写统一异常处理过滤器(cn.duozai.sadmin.filter.ExceptionFilter),提供统一异常处理过滤方法:

/**
 * 统一异常处理过滤器
 * @visduo
 *
 * 参考文档:https://solon.noear.org/article/765
 */
@Component
public class ExceptionFilter implements Filter {

    /**
     * 统一异常处理过滤
     *
     * @param ctx 上下文对象
     * @param chain 过滤器链
     * @throws Throwable 抛出异常对象
     */
    @Override
    public void doFilter(Context ctx, FilterChain chain) throws Throwable {
        try {
            // 放行操作
            chain.doFilter(ctx);
        } catch (ValidatorException e){
            // ValidatorException:请求参数校验异常
            // 参考文档:https://solon.noear.org/article/49
            // 封装返回400状态
            ctx.render(new ResponseResult(400, e.getMessage(), null));
        } catch (StatusException e){
            // StatusException:请求状态异常
            // 参考文档:https://solon.noear.org/article/871
            // 封装返回对应状态
            ctx.render(new ResponseResult(e.getCode(), e.getMessage(), null));
        } catch (Throwable e) {
            // 封装返回500状态
            ctx.render(new ResponseResult(500, e.getMessage(), null));
        }
    }

}

在 Apifox 中测试:

前端布局

根组件

App.vue 作为整个应用的根组件,只需要保留 router-view。

编写根组件(App.vue),提供视图占位符:

<template>
    <router-view/>
</template>

登录组件

核心注意事项:

  • 编写路由配置文件,提供登录路由信息。
  • 登录表单需要结合 ElementPlus 提供表单数据校验,可通过 AI 进行调整。
  • 完善登录处理函数,在表单数据校验通过后发送 Axios 请求给后端接口,并判断登录结果,登录成功则跳转到后台首页。
  • 发送 Axios 请求时,请求参数使用 QS 转换为表单格式字符串。

编写路由配置文件(router/index.js),提供登录路由信息:

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
            // 登录
            path: '/login',
            component: () => import('@/views/Login.vue'),
        }
    ],
})

AI 生成组件提示词:

基于ElementPlus,生成登录页面,要求如下:
1、登录表单项包含账户账号、账户密码。
2、登录表单提供登录按钮、重置按钮。
3、登录表单提交前,需要进行表单验证,验证规则为账户账号和账户密码不能为空,且账户账号长度为6-18,账户密码长度为6-18。
4、严格遵循ElementPlus布局,不需要额外添加多余的样式。

编写登录组件(views/Login.vue),提供登录视图:

<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'

// 表单数据
const loginForm = reactive({
    username: '',
    password: ''
})

// 表单引用
const loginFormRef = ref()

// 表单验证规则
const loginRules = {
    username: [
        { required: true, message: '请输入账户账号', trigger: 'blur' },
        { min: 6, max: 18, message: '账户账号长度应为6-18个字符', trigger: 'blur' }
    ],
    password: [
        { required: true, message: '请输入账户密码', trigger: 'blur' },
        { min: 6, max: 18, message: '账户密码长度应为6-18个字符', trigger: 'blur' }
    ]
}

// 登录处理函数
const handleLogin = async () => {
    if (!loginFormRef.value) return

    await loginFormRef.value.validate((valid) => {
        if (valid) {
            // 这里执行实际的登录逻辑
            ElMessage.success('登录成功')
            console.log('登录信息:', loginForm)
        } else {
            ElMessage.error('请正确填写表单信息')
            return false
        }
    })
}

// 重置表单
const resetForm = () => {
    loginFormRef.value.resetFields()
}
</script>

<template>
    <el-container>
        <el-main>
            <el-row justify="center">
                <el-col :span="8">
                    <el-card>
                        <template #header>
                            <div class="card-header">
                                <span>用户登录</span>
                            </div>
                        </template>

                        <el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" label-width="80px" label-position="left">
                            <el-form-item label="账户账号" prop="username">
                                <el-input v-model="loginForm.username" placeholder="请输入账户账号"/>
                            </el-form-item>

                            <el-form-item label="账户密码" prop="password">
                                <el-input v-model="loginForm.password" type="password" placeholder="请输入账户密码" show-password/>
                            </el-form-item>

                            <el-form-item>
                                <el-button type="primary" @click="handleLogin">登录</el-button>
                                <el-button @click="resetForm">重置</el-button>
                            </el-form-item>
                        </el-form>
                    </el-card>
                </el-col>
            </el-row>
        </el-main>
    </el-container>
</template>

<style scoped>
.card-header {
    text-align: center;
    font-size: 18px;
    font-weight: bold;
}
</style>

在浏览器中测试:

视图组件样式,可根据实际需求进行调整,或借助 AI 工具优化。

完善登录组件(views/Login.vue),在执行实际的登录逻辑处,发送 Axios 请求进行登录:

// 登录处理函数
const handleLogin = async () => {
    if (!loginFormRef.value) return

    await loginFormRef.value.validate((valid) => {
        if (valid) {
            // 这里执行实际的登录逻辑
            // 发送Axios请求
            axios.post('/passport/login', qs.stringify(loginForm)).then((res) => {
                if (res.data.code === 200) {
                    ElMessage.success('登录成功')
                    // 跳转到后台首页
                    router.push('/admin/index')
                } else {
                    ElMessage.error(res.data.message)
                }
            }).catch((error) => {
                ElMessage.error(error.message)
            })
        } else {
            ElMessage.error('请正确填写表单信息')
            return false
        }
    })
}

在浏览器中测试:

后台首页组件

核心注意事项:

  • 编写路由配置文件,提供后台首页路由信息。
  • 后台的路由信息,使用嵌套路由模式,核心适配布局复用 + 子页面切换的场景,侧边栏 + 顶部导航的公共布局不变,只刷新中间的内容区,同时让路由结构和页面结构、提升开发和维护效率。
  • 后台首页、顶部页面显示的欢迎提示词,需要发送 Axios 请求给后端接口,获取当前登录的用户的信息,保存到数据仓库中,并在需要显示的组件处调用展示。
  • 发送 Axios 请求时,需要在请求头上携带 Token,因此登录完成后,需要先将 Token 存储到 localStorage 中,再次发送请求时,从 localStorage 中取出 Token 并放入请求头上。

编写路由配置文件(router/index.js),提供后台首页路由信息:

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
            // 登录
            path: '/login',
            component: () => import('@/views/Login.vue'),
        },
        {
            // 后台
            path: '/admin',
            // 使用嵌套路由
            children: [
                {
                    // 后台首页
                    path: 'index',
                    component: () => import('@/views/Index.vue'),
                }
            ]
        },
    ],
})

AI 生成组件提示词:

基于ElementPlus,生成后台页面,要求如下:
1、左侧菜单栏,展示多级菜单列表。
2、顶部导航栏,展示欢迎您xxx,下拉框跳转到修改信息、修改密码、登录日志、操作日志、注销。
3、底部展示版权信息。
4、中间部分显示欢迎您,xxx。

编写后台首页组件(views/Index.vue),提供后台首页视图:

<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'

// 模拟用户信息
const userInfo = ref({
    name: '管理员'
})

// 模拟菜单数据
const menuData = ref([
    {
        id: 1,
        title: '系统管理',
        children: [
            {
                id: 11,
                title: '用户管理',
                path: '/user'
            },
            {
                id: 12,
                title: '角色管理',
                path: '/role'
            },
            {
                id: 13,
                title: '菜单管理',
                path: '/menu'
            }
        ]
    },
    {
        id: 2,
        title: '业务管理',
        children: [
            {
                id: 21,
                title: '订单管理',
                path: '/order'
            },
            {
                id: 22,
                title: '产品管理',
                path: '/product'
            }
        ]
    },
    {
        id: 3,
        title: '数据统计',
        children: [
            {
                id: 31,
                title: '销售统计',
                path: '/sales'
            },
            {
                id: 32,
                title: '用户统计',
                path: '/user-statistics'
            }
        ]
    }
])
</script>

<template>
    <el-container class="layout-container">
        <!-- 左侧菜单栏 -->
        <el-aside width="200px">
            <el-menu default-active="1" class="el-menu-vertical" :router="true" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
                <el-menu-item index="0" disabled>
                    <span>后台管理系统</span>
                </el-menu-item>

                <template v-for="item in menuData" :key="item.id">
                    <el-sub-menu v-if="item.children && item.children.length > 0" :index="String(item.id)">
                        <template #title>
                            <span>{{ item.title }}</span>
                        </template>
                        <el-menu-item v-for="child in item.children" :key="child.id" :index="String(child.id)">
                            {{ child.title }}
                        </el-menu-item>
                    </el-sub-menu>

                    <el-menu-item v-else :index="String(item.id)">
                        <template #title>{{ item.title }}</template>
                    </el-menu-item>
                </template>
            </el-menu>
        </el-aside>

        <!-- 右侧内容区 -->
        <el-container>
            <!-- 顶部导航栏 -->
            <el-header class="layout-header">
                <div class="header-right">
                    <el-dropdown>
                        <template #default>
                            欢迎您,{{ userInfo.name }}
                            <el-icon class="el-icon--right">
                                <arrow-down />
                            </el-icon>
                        </template>
                        <template #dropdown>
                            <el-dropdown-menu>
                                <el-dropdown-item>修改信息</el-dropdown-item>
                                <el-dropdown-item>修改密码</el-dropdown-item>
                                <el-dropdown-item>登录日志</el-dropdown-item>
                                <el-dropdown-item>操作日志</el-dropdown-item>
                                <el-dropdown-item>注销</el-dropdown-item>
                            </el-dropdown-menu>
                        </template>
                    </el-dropdown>
                </div>
            </el-header>

            <!-- 主要内容区 -->
            <el-main class="layout-main">
                <div class="welcome-container">
                    <h2>欢迎您,{{ userInfo.name }}!</h2>
                    <p>祝您工作愉快!</p>
                </div>
            </el-main>

            <!-- 底部版权信息 -->
            <el-footer class="layout-footer">
                <div class="footer-content">
                    <p>© 2025 后台管理系统. All Rights Reserved.</p>
                </div>
            </el-footer>
        </el-container>
    </el-container>
</template>

<style scoped>
.layout-container {
    height: 100vh;
}

.el-aside {
    background-color: #545c64;
}

.el-menu-vertical {
    height: 100%;
    border: none;
}

.layout-header {
    background-color: #ffffff;
    color: #333;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding: 0 20px;
}

.header-right {
    display: flex;
    align-items: center;
}

.el-dropdown-link {
    cursor: pointer;
    color: #333;
    display: flex;
    align-items: center;
}

.layout-main {
    background-color: #f5f7fa;
    padding: 20px;
}

.welcome-container {
    background-color: #ffffff;
    padding: 40px;
    border-radius: 4px;
    text-align: center;
    box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}

.layout-footer {
    background-color: #ffffff;
    color: #666;
    text-align: center;
    padding: 20px;
    border-top: 1px solid #ebeef5;
}

.footer-content p {
    margin: 0;
}
</style>

在浏览器中测试:

编写登录组件(views/Login.vue),完善登录请求:

ElMessage.success('登录成功')
// 将Token存储到localStorage中
localStorage.setItem('authtoken', res.data.data)
// 跳转到后台首页
router.push('/admin/index')

创建当前登录用户数据仓库(store/currentUser.js),存储当前登录的登录用户信息:

export const useCurrentUserStore = defineStore('currentUser', () => {
    // 登录用户信息实体
    let currentUser = reactive({})

    // 设置登录用户信息实体
    function setCurrentUser(currentUser) {
        this.currentUser = currentUser
    }

    return { currentUser, setCurrentUser }
})

编写后台首页组件(views/Index.vue),在组件创建完成后,发送 Axios 请求获取当前登录的用户的信息:

// 从数据仓库中获取响应式对象currentUser
let { currentUser } = storeToRefs(useCurrentUserStore)

// 发送请求,获取登录用户信息实体
axios.get('/passport/currentUser', {
    // 发送请求时,添加请求头Header,传递Token
    headers: {
        'authtoken': localStorage.getItem('authtoken')
    }
}).then((res) => {
    if (res.data.code === 200) {
        // 将登录用户信息实体存储到数据仓库中
        useCurrentUserStore().setCurrentUser(res.data.data)
    } else {
        ElMessage.error(res.data.message)
    }
}).catch((error) => {
    ElMessage.error(error.message)
})

编写后台首页组件(views/Index.vue),调用数据仓库展示当前登录的用户的信息:

欢迎您,{{ useCurrentUserStore().currentUser.realname }}

在浏览器中测试: