1. 项目概述与核心技术栈选型
在前后端分离架构成为主流的今天,使用C#作为后端服务,配合Vue.js前端框架和ElementUI组件库构建登录界面,是一种非常典型的现代化开发组合。这个方案结合了.NET生态的稳健性和Vue.js的灵活高效,特别适合需要快速开发又要求长期维护的企业级应用。
为什么选择这个技术组合?
- C#/.NET Core:提供强大的类型安全、成熟的Web API开发框架和出色的性能表现,特别适合构建需要高安全性的认证服务
- Vue 3.x:渐进式框架的特性使其既能快速上手,又能应对复杂交互场景,组合式API让登录逻辑的组织更清晰
- Element Plus:基于Vue 3的组件库,提供现成的表单、弹窗、通知等UI元素,大幅减少基础样式开发时间
- JavaScript/TypeScript:作为粘合剂处理界面交互逻辑,与后端API通信
实际开发中我推荐使用TypeScript替代原生JS,它能提供更好的类型提示和代码维护性,与C#的类型系统形成前后端呼应。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与项目初始化
2.1 后端环境配置
首先创建ASP.NET Core Web API项目:
bash复制dotnet new webapi -n AuthDemo.Api
cd AuthDemo.Api
安装JWT认证相关NuGet包:
bash复制dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet package Microsoft.IdentityModel.Tokens
配置Program.cs添加JWT支持:
csharp复制builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
ValidateIssuerSigningKey = true
};
});
2.2 前端项目搭建
使用Vite创建Vue 3项目:
bash复制npm create vite@latest auth-demo-ui --template vue-ts
cd auth-demo-ui
安装Element Plus和axios:
bash复制npm install element-plus axios
npm install -D @element-plus/icons-vue
配置main.ts:
typescript复制import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
3. 登录界面核心实现
3.1 ElementUI表单设计与验证
创建src/views/Login.vue:
vue复制<template>
<div class="login-container">
<el-form
:model="loginForm"
:rules="rules"
ref="loginFormRef"
label-position="top"
@keyup.enter="handleLogin"
>
<h2 class="title">系统登录</h2>
<el-form-item prop="username" label="用户名">
<el-input
v-model="loginForm.username"
placeholder="请输入用户名"
prefix-icon="User"
/>
</el-form-item>
<el-form-item prop="password" label="密码">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
show-password
prefix-icon="Lock"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="handleLogin"
:loading="loading"
>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import axios from 'axios'
const loginForm = ref({
username: '',
password: ''
})
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在3到20个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 30, message: '长度在6到30个字符', trigger: 'blur' }
]
}
const loading = ref(false)
const router = useRouter()
const handleLogin = async () => {
loading.value = true
try {
const response = await axios.post('/api/auth/login', loginForm.value)
localStorage.setItem('token', response.data.token)
router.push('/dashboard')
} catch (err) {
ElMessage.error(err.response?.data?.message || '登录失败')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.login-container {
width: 400px;
margin: 100px auto;
padding: 30px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
border-radius: 5px;
}
.title {
text-align: center;
margin-bottom: 30px;
color: #409EFF;
}
</style>
3.2 后端认证接口实现
创建AuthController:
csharp复制[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IConfiguration _config;
public AuthController(IConfiguration config)
{
_config = config;
}
[HttpPost("login")]
public IActionResult Login([FromBody] LoginModel model)
{
// 实际项目中这里应该查询数据库验证用户
if (model.Username != "admin" || model.Password != "123456")
return Unauthorized(new { message = "用户名或密码错误" });
var token = GenerateJwtToken(model.Username);
return Ok(new { token });
}
private string GenerateJwtToken(string username)
{
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: new[] { new Claim(ClaimTypes.Name, username) },
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
public class LoginModel
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
4. 前后端联调与安全加固
4.1 跨域与代理配置
在Vite中配置代理(vite.config.ts):
typescript复制import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
在.NET中配置CORS(Program.cs):
csharp复制builder.Services.AddCors(options => {
options.AddPolicy("VueClient", policy => {
policy.WithOrigins("http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
app.UseCors("VueClient");
4.2 安全最佳实践
-
密码传输安全:
- 前端使用HTTPS传输
- 考虑对密码进行客户端哈希(如bcryptjs)后再传输
-
Token存储安全:
typescript复制// 使用httpOnly的cookie替代localStorage document.cookie = `[token](https://taotoken.net?utm_source=general)=${token}; Path=/; Secure; SameSite=Strict` -
防暴力破解:
csharp复制[HttpPost("login")] public async Task<IActionResult> Login([FromBody] LoginModel model) { var ip = Request.HttpContext.Connection.RemoteIpAddress?.ToString(); var cache = Request.HttpContext.RequestServices.GetRequiredService<IMemoryCache>(); if (cache.Get<int>($"login_attempts_{ip}") >= 5) return Unauthorized(new { message = "尝试次数过多,请稍后再试" }); // ...验证逻辑 if (!valid) { var attempts = cache.GetOrCreate($"login_attempts_{ip}", entry => { entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5); return 0; }); cache.Set($"login_attempts_{ip}", attempts + 1); } }
5. 扩展功能实现
5.1 验证码功能
后端添加验证码接口:
csharp复制[HttpGet("captcha")]
public IActionResult GetCaptcha()
{
var code = GenerateRandomCode(4);
var image = GenerateCaptchaImage(code);
HttpContext.Session.SetString("CaptchaCode", code);
return File(image, "image/png");
}
private static string GenerateRandomCode(int length)
{
const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[Random.Shared.Next(s.Length)]).ToArray());
}
前端集成验证码:
vue复制<template>
<el-form-item prop="captcha" label="验证码">
<div class="captcha-container">
<el-input v-model="loginForm.captcha" placeholder="请输入验证码" />
<img :src="captchaUrl" @click="refreshCaptcha" class="captcha-image" />
</div>
</el-form-item>
</template>
<script setup>
const captchaUrl = ref('/api/auth/captcha?t=' + Date.now())
const refreshCaptcha = () => {
captchaUrl.value = `/api/auth/captcha?t=${Date.now()}`
}
</script>
<style>
.captcha-container {
display: flex;
gap: 10px;
}
.captcha-image {
height: 40px;
cursor: pointer;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
</style>
5.2 路由守卫与权限验证
创建路由守卫(src/router/authGuard.ts):
typescript复制import { createRouter, createWebHistory } from 'vue-router'
import Login from '../views/Login.vue'
import Dashboard from '../views/Dashboard.vue'
const routes = [
{ path: '/login', component: Login },
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !localStorage.getItem('token')) {
next('/login')
} else {
next()
}
})
export default router
6. 常见问题与调试技巧
-
ElementUI样式未加载:
- 确保正确导入CSS文件:
import 'element-plus/dist/index.css' - 检查vite.config.js中是否正确配置了CSS预处理器
- 确保正确导入CSS文件:
-
跨域请求失败:
- 后端必须配置正确的CORS策略
- 前端axios请求需要设置
withCredentials: true(当使用cookie时)
-
表单验证不生效:
- 确保
el-form上绑定了:rules属性 - 每个
el-form-item必须设置prop属性对应rules中的字段名
- 确保
-
JWT令牌无效:
csharp复制// 检查.NET中的JWT配置是否一致 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])), ValidateIssuer = true, ValidIssuer = Configuration["Jwt:Issuer"], ValidateAudience = true, ValidAudience = Configuration["Jwt:Audience"], ValidateLifetime = true, ClockSkew = TimeSpan.Zero // 严格校验过期时间 }; }); -
生产环境部署建议:
- 前端使用Nginx部署,配置gzip压缩和缓存策略
- 后端使用Kestrel或IIS,配置HTTPS重定向
- 敏感配置(如JWT密钥)使用环境变量或密钥管理服务
