.NET Core與Vue3登入者和角色授權管理功能模組(用js版本,cors+Identity+Jwt)_中文內容亂碼異常排除_增加導覽列_註冊與登入
刪除預設的vue介面程式檔案
調整App.vue的部分
1 2 3 4 5 6 7 | <script setup> import Home from './components/Home.vue' </script> <template> <Home /> </template> |
預設導入Home元件
會發現中文內容運行起來出現亂碼異常
重新運行起來即可正常
NavBar.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <template> <nav class="navbar navbar-expand-lg bg-body-tertiary"> <div class="container-fluid ms-3 me-3"> <router-link class="navbar-brand" to="/">管理系統</router-link> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNavDropdown"> <ul class="navbar-nav"> <li class="nav-item"> <router-link class="nav-link active" aria-current="page" to="/">首頁</router-link> </li> <li class="nav-item" v-if="isAdmin"> <router-link class="nav-link" to="/manage/user">使用者管理</router-link> </li> <li class="nav-item" v-if="isAdmin"> <router-link class="nav-link" to="/manage/role">角色管理</router-link> </li> </ul> </div> <div class="d-flex"> <ul class="navbar-nav" v-if="!isLogin"> <li class="nav-item"> <router-link class="nav-link" to="/register">註冊</router-link> </li> <li class="nav-item"> <router-link class="nav-link" to="/login">登入</router-link> </li> </ul> </div> </div> </nav> </template> |
調整App.vue 替換改引入它
測試運行起來
可以透過<router-view>實踐路由渲染
於App.vue中 加入<router-view>元素
如此一來即可實踐版面布局中子頁鑲嵌進來統一布局的部分了
因為在src/router.js中有配置了根路徑 (/) 訪問到對應元件為 Home.vue元件
在index.html中可以調整title 改顯示我們指定的
後端web api專案目錄增加 Models目錄
並加入AppUser.cs的類別
1 2 3 4 5 6 7 8 9 10 11 12 13 | using Microsoft.AspNetCore.Identity; namespace Vue.NET.Identity.Auth.Models { public class AppUser : IdentityUser { //姓名 public string? FullName { get; set; } //大頭照 public string? Avatar { get; set; } } } |
加入AppRole.cs的類別
1 2 3 4 5 6 7 8 9 10 | using Microsoft.AspNetCore.Identity; namespace Vue.NET.Identity.Auth.Models { public class AppRole : IdentityRole { //啟用與否 public bool IsEnabled { get; set; } } } |
進行ASP.NET Core Identity EFCore ORM資料操作相關資料庫配置
添加編寫DbContext子類別
於專案根目錄增加
AppDbContext.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Vue.NET.Identity.Auth.Models; namespace Vue.NET.Identity.Auth { //繼承IdentityDbContext (為Identity框架中用的DbContext) //這邊我們整套應用都會只統一使用單一個DbContext也就是此AppDbContext //IdentityDbContext<T1,T2,T3>各自泛型代表 //AppUser:使用者帳號延伸欄位model定義,若無延伸就改用預設的IdentityUser //AppRole:使用者帳號角色延伸欄位model定義,若無延伸就改用預設的IdentityRole。 //string:代表使用者帳號table和Role Table的PK類型,這邊兩者必須用相同的PK DataType,現階段都為string。 public class AppDbContext : IdentityDbContext<AppUser, AppRole, string> { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } } } |
配置資料庫連線字串 WooBetterConnection
1 2 3 4 5 6 7 8 9 10 11 12 | { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "WooBetterConnection": "Server=.\\SQLEXPRESS;Database=WooSysDb;Trusted_Connection=True;TrustServerCertificate=true;MultipleActiveResultSets=true;User ID=admin;Password=rootroot" } } |
開啟Program.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Vue.NET.Identity.Auth.Models; using Vue.NET.Identity.Auth; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddCors(options => { options.AddDefaultPolicy(builder => { builder.WithOrigins("http://localhost:5173").AllowAnyHeader(); }); }); //註冊AppDbContext builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("WooBetterConnection")), contextLifetime: ServiceLifetime.Singleton); //註冊Identity服務,並指定使用者和角色物件。 builder.Services.AddIdentity<AppUser, AppRole>(options => options.SignIn.RequireConfirmedAccount = false).AddEntityFrameworkStores<AppDbContext>().AddDefaultTokenProviders(); //配置Identity密碼規範 builder.Services.Configure<IdentityOptions>(options => { //密碼中需要有一個(0-9)之間的數字。 options.Password.RequireDigit = false; //密碼中需要有小寫字符。 options.Password.RequireLowercase = false; //密碼中需要有非字母數字字符。 options.Password.RequireNonAlphanumeric = false; //密碼中需要有大寫字符。 options.Password.RequireUppercase = false; //密碼的最小長度。 options.Password.RequiredLength = 6; //密碼中需要有非重覆字符數。 options.Password.RequiredUniqueChars = 1; }); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseCors(); app.UseAuthorization(); app.MapControllers(); app.Run(); |
在var app = builder.Build(); 和if (app.Environment.IsDevelopment())兩句之間
來做只有第一次的配置(只運行一次的配置)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //生成資料庫和table結構 var scope = app.Services.CreateScope(); var context = scope.ServiceProvider.GetRequiredService<AppDbContext>(); //如果資料庫不存在,就生成table結構 var r = context.Database.EnsureCreated(); if (r) { //獲取user和role manager。 var userManager = scope.ServiceProvider.GetRequiredService<UserManager<AppUser>>(); var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<AppRole>>(); //添加admin user var adminUser = new AppUser() { Id = Guid.NewGuid().ToString(), UserName = "admin", Email = "chousml@gmail.com", EmailConfirmed = true, }; await userManager.CreateAsync(adminUser, "admin12345678"); //添加admin role group var adminRole = new AppRole() { Name = "Admin", IsEnabled = true }; await roleManager.CreateAsync(adminRole); //將admin user 添加至admin role group await userManager.AddToRolesAsync(adminUser, new string[] { adminRole.Name }); userManager.Dispose(); roleManager.Dispose(); } |
運行web api專案後即可看到DB成功migrate並產生資料
註冊(添加user)
[前端部分]
於vue專案
在/src/components目錄下產生UserRegister.vue元件(註冊頁面的元件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <template> <div class="shadow-sm p-3 m-5 bg-body-tertiary row rounded"> <div class="col"> <img width="600" class="rounded" src="../assets/First.png" /> </div> <div class="col"> <h1 class="mb-5"><span class="text-primary">註冊</span> 後台管理</h1> <div class="mt-3"> <input type="text" v-model="userName" placeholder="請輸入帳號" class="form-control w-75" /> </div> <div class="mt-3"> <input type="password" v-model="userPwd" placeholder="請輸入密碼" class="form-control w-75" /> </div> <div class="mt-3"> <input type="email" v-model="userEmail" placeholder="請輸入Email" class="form-control w-75" /> </div> <div class="form-check mt-3"> <input class="form-check-input" v-model="isAccept" type="checkbox" value="true" id="flexCheckDefault"> <label class="form-check-label small" for="flexCheckDefault"> 閱讀並同意 </label> </div> <div class="d-grid gap-2 mt-3"> <button class="btn btn-primary btn-lg w-75" @click="save" type="button">註冊</button> </div> </div> </div> </template> |
於vue專案
在/src/router.js中添加相應路由配置(如此使用者就可透過/register訪問到對應註冊頁面元件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), //html5 history routes: [ { name: 'Home', path: '/', component: () => import('@/components/Home.vue'), meta: { title: '首頁' } }, { name: 'UserRegister', path: '/register', component: () => import('@/components/UserRegister.vue'), meta: { title: '帳號註冊' } } ] }) export default router; |
[後端部分]
加入一個對應ViewModel class 於Models目錄下,名稱:UserViewModel.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using System.ComponentModel.DataAnnotations; namespace Vue.NET.Identity.Auth.Models { public class UserViewModel { //使用者帳號(名稱) [Required] public string? UserName { get; set; } //密碼 [Required] public string? Password { get; set; } //郵件 public string? Email { get; set; } } } |
增加對應後端web api控制器 ,名稱:AccountController.cs
依賴注入進來UserManager 和SignInManager進來
並撰寫給vue前端http post請求的UserRegister() method
[AllowAnonumouse]代表可匿名訪問不受權限管控。
EmailConfirmed 這邊自動先turn true不然還要處裡發信驗證,如此一來就能直接登入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Vue.NET.Identity.Auth.Models; namespace Vue.NET.Identity.Auth.Controllers { [Route("api/[controller]")] [ApiController] public class AccountController : ControllerBase { private readonly UserManager<AppUser> _userManager; private readonly SignInManager<AppUser> _signInManager; public AccountController(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager) { _userManager = userManager; _signInManager = signInManager; } /// <summary> /// 註冊添加使用者帳號。 /// </summary> /// <returns></returns> [AllowAnonymous] [HttpPost("Register")] public async Task<IdentityResult> UserRegister(UserViewModel user) { if (string.IsNullOrWhiteSpace(user.Password)) return IdentityResult.Failed(); var appUser = new AppUser { UserName = user.UserName, Email = user.Email, EmailConfirmed = true }; //提交 var result = await _userManager.CreateAsync(appUser, user.Password); return result; } } } |
這邊方法回傳型別是IdentityResult 於vue前端接收得到Failed 或Succeeded 來判斷成功與否。
可將web api運行在swag文檔中測試
因此若有遺失遺忘就只能修改密碼重置。
[前端部分]
這邊我們要來調整於vue頁面增加call api的js邏輯
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | <template> <div class="shadow-sm p-3 m-5 bg-body-tertiary row rounded"> <div class="col"> <img width="600" height="700" class="rounded" src="../assets/First.avif" /> </div> <div class="col"> <h1 class="mb-5"><span class="text-primary">註冊</span> 後台管理</h1> <div v-show="isMsg" class="alert alert-warning w-75"> {{msg}} </div> <div class="mt-3"> <input type="text" v-model="userName" placeholder="請輸入帳號" class="form-control w-75" /> </div> <div class="mt-3"> <input type="password" v-model="userPwd" placeholder="請輸入密碼" class="form-control w-75" /> </div> <div class="mt-3"> <input type="email" v-model="userEmail" placeholder="請輸入Email" class="form-control w-75" /> </div> <div class="form-check mt-3"> <input class="form-check-input" v-model="isAccept" type="checkbox" value="true" id="flexCheckDefault"> <label class="form-check-label small" for="flexCheckDefault"> 閱讀並同意 </label> </div> <div class="d-grid gap-2 mt-3"> <button class="btn btn-primary btn-lg w-75" @click="save" type="button">註冊</button> </div> </div> </div> </template> <script setup> import { ref } from 'vue' import axios from "../axios" const userName = ref('');//用戶名 const userPwd = ref('');//密碼 const userEmail = ref('');//郵箱地址 const isAccept = ref();//閱讀並同意 //提示信息 const msg = ref(''); //提示信息是否顯示 const isMsg = ref(false); //保存 const save = () => { if (!isAccept.value) { isMsg.value = true; msg.value = "請閱讀並同意條款。"; return; } //用戶名和密碼不為空。 if (userName.value != '' && userPwd.value != '') { //關閉提示框 isMsg.value = false; //發起HTTP請求 axios({ url: '/api/account/register', method: 'post', data: { userName: userName.value, password: userPwd.value, email: userEmail.value } }).then((res) => { const d = res.data; if (d.succeeded == true) { isMsg.value = true; msg.value = "您已成功註冊,請登入系統"; } else { isMsg.value = true; msg.value = d.errors[0].description; } }); } else { //顯示提示框 isMsg.value = true; //提示信息 msg.value = '帳號和密碼為必填。'; } } </script> |
這樣就有相關的前端UI互動
比方沒勾選閱讀與同意
登入(authentication)頁面功能
[前端部分]
於vue專案
在/src/components目錄下產生UserLogin.vue元件(登入頁面的元件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | <template> <div class="shadow-sm p-3 m-5 bg-body-tertiary row rounded"> <div class="col"> <div class="image-container"> <img class="rounded" src="../assets/First.avif" /> </div> </div> <div class="col"> <h1 class="mb-5"><span class="text-primary">登入</span> 後台管理</h1> <div v-show="isMsg" class="alert alert-warning w-75"> {{msg}} </div> <div class="mt-3"> <input type="text" v-model="userName" placeholder="請輸入帳號" class="form-control w-75" /> </div> <div class="mt-3"> <input type="password" v-model="userPwd" placeholder="請輸入密碼" class="form-control w-75" /> </div> <div class="form-check mt-3"> <input class="form-check-input" type="checkbox" value="true" v-model="isAccept" id="flexCheckDefault"> <label class="form-check-label small" for="flexCheckDefault"> 閱讀並同意 </label> </div> <div class="d-grid gap-2 mt-3"> <button class="btn btn-primary btn-lg w-75" @click="login" type="button">登入</button> </div> </div> </div> </template> <style> .image-container { display: flex; align-items: center; justify-content: center; } .image-container img { max-height: 100%; max-width: 100%; object-fit: contain; } </style> <script setup> import { ref } from 'vue' import axios from "../axios" import { useRouter } from 'vue-router' const router = useRouter(); const userName = ref('');//用戶名 const userPwd = ref('');//密碼 const isAccept = ref();//是否同意條款 const msg = ref('');//提示信息 const isMsg = ref(false);//提示信息是否顯示 const login = () => { if (!isAccept.value) { isMsg.value = true; msg.value = "請閱讀並同意條款。"; return; } //帳號和密碼不為空。 if (userName.value != '' && userPwd.value != '') { //關閉提示框 isMsg.value = false; axios({ url: '/api/account/login', method: 'post', data: { userName: userName.value, password: userPwd.value } }).then((res) => { try { if (res.data) { const d = res.data; isMsg.value = true; msg.value = "登入成功。"; localStorage.setItem('userName', userName.value); localStorage.setItem('token', d.token); localStorage.setItem('role', d.roleList); localStorage.setItem('headImg', d.avatar); router.push('/');//跳轉回首頁Home.vue } } catch (error) { isMsg.value = true; msg.value = error; } }); } else { //顯示提示框 isMsg.value = true; //提示信息 msg.value = '帳號和密碼為必填。'; } } </script> |
於vue專案
在/src/router.js中添加相應路由配置(如此使用者就可透過/login訪問到對應登入頁面元件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), //html5 history routes: [ { name: 'Home', path: '/', component: () => import('@/components/Home.vue'), meta: { title: '首頁' } }, { name: 'UserRegister', path: '/register', component: () => import('@/components/UserRegister.vue'), meta: { title: '帳號註冊' } }, { name: 'UserLogin', path: '/login', component: () => import('@/components/UserLogin.vue'), meta: { title: '登入' } } ] }) export default router; |
[後端部分]
添加UserLogin web api方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Vue.NET.Identity.Auth.Models; namespace Vue.NET.Identity.Auth.Controllers { [Route("api/[controller]")] [ApiController] public class AccountController : ControllerBase { private readonly UserManager<AppUser> _userManager; private readonly SignInManager<AppUser> _signInManager; public AccountController(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager) { _userManager = userManager; _signInManager = signInManager; } /// <summary> /// 註冊添加使用者帳號。 /// </summary> /// <returns></returns> [AllowAnonymous] [HttpPost("Register")] public async Task<IdentityResult> UserRegister(UserViewModel user) { if (string.IsNullOrWhiteSpace(user.Password)) return IdentityResult.Failed(); var appUser = new AppUser { UserName = user.UserName, Email = user.Email, EmailConfirmed = true }; //提交 var result = await _userManager.CreateAsync(appUser, user.Password); return result; } private const string signingKey = "test1968196828825252"; private const string issuer = "Samuel"; private const string audience = "SamuelAPI"; /// <summary> /// 使用者登入 /// </summary> /// <param name="user"></param> /// <returns></returns> [AllowAnonymous] [HttpPost("Login")] public async Task<dynamic> UserLogin(UserViewModel user) { try { if (string.IsNullOrWhiteSpace(user.UserName) || string.IsNullOrWhiteSpace(user.Password)) return NotFound(); //Login 處理 var result = await _signInManager.PasswordSignInAsync(user.UserName, user.Password, false, false); if (!result.Succeeded) return BadRequest("帳號或密碼錯誤。"); //By帳號名取得appUser var appUser = await _userManager.FindByNameAsync(user.UserName); if (appUser == null) return NotFound(); //取得登入帳號掛的對應角色清單 var roleList = await _userManager.GetRolesAsync(appUser); var claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.Sid, appUser.Id)); claims.Add(new Claim(ClaimTypes.Name, user.UserName)); if (roleList.Count > 0) { //分配權限給使用者帳號 foreach (var role in roleList) { claims.Add(new Claim(ClaimTypes.Role, role)); } } //配置密鑰 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)); //配置憑證 var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); //產生 token var jwtToken = new JwtSecurityToken(issuer, audience, claims, expires: DateTime.UtcNow.AddSeconds(5), signingCredentials: credentials); var token = new JwtSecurityTokenHandler().WriteToken(jwtToken); return new { token, roleList, appUser.Avatar };//完成登入後,回傳給前端token與role list以及大頭照資訊 } catch (Exception ex) { throw; } } } } |
密鑰的長度要盡可能設長不然會報錯
IDX10653: The encryption algorithm 'System. String' requires a key size of at least 'System. Int32' bits.
Ref:
IDX10653: The encryption algorithm 'System. String' requires a key size of at least 'System. Int32' bits.
https://www.cnblogs.com/jiujiaoxiaoshun/p/16627792.html
留言
張貼留言