【.NET Web API+Vue3】前后端分离登录案例

本案例偏向业务实战,详细原理请根据参考资料、网上检索等自行学习。

.*** 8 后端

项目目录结构

项目的引用依赖链为:WebAPI => Service => Infrastructure => Model

本案例中,Test 层不会用到。

EF Core 与 Identity

  1. 在 Model 层中安装 Microsoft.Asp***Core.Identity.EntityFrameworkCore Nuget 包

  2. 在 Model.DbEntities 目录下新建 ApplicationRoleApplicationUserUserInfo 实体类

    using Microsoft.Asp***Core.Identity;
    
    namespace CampusServicePlatform.Model.DbEntities
    {
        public class ApplicationRole : IdentityRole<Guid>
        {
        }
    }
    
    using Microsoft.Asp***Core.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; }
        }
    }
    
  3. 在 Infrastructure 层中安装 Microsoft.EntityFrameworkCore.DesignMicrosoft.EntityFrameworkCore.SqlServerMicrosoft.EntityFrameworkCore.Tools NuGet 包

  4. 在 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);
            }
        }
    }
    
    
  5. 在 Infrastructure.DbEntityConfigs 目录下新建 ApplicationDbContext EF Core 数据库上下文

    using CampusServicePlatform.Model.DbEntities;
    using Microsoft.Asp***Core.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);
            }
        }
    }
    
    
  6. 在 WebAPI.Program.cs 中配置 DbContext 与 Identity

    此处的数据库连接字符串获取不再赘述,详情请移步另一篇文章查看:https://blog.csdn.***/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();
    
  7. 进入程序包管理控制台,将解决方案的启动项目、程序包管理控制台的默认项目都切换成 Infrastructure

  8. 在程序包管理控制台中依次输入 EF Core 迁移命令:Add-Migration InitialCreate -OutputDir DatabaseEntityConfig/_MigrationsUpdate-database,以生成数据库

JWT

  1. 在 Infrastructure 层中新建 JwtHelper

    using CampusServicePlatform.Model.DbEntities;
    using Microsoft.Asp***Core.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;
            }
        }
    }
    
  2. 在 WebAPI 层中编写 JWT 的配置文件

    {
      "Jwt": {
        "Key": "自行设置密钥,推荐写 GUID 值",
        "Issuer": "签发者,推荐写 API 地址",
        "Audience": "接收者,推荐写前端地址"
      }
    }
    
  3. 在 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

  1. 在 Service 层新建 IA***ountService 接口

    using CampusServicePlatform.Model.DTO;
    
    namespace CampusServicePlatform.Service
    {
        public interface IA***ountervice
        {
            Task<bool> CheckJwtToken(string? jwtToken);
            Task<string?> SignIn(UserRequest userRequest);
            Task<bool> SingUp(UserRequest userRequest);
        }
    }
    
  2. 在 Service 层新建 A***ountService 实现类

    using CampusServicePlatform.Infrastructure;
    using CampusServicePlatform.Infrastructure.DbEntityConfigs;
    using CampusServicePlatform.Model.DbEntities;
    using CampusServicePlatform.Model.DTO;
    using CampusServicePlatform.Model.Enum;
    using Microsoft.Asp***Core.Identity;
    using Microsoft.EntityFrameworkCore;
    
    namespace CampusServicePlatform.Service
    {
        public class A***ountService : IA***ountervice
        {
            private readonly ApplicationDbContext _context;
            private readonly UserManager<ApplicationUser> _userManager;
            private readonly RoleManager<ApplicationRole> _roleManager;
            private readonly JwtHelper _jwtHelper;
            private readonly StringHelper _stringHelper;
    
            public A***ountService(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.Su***eeded)
                {
                    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.Su***eeded)
                {
                    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);
            }
        }
    }
    
  3. 在 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)));
            }
        }
    }
    
  4. 在 WebAPI.Program.cs 中注入 Service 和辅助类

    var builder = WebApplication.CreateBuilder(args);
    
    // 3...
    
    builder.Services.AddScoped<StringHelper>();
    builder.Services.AddScoped<IA***ountervice, A***ountService>();
    
    // ...
    
    var app = builder.Build();
    

Web API

  1. 在 WebAPI.Controllers 目录下新建 A***ountController 控制器

    using CampusServicePlatform.Model.Attributes;
    using CampusServicePlatform.Model.DTO;
    using CampusServicePlatform.Service;
    using Microsoft.Asp***Core.Authorization;
    using Microsoft.Asp***Core.Mvc;
    
    namespace CampusServicePlatform.WebAPI.Controllers
    {
        [Route("api/[controller]")]
        [ApiController]
        public class A***ountController : ControllerBase
        {
            private readonly IA***ountervice _a***ountService;
    
            public A***ountController(IA***ountervice a***ountService)
            {
                _a***ountService = a***ountService;
            }
    
            [HttpPost("sign-up")]
            [Transactional]
            public async Task<IActionResult> SignUp(UserRequest userRequest)
            {
                await _a***ountService.SingUp(userRequest);
    
                return Ok(new ApiResponse
                {
                    Message = "注册成功"
                });
            }
    
            [HttpPost("sign-in")]
            public async Task<IActionResult> SignIn(UserRequest userRequest)
            {
                var jwtToken = await _a***ountService.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 _a***ountService.CheckJwtToken(jwtToken);
                if (!result)
                {
                    throw new ApplicationException("无效的Token");
                }
    
                return Ok(new ApiResponse
                {
                    Message = "Token验证成功"
                });
            }
        }
    }
    
  2. 在 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>();
        }
    }
    
  3. 在 WebAPI 层新建最终一致性事务操作 Filter异常处理 Filter,详情请移步另一篇文章查看:https://blog.csdn.***/Felix61Felix/article/details/134773734。

vue3+Vite 前端

视图创建

关于动态路由的生成以及视图的创建规则,请移步另一篇文章查看:https://blog.csdn.***/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" auto***plete="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/a***ountHelper'
import { useA***ountStore } from '@/stores/useA***ountStore'
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 (useA***ountStore().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: 'su***ess'
        })

        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" auto***plete="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/a***ountHelper'

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: 'su***ess'
        })

        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 { useA***ountStore } from '@/stores/useA***ountStore'
import { ***puted } from '@vue/reactivity'

const userInfo = ***puted(() => {
    return useA***ountStore().userInfo
})
</script>

utils/a***ountHelper

import { useA***ountStore } from '@/stores/useA***ountStore'
import httpRequester from '@/http-requester'

export const signIn = async (formData: any) => {
  const response = await httpRequester.post('/api/a***ount/sign-in', formData)
  const data = response.data

  const jwtToken = data.jwtToken
  useA***ountStore().setJwtToken(jwtToken)
}

export const signOut = () => {
  useA***ountStore().setJwtToken('')
}

export const checkJwtToken = async () => {
  const response: any = await httpRequester.get('/api/a***ount/check-jwt-token')
  if (response.code !== 200) {
    signOut()
  }

  return response.code
}

export const signUp = async (formData: any) => {
  const response: any = await httpRequester.post('/api/a***ount/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] = encodeURI***ponent(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] = decodeURI***ponent(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.su***ess({
        message: `${code}${message}`,
        grouping: true
      })
    } else if (env === 'production') {
      const message = response?.message

      if (message) {
        ElMessage.su***ess({
          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.***/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/a***ountHelper'
import { isEmpty } from '@/utils/objectHelper'
import { deepDecodeURI, deepEncodeURI } from '@/utils/codeHelper'

const pageModules = import.meta.glob('../views/**/page.ts', {
  eager: true,
  import: 'default'
})
const ***ponentModules = 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 ***poentPath = pagePath.replace('page.ts', 'index.vue')
  return {
    path,
    name,
    ***ponent: ***ponentModules[***poentPath],
    meta: config
  }
})

const routeMap: Map<string, RouteRecordRaw> = new Map().set('404', {
  path: '/:catchAll(.*)',
  ***ponent: () => import('@/***ponents/ly-***ponents/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/useA***ountStore

import { ***puted, ref } from 'vue'
import { defineStore } from 'pinia'
import type JwtTokenPayload from '@/types/jwtTokenPayload'
import type UserInfo from '@/types/userInfo'

export const useA***ountStore = defineStore('a***ount', () => {
  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 = ***puted((): 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 { useA***ountStore } from '@/stores/useA***ountStore'

onMounted(async () => {
  getA***ount()
})

const getA***ount = async () => {
  const jwtToken = localStorage.getItem('jwtToken') || ''
  if (jwtToken) {
    useA***ountStore().setJwtToken(jwtToken)
    ElNotification({
      title: '自动登录',
      message: `${useA***ountStore().userInfo?.nickname} 你好,欢迎回来!🎉`,
      type: 'su***ess'
    })
  } else {
    ElNotification({
      title: '提示',
      message: '登录以解锁更多功能!',
      type: 'info'
    })
  }
}

整体流程图

参考资料

[1] 杨中科. ASP.*** Core技术内幕与项目实战:基于DDD与前后端分离[M]. 北京: 人民邮电出版社, 2022.

[2] 杨中科. .*** 6教程,.*** Core 2022视频教程,杨中科主讲[Z/OL]. https://www.bilibili.***/video/BV1pK41137He?p=144. 2020.

[3] Foad Alavi. Authenticating Web API Using ASP .*** Identity and JSON Web Tokens (JWT)[Z/OL]. https://www.youtube.***/watch?v=99-r3Y48SYE/. 2023.

转载请说明出处内容投诉
CSS教程_站长资源网 » 【.NET Web API+Vue3】前后端分离登录案例

发表评论

欢迎 访客 发表评论

一个令你着迷的主题!

查看演示 官网购买