本案例偏向业务实战,详细原理请根据参考资料、网上检索等自行学习。
.NET 8 后端
项目目录结构
项目的引用依赖链为:WebAPI => Service => Infrastructure => Model。
本案例中,Test 层不会用到。
EF Core 与 Identity
在 Model 层中安装 Microsoft.AspNetCore.Identity.EntityFrameworkCore
Nuget 包
在 Model.DbEntities 目录下新建 ApplicationRole
、ApplicationUser
、UserInfo
实体类
using Microsoft.AspNetCore.Identity;namespace CampusServicePlatform.Model.DbEntities{ public class ApplicationRole : IdentityRole<Guid> { }}
using Microsoft.AspNetCore.Identity;namespace CampusServicePlatform.Model.DbEntities{ public class ApplicationUser : IdentityUser<Guid> { public virtual UserInfo? UserInfo { get; set; } }}
namespace CampusServicePlatform.Model.DbEntities{ public class UserInfo { public int Id { get; set; } public string? Nickname { get; set; } public string? Avatar { get; set; } public DateTime? CreatedTime { get; set; } public Guid UserId { get; set; } public virtual ApplicationUser? User { get; set; } }}
在 Infrastructure 层中安装 Microsoft.EntityFrameworkCore.Design
、Microsoft.EntityFrameworkCore.SqlServer
、Microsoft.EntityFrameworkCore.Tools
NuGet 包
在 Infrastructure.DbEntityConfigs 目录下新建 ApplicationRoleEntityConfig
、ApplicationUserEntityConfig
、UserInfoEntityConfig
实体配置类
using CampusServicePlatform.Model.DbEntities;using Microsoft.EntityFrameworkCore;using Microsoft.EntityFrameworkCore.Metadata.Builders;namespace CampusServicePlatform.Infrastructure.DbEntityConfigs{ public class ApplicationRoleEntityConfig : IEntityTypeConfiguration<ApplicationRole> { public void Configure(EntityTypeBuilder<ApplicationRole> builder) { } }}
using CampusServicePlatform.Model.DbEntities;using Microsoft.EntityFrameworkCore;using Microsoft.EntityFrameworkCore.Metadata.Builders;namespace CampusServicePlatform.Infrastructure.DbEntityConfigs{ public class ApplicationUserEntityConfig : IEntityTypeConfiguration<ApplicationUser> { public void Configure(EntityTypeBuilder<ApplicationUser> builder) { } }}
using CampusServicePlatform.Model.DbEntities;using Microsoft.EntityFrameworkCore;using Microsoft.EntityFrameworkCore.Metadata.Builders;namespace CampusServicePlatform.Infrastructure.DbEntityConfigs{ public class UserInfoEntityConfig : IEntityTypeConfiguration<UserInfo> { public void Configure(EntityTypeBuilder<UserInfo> builder) { builder.Property(e => e.UserId).IsRequired(); // 指定外键 builder.HasOne(e => e.User).WithOne(e => e.UserInfo).HasForeignKey<UserInfo>(e => e.UserId).HasPrincipalKey<ApplicationUser>(e => e.Id); // 创建非聚集索引,加快 GUID 列连接查询速度 builder.HasIndex(e => e.UserId).IsUnique().IsClustered(false); } }}
在 Infrastructure.DbEntityConfigs 目录下新建 ApplicationDbContext
EF Core 数据库上下文
using CampusServicePlatform.Model.DbEntities;using Microsoft.AspNetCore.Identity.EntityFrameworkCore;using Microsoft.EntityFrameworkCore;namespace CampusServicePlatform.Infrastructure.DbEntityConfigs{ public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid> { public virtual DbSet<UserInfo> UserInfos { get; set; } public ApplicationDbContext() { } public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer("本地数据库连接字符串"); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); } }}
在 WebAPI.Program.cs 中配置 DbContext 与 Identity
此处的数据库连接字符串获取不再赘述,详情请移步另一篇文章查看:https://blog.csdn.net/Felix61Felix/article/details/134634047。
var builder = WebApplication.CreateBuilder(args);// ...builder.Services.AddDbContext<ApplicationDbContext>(options =>{ string? connectionString = builder.Configuration.GetConnectionString("Default"); options.UseSqlServer(connectionString);});builder.Services .AddIdentity<ApplicationUser, ApplicationRole>(options => { // 在这里仅要求简单的限制,即密码长度为 6,其他限制请自行配置 options.Password.RequiredLength = 6; options.Password.RequireDigit = false; options.Password.RequireLowercase = false; options.Password.RequireUppercase = false; options.Password.RequireNonAlphanumeric = false; }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders();// ...var app = builder.Build();
进入程序包管理控制台,将解决方案的启动项目、程序包管理控制台的默认项目都切换成 Infrastructure
在程序包管理控制台中依次输入 EF Core 迁移命令:Add-Migration InitialCreate -OutputDir DatabaseEntityConfig/_Migrations
、Update-database
,以生成数据库
JWT
在 Infrastructure 层中新建 JwtHelper
类
using CampusServicePlatform.Model.DbEntities;using Microsoft.AspNetCore.Identity;using Microsoft.Extensions.Configuration;using Microsoft.IdentityModel.Tokens;using System.IdentityModel.Tokens.Jwt;using System.Security.Claims;using System.Text;namespace CampusServicePlatform.Infrastructure{ public class JwtHelper { private readonly IConfiguration _configuration; private readonly UserManager<ApplicationUser> _userManager; public JwtHelper(IConfiguration configuration, UserManager<ApplicationUser> userManager) { _configuration = configuration; _userManager = userManager; } public string GenerateJwtToken(ApplicationUser? user, UserInfo? userInfo, IList<string>? roles) { var claims = new List<Claim> { new Claim(ClaimTypes.Name, user.UserName), new Claim("avatar", userInfo.Avatar), new Claim("nickname", userInfo.Nickname) }; foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value)); var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature); var jwtSecurityToken = new JwtSecurityToken( claims: claims, // 过期时间,单位是“分” expires: DateTime.Now.AddMinutes(60 * 24 * 7), notBefore: DateTime.Now, issuer: _configuration.GetSection("Jwt:Issuer").Value, audience: _configuration.GetSection("Jwt:Audience").Value, signingCredentials: signingCredentials ); var jwtToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); return jwtToken; } public async Task<bool> CheckJwtToken(string? jwtToken) { var jwtTokenHandler = new JwtSecurityTokenHandler(); var issuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value)); var validationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = issuerSigningKey, ValidateIssuer = true, ValidIssuer = _configuration.GetSection("Jwt:Issuer").Value, ValidateAudience = true, ValidAudience = _configuration.GetSection("Jwt:Audience").Value, ClockSkew = TimeSpan.Zero }; var principal = jwtTokenHandler.ValidateToken(jwtToken, validationParameters, out var securityToken); if (principal.Identity?.IsAuthenticated != true) { return false; } return true; } }}
在 WebAPI 层中编写 JWT 的配置文件
{ "Jwt": { "Key": "自行设置密钥,推荐写 GUID 值", "Issuer": "签发者,推荐写 API 地址", "Audience": "接收者,推荐写前端地址" }}
在 WebAPI.Program.cs 中配置 JWT
var builder = WebApplication.CreateBuilder(args);// ...builder.Services .AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateActor = true, ValidateIssuer = true, ValidateAudience = true, RequireExpirationTime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration.GetSection("Jwt:Issuer").Value, ValidAudience = builder.Configuration.GetSection("Jwt:Audience").Value, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration.GetSection("Jwt:Key").Value)) }; });builder.Services.AddScoped<JwtHelper>();// ...var app = builder.Build();
Service
在 Service 层新建 IAccountService
接口
using CampusServicePlatform.Model.DTO;namespace CampusServicePlatform.Service{ public interface IAccountervice { Task<bool> CheckJwtToken(string? jwtToken); Task<string?> SignIn(UserRequest userRequest); Task<bool> SingUp(UserRequest userRequest); }}
在 Service 层新建 AccountService
实现类
using CampusServicePlatform.Infrastructure;using CampusServicePlatform.Infrastructure.DbEntityConfigs;using CampusServicePlatform.Model.DbEntities;using CampusServicePlatform.Model.DTO;using CampusServicePlatform.Model.Enum;using Microsoft.AspNetCore.Identity;using Microsoft.EntityFrameworkCore;namespace CampusServicePlatform.Service{ public class AccountService : IAccountervice { private readonly ApplicationDbContext _context; private readonly UserManager<ApplicationUser> _userManager; private readonly RoleManager<ApplicationRole> _roleManager; private readonly JwtHelper _jwtHelper; private readonly StringHelper _stringHelper; public AccountService(ApplicationDbContext context, UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager, JwtHelper jwtHelper, StringHelper stringHelper) { _context = context; _userManager = userManager; _roleManager = roleManager; _jwtHelper = jwtHelper; _stringHelper = stringHelper; } public async Task<bool> SingUp(UserRequest userRequest) { var user = new ApplicationUser { UserName = userRequest.UserName }; var result = await _userManager.CreateAsync(user, userRequest.Password); if (!result.Succeeded) { throw new ApplicationException(_stringHelper.ListItemToString(result.Errors, "Description")); } if (userRequest.Roles.Count == 0) { userRequest.Roles.Add(RoleEnum.Normal.ToString()); } result = await _userManager.AddToRolesAsync(user, userRequest.Roles); if (!result.Succeeded) { throw new ApplicationException(_stringHelper.ListItemToString(result.Errors, "Description")); } var userInfo = new UserInfo { UserId = user.Id, Avatar = "favicon.ico", Nickname = user.UserName, CreatedTime = DateTime.Now, }; _context.UserInfos.Add(userInfo); await _context.SaveChangesAsync(); return true; } public async Task<string?> SignIn(UserRequest userRequest) { var user = await _userManager.FindByNameAsync(userRequest.UserName); if (user == null) { throw new KeyNotFoundException("用户名或密码错误"); } var result = await _userManager.CheckPasswordAsync(user, userRequest.Password); if (!result) { throw new KeyNotFoundException("用户名或密码错误"); } var userInfo = await _context.UserInfos.SingleOrDefaultAsync(e => e.UserId == user.Id); var roles = await _userManager.GetRolesAsync(user); var jwtToken = _jwtHelper.GenerateJwtToken(user, userInfo, roles); return jwtToken; } public Task<bool> CheckJwtToken(string? jwtToken) { return _jwtHelper.CheckJwtToken(jwtToken); } }}
在 Infrastructure 层新建 StringHelper
辅助类
namespace CampusServicePlatform.Infrastructure{ public class StringHelper { public string ListItemToString<T>(IEnumerable<T> collection, string? propertyName = null) { if (collection == null) { throw new ArgumentNullException(nameof(collection)); } if (string.IsNullOrEmpty(propertyName)) { if (typeof(T) == typeof(string)) { return string.Join(";", collection.Cast<string>()); } else { throw new ArgumentException("属性名不能为空", nameof(propertyName)); } } var property = typeof(T).GetProperty(propertyName); if (property == null) { throw new ArgumentException($"对象不存在名为 {propertyName} 的属性"); } return string.Join(";", collection.Select(item => property.GetValue(item))); } }}
在 WebAPI.Program.cs 中注入 Service 和辅助类
var builder = WebApplication.CreateBuilder(args);// 3...builder.Services.AddScoped<StringHelper>();builder.Services.AddScoped<IAccountervice, AccountService>();// ...var app = builder.Build();
Web API
在 WebAPI.Controllers 目录下新建 AccountController
控制器
using CampusServicePlatform.Model.Attributes;using CampusServicePlatform.Model.DTO;using CampusServicePlatform.Service;using Microsoft.AspNetCore.Authorization;using Microsoft.AspNetCore.Mvc;namespace CampusServicePlatform.WebAPI.Controllers{ [Route("api/[controller]")] [ApiController] public class AccountController : ControllerBase { private readonly IAccountervice _accountService; public AccountController(IAccountervice accountService) { _accountService = accountService; } [HttpPost("sign-up")] [Transactional] public async Task<IActionResult> SignUp(UserRequest userRequest) { await _accountService.SingUp(userRequest); return Ok(new ApiResponse { Message = "注册成功" }); } [HttpPost("sign-in")] public async Task<IActionResult> SignIn(UserRequest userRequest) { var jwtToken = await _accountService.SignIn(userRequest); return Ok(new ApiResponse { Message = "登录成功", Data = new { JwtToken = jwtToken } }); } [HttpGet("check-jwt-token")] [Authorize] public async Task<IActionResult> CheckJwtToken() { var jwtToken = HttpContext.Request.Headers["Authorization"].ToString().Substring("Bearer ".Length).Trim(); var result = await _accountService.CheckJwtToken(jwtToken); if (!result) { throw new ApplicationException("无效的Token"); } return Ok(new ApiResponse { Message = "Token验证成功" }); } }}
在 Model.DTO 目录下新建 ApiResponse
、UserRequest
数据传输类
namespace CampusServicePlatform.Model.DTO{ public class ApiResponse { public int Code { get; set; } = 200; public object? Data { get; set; } public string? Message { get; set; } }}
namespace CampusServicePlatform.Model.DTO{ public class UserRequest { public string? UserName { get; set; } public string? Password { get; set; } public IList<string>? Roles { get; set; } = new List<string>(); }}
在 WebAPI 层新建最终一致性事务操作 Filter
、异常处理 Filter
,详情请移步另一篇文章查看:https://blog.csdn.net/Felix61Felix/article/details/134773734。
Vue3+Vite 前端
视图创建
关于动态路由的生成以及视图的创建规则,请移步另一篇文章查看:https://blog.csdn.net/Felix61Felix/article/details/134753420。
登录表单
<template> <el-card v-loading="userFriendlyTips.isLoading"> <h2>登录</h2> <el-form @keyup.enter.native.prevent="handleSignIn" label-position="top"> <el-form-item label="账号"> <el-input v-model="formData.userName" /> </el-form-item> <el-form-item label="密码"> <el-input v-model="formData.password" type="password" autocomplete="new-password" /> </el-form-item> <el-form-item> <el-link @click="routePush(getPath('忘记密码'))" type="primary" :underline="false">忘记密码?</el-link> </el-form-item> <el-form-item> <el-button @click="handleSignIn" type="primary" plain class="w-100">登录</el-button> </el-form-item> <el-form-item> <el-button @click="routePush(getPath('注册'))" plain class="w-100">注册</el-button> </el-form-item> </el-form> </el-card></template><script setup lang="ts">import { routeBack, routePush, getPath } from '@/router'import { ref, onBeforeMount } from 'vue'import { signIn } from '@/utils/accountHelper'import { useAccountStore } from '@/stores/useAccountStore'import { getQuery, routeReplace } from '@/router'import { ElNotification } from 'element-plus'import { userFriendlyTips as _userFriendlyTips } from '@/utils/renderHelper'const userFriendlyTips = ref({ ..._userFriendlyTips })const formData = ref({ userName: '', password: ''})const from = ref('')onBeforeMount(() => { if (useAccountStore().getJwtToken()) { routeBack() } const query = getQuery() from.value = query.from})const handleSignIn = async () => { try { userFriendlyTips.value.isLoading = true await signIn(formData.value) ElNotification({ title: '登录成功', message: '七天内将自动登录本站!', type: 'success' }) if (from.value) { routeReplace(from.value) } else { routeReplace(getPath('主页')) } } catch (error) { } finally { userFriendlyTips.value.isLoading = false }}</script><style scoped lang="scss">.el-form-item__content { justify-content: space-between !important;}</style>
注册表单
<template> <el-card v-loading="userFriendlyTips.isLoading"> <h2>注册</h2> <el-form @keyup.enter.native.prevent="handleSignUp" label-position="top"> <el-form-item label="账号" required="true"> <el-input v-model="formData.userName" /> </el-form-item> <el-form-item label="密码" required="true"> <el-input v-model="formData.password" type="password" autocomplete="new-password" /> </el-form-item> <el-form-item label="确认密码" required="true"> <el-input v-model="formData.confirmPassword" type="password" /> </el-form-item> <el-form-item> <el-link @click="routePush(getPath('登录'))" type="primary" :underline="false">已有账号?</el-link> </el-form-item> <el-form-item> <el-button @click="handleSignUp" type="primary" plain class="w-100">注册</el-button> </el-form-item> </el-form> </el-card></template><script setup lang="ts">import { routePush, getPath, routeReplace } from '@/router'import { ref } from 'vue'import { userFriendlyTips as _userFriendlyTips } from '@/utils/renderHelper'import { ElNotification } from 'element-plus'import { signUp, signIn } from '@/utils/accountHelper'const userFriendlyTips = ref({ ..._userFriendlyTips })const formData = ref({ userName: '', password: '', confirmPassword: ''})const handleSignUp = async () => { try { userFriendlyTips.value.isLoading = true if (!(await signUp(formData.value))) { return } await signIn(formData.value) ElNotification({ title: '注册成功', message: '七天内将自动登录本站!', type: 'success' }) routeReplace(getPath('主页')) } catch (error) { } finally { userFriendlyTips.value.isLoading = false }}</script>
个人主页
请自行设置跳转测试(按钮、URL编写等)。
<template> <h1>个人主页</h1> <h3>昵称:{{ userInfo?.nickname }}</h3> <h3>头像:{{ userInfo?.avatar }}</h3></template><script setup lang="ts">import { useAccountStore } from '@/stores/useAccountStore'import { computed } from '@vue/reactivity'const userInfo = computed(() => { return useAccountStore().userInfo})</script>
utils/accountHelper
import { useAccountStore } from '@/stores/useAccountStore'import httpRequester from '@/http-requester'export const signIn = async (formData: any) => { const response = await httpRequester.post('/api/account/sign-in', formData) const data = response.data const jwtToken = data.jwtToken useAccountStore().setJwtToken(jwtToken)}export const signOut = () => { useAccountStore().setJwtToken('')}export const checkJwtToken = async () => { const response: any = await httpRequester.get('/api/account/check-jwt-token') if (response.code !== 200) { signOut() } return response.code}export const signUp = async (formData: any) => { const response: any = await httpRequester.post('/api/account/sign-up', formData) if (response.code !== 200) { return false } return true}
utils/codeHelper
export const deepEncodeURI = (obj: any) => { for (let prop in obj) { if (typeof obj[prop] === 'object') { deepEncodeURI(obj[prop]) } else { obj[prop] = encodeURIComponent(obj[prop]) } } return obj}export const deepDecodeURI = (obj: any) => { for (let prop in obj) { if (typeof obj[prop] === 'object') { deepDecodeURI(obj[prop]) } else { obj[prop] = decodeURIComponent(obj[prop]) } } return obj}
utils/objectHelper
export const isEmpty = (e: any): boolean => { if (e === null || e === undefined) { return false } if (typeof e === 'boolean') { return e } if (typeof e === 'string' && e.trim() === '') { return true } if ((typeof e === 'number' && isNaN(e)) || e === 0) { return true } if (Array.isArray(e) && JSON.stringify(e) === '[]' && e.length === 0) { return true } if ((typeof e === 'object' && JSON.stringify(e) === '{}') || Object.keys(e).length === 0) { return true } return false}
utils/renderHelper
// 用户友好提示export const userFriendlyTips = { isLoading: false, isEmpty: true}// 分页参数export const pagination = { pageSize: 20, pageCount: 1, currentPage: 1}
http-requester/index
import _axios from 'axios'import { ElMessage } from 'element-plus'const env = import.meta.env.MODEconst baseURL = import.meta.env.VITE_API_BASE_URLconst axios = _axios.create({ baseURL: baseURL, timeout: 10000, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' }})axios.interceptors.request.use( (config) => { const jwtToken = localStorage.getItem('jwtToken') if (jwtToken) { config.headers.Authorization = `Bearer ${jwtToken}` } return config }, (error) => { return Promise.reject(error) })axios.interceptors.response.use( (_response) => { const response = _response?.data || null if (env === 'development') { const code = response?.code || _response?.status || 200 const message = response?.message || _response?.statusText || '请求成功' ElMessage.success({ message: `【${code}】${message}`, grouping: true }) } else if (env === 'production') { const message = response?.message if (message) { ElMessage.success({ message: `${message}`, grouping: true }) } } return response }, (error) => { const _response = error.response || null const response = _response?.data || null if (env === 'development') { const code = response?.code || _response?.status || 400 const message = response?.message || _response?.statusText || '请求失败' ElMessage.error({ message: `【${code}】${message}`, grouping: true }) } else if (env === 'production') { const message = response?.message || '服务器异常,请稍后重试' ElMessage.error({ message: `${message}`, grouping: true }) } return response })const httpRequester = { get: (url: string, params?: any) => { return axios.get(url, { params }) }, post: (url: string, data?: any) => { return axios.post(url, data || {}) }, put: (url: string, data?: any) => { return axios.put(url, data || {}) }, delete: (url: string, data?: any) => { return axios.delete(url, data || {}) }}export default httpRequester
router/index
关于动态路由的生成以及视图的创建规则,请移步另一篇文章查看:https://blog.csdn.net/Felix61Felix/article/details/134753420。
import { createRouter, createWebHashHistory, useRoute } from 'vue-router'import { elMenuActiveStore } from '@/stores/elMenuActiveStore'import type { RouteRecordRaw } from 'vue-router'import { ElMessage } from 'element-plus'import { checkJwtToken } from '@/utils/accountHelper'import { isEmpty } from '@/utils/objectHelper'import { deepDecodeURI, deepEncodeURI } from '@/utils/codeHelper'const pageModules = import.meta.glob('../views/**/page.ts', { eager: true, import: 'default'})const componentModules = import.meta.glob('../views/**/index.vue', { eager: true, import: 'default'})const routes = Object.entries(pageModules).map(([pagePath, config]) => { const path = pagePath.replace('../views', '').replace('/page.ts', '') || '/' const name = path.split('/').filter(Boolean).join('-') || 'index' const compoentPath = pagePath.replace('page.ts', 'index.vue') return { path, name, component: componentModules[compoentPath], meta: config }})const routeMap: Map<string, RouteRecordRaw> = new Map().set('404', { path: '/:catchAll(.*)', component: () => import('@/components/ly-components/LyNotFound.vue'), meta: { title: '404' }})routes.forEach((route: any) => { const title = route.meta.title routeMap.set(title, route)})const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), routes: Array.from(routeMap.values())})const baseTitle = 'XX网'// 路由守卫 => 路由切换之前router.beforeEach(async (to, from, next) => { // 需要查询参数的页面 if (to.meta.hasQuery && isEmpty(to.query)) { return next({ path: from.fullPath }) } // 需要验证的页面 if (to.meta.hasAuthorize) { const jwtToken = localStorage.getItem('jwtToken') if (!jwtToken) { ElMessage.warning({ message: '请先登录', grouping: true }) return next({ path: getPath('登录'), query: deepEncodeURI({ from: to.fullPath }) }) } else { const code = await checkJwtToken() if (code !== 200) { ElMessage.warning({ message: '身份验证失败,请重新登录', grouping: true }) return next({ path: getPath('登录'), query: deepEncodeURI({ from: to.fullPath }) }) } } } // 设置页面标题 if (to.meta.title) { document.title = `${to.meta.title} - ${baseTitle}` } else { document.title = baseTitle } // el-menu 高亮 elMenuActiveStore().elMenuActive = to.path // 切换路由 return next()})export default routerexport const routePush = (path: string, query?: any) => { router.push({ path, query: deepEncodeURI(query) })}export const routeReplace = (path: any) => { router.replace(path ?? '/')}export const routeBack = () => { router.go(-1)}export const routeForward = () => { router.go(1)}export const openUrl = (url: string, target: string = '_blank') => { window.open(url, target)}export const getPath = (title: string): string => { return routeMap.get(title)?.path || '/'}export const getRoute = () => { return useRoute()}export const getQuery = () => { return deepDecodeURI(getRoute().query)}
stores/useAccountStore
import { computed, ref } from 'vue'import { defineStore } from 'pinia'import type JwtTokenPayload from '@/types/jwtTokenPayload'import type UserInfo from '@/types/userInfo'export const useAccountStore = defineStore('account', () => { const _jwtToken = ref() const getJwtToken = () => { return Object.freeze(_jwtToken.value) } const setJwtToken = (jwtToken: string) => { _jwtToken.value = jwtToken if (jwtToken) { localStorage.setItem('jwtToken', jwtToken) } else { localStorage.removeItem('jwtToken') } } const userInfo = computed((): UserInfo | null => { if (!_jwtToken.value) { return null } const jwtTokenPayload: JwtTokenPayload = JSON.parse(atob(_jwtToken.value.split('.')[1])) return Object.freeze({ nickname: jwtTokenPayload.nickname, avatar: jwtTokenPayload.avatar }) }) return { setJwtToken, getJwtToken, userInfo }})
App.vue
import { onMounted } from 'vue'import { useAccountStore } from '@/stores/useAccountStore'onMounted(async () => { getAccount()})const getAccount = async () => { const jwtToken = localStorage.getItem('jwtToken') || '' if (jwtToken) { useAccountStore().setJwtToken(jwtToken) ElNotification({ title: '自动登录', message: `${useAccountStore().userInfo?.nickname} 你好,欢迎回来!?`, type: 'success' }) } else { ElNotification({ title: '提示', message: '登录以解锁更多功能!', type: 'info' }) }}
整体流程图
参考资料
[1] 杨中科. ASP.NET Core技术内幕与项目实战:基于DDD与前后端分离[M]. 北京: 人民邮电出版社, 2022.
[2] 杨中科. .NET 6教程,.Net Core 2022视频教程,杨中科主讲[Z/OL]. https://www.bilibili.com/video/BV1pK41137He?p=144. 2020.
[3] Foad Alavi. Authenticating Web API Using ASP .Net Identity and JSON Web Tokens (JWT)[Z/OL]. https://www.youtube.com/watch?v=99-r3Y48SYE/. 2023.