【.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 目录下新建 ApplicationRoleApplicationUserUserInfo 实体类

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.DesignMicrosoft.EntityFrameworkCore.SqlServerMicrosoft.EntityFrameworkCore.Tools NuGet 包

在 Infrastructure.DbEntityConfigs 目录下新建 ApplicationRoleEntityConfigApplicationUserEntityConfigUserInfoEntityConfig 实体配置类

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

将程序包管理控制台的默认项目切换成 Infrastructure

解决方案的启动项目切换成 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 目录下新建 ApiResponseUserRequest 数据传输类

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.



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。