【.NET Web API+Vue3】前后端分离登录案例
老咸鱼催吐 2024-07-08 12:33:03 阅读 60
本案例偏向业务实战,详细原理请根据参考资料、网上检索等自行学习。
.NET 8 后端
项目目录结构
项目的引用依赖链为:WebAPI => Service => Infrastructure => Model。
本案例中,Test 层不会用到。
EF Core 与 Identity
在 Model 层中安装 <code>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 迁移命令:<code>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">code>
<h2>登录</h2>
<el-form @keyup.enter.native.prevent="handleSignIn" label-position="top">code>
<el-form-item label="账号">code>
<el-input v-model="formData.userName" />code>
</el-form-item>
<el-form-item label="密码">code>
<el-input v-model="formData.password" type="password" autocomplete="new-password" />code>
</el-form-item>
<el-form-item>
<el-link @click="routePush(getPath('忘记密码'))" type="primary" :underline="false">忘记密码?</el-link>code>
</el-form-item>
<el-form-item>
<el-button @click="handleSignIn" type="primary" plain class="w-100">登录</el-button>code>
</el-form-item>
<el-form-item>
<el-button @click="routePush(getPath('注册'))" plain class="w-100">注册</el-button>code>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup lang="ts">code>
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">code>
.el-form-item__content {
justify-content: space-between !important;
}
</style>
注册表单
<template>
<el-card v-loading="userFriendlyTips.isLoading">code>
<h2>注册</h2>
<el-form @keyup.enter.native.prevent="handleSignUp" label-position="top">code>
<el-form-item label="账号" required="true">code>
<el-input v-model="formData.userName" />code>
</el-form-item>
<el-form-item label="密码" required="true">code>
<el-input v-model="formData.password" type="password" autocomplete="new-password" />code>
</el-form-item>
<el-form-item label="确认密码" required="true">code>
<el-input v-model="formData.confirmPassword" type="password" />code>
</el-form-item>
<el-form-item>
<el-link @click="routePush(getPath('登录'))" type="primary" :underline="false">已有账号?</el-link>code>
</el-form-item>
<el-form-item>
<el-button @click="handleSignUp" type="primary" plain class="w-100">注册</el-button>code>
</el-form-item>
</el-form>
</el-card>
</template>
<script setup lang="ts">code>
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">code>
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.MODE
const baseURL = import.meta.env.VITE_API_BASE_URL
const 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 router
export 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.
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。