iT邦幫忙

0

Day 25|Authorization:使用者權限與任務擁有權管理

c#
  • 分享至 

  • xImage
  •  

今日學習目標

  • 理解身分驗證與授權的差別
  • 學會使用 [Authorize] 保護 Controller
  • 了解 ClaimTypes.NameIdentifier
  • 替 TaskItem 建立登入帳號的擁有者欄位
  • 讓 Service 依登入者身分篩選任務
  • 防止使用者修改或刪除其他人的任務
  • 使用兩個測試帳號驗證資料隔離

為什麼登入成功還不夠

今天的 Authorization 負責確認「你可以存取哪些資料」。
https://ithelp.ithome.com.tw/upload/images/20261024/20184430TkaZTmWh36.png

修改 TaskItem.cs

使用昨日專案

namespace TaskFlow3.Api.Models
{
    public class TaskItem
    {
        public int Id { get; set; }

        public string Title { get; set; } = "";

        public bool IsCompleted { get; set; }

        // Day 21:任務分類
        public int? CategoryId { get; set; }

        public Category? Category { get; set; }

        // Day 22:原本的 AppUser 關聯
        public int? UserId { get; set; }

        public AppUser? User { get; set; }

        // Day 25:Identity 帳號擁有者
        public string? OwnerIdentityId { get; set; }
    }
}

建立新的 Migration

檢查編譯:dotnet build
建立 Migration:dotnet ef migrations add AddTaskOwnership --context TaskFlowDbContext
更新資料庫:dotnet ef database update --context TaskFlowDbContext

修改 ITaskService.cs

using TaskFlow3.Api.Models;

namespace TaskFlow3.Api.Services
{
    public interface ITaskService
    {
        Task<List<TaskItem>> GetAllAsync(
            string ownerId,
            bool? completed = null,
            string? keyword = null,
            string sort = "asc"
        );

        Task<TaskItem?> GetByIdAsync(
            int id,
            string ownerId
        );

        Task<TaskItem> CreateAsync(
            string title,
            string ownerId
        );

        Task<bool> UpdateAsync(
            int id,
            string ownerId,
            string title,
            bool isCompleted
        );

        Task<bool> DeleteAsync(
            int id,
            string ownerId
        );
    }
}

修改 TaskService.cs

using Microsoft.EntityFrameworkCore;
using TaskFlow3.Api.Data;
using TaskFlow3.Api.Models;

namespace TaskFlow3.Api.Services
{
    public class TaskService : ITaskService
    {
        private readonly TaskFlowDbContext _db;

        public TaskService(TaskFlowDbContext db)
        {
            _db = db;
        }

        // 查看目前使用者的任務
        public async Task<List<TaskItem>> GetAllAsync(
            string ownerId,
            bool? completed = null,
            string? keyword = null,
            string sort = "asc")
        {
            IQueryable<TaskItem> query = _db.Tasks
                .AsNoTracking()
                .Where(t => t.OwnerIdentityId == ownerId);

            // 完成狀態篩選
            if (completed.HasValue)
            {
                query = query.Where(
                    t => t.IsCompleted == completed.Value
                );
            }

            // 關鍵字搜尋
            if (!string.IsNullOrWhiteSpace(keyword))
            {
                string searchText = keyword.Trim();

                query = query.Where(
                    t => t.Title.Contains(searchText)
                );
            }

            // 排序
            if (string.Equals(
                sort,
                "desc",
                StringComparison.OrdinalIgnoreCase))
            {
                query = query.OrderByDescending(t => t.Id);
            }
            else
            {
                query = query.OrderBy(t => t.Id);
            }

            return await query.ToListAsync();
        }

        // 查看指定任務
        public async Task<TaskItem?> GetByIdAsync(
            int id,
            string ownerId)
        {
            return await _db.Tasks
                .FirstOrDefaultAsync(
                    t => t.Id == id &&
                         t.OwnerIdentityId == ownerId
                );
        }

        // 新增自己的任務
        public async Task<TaskItem> CreateAsync(
            string title,
            string ownerId)
        {
            TaskItem task = new TaskItem
            {
                Title = title,
                IsCompleted = false,
                OwnerIdentityId = ownerId
            };

            _db.Tasks.Add(task);

            await _db.SaveChangesAsync();

            return task;
        }

        // 修改自己的任務
        public async Task<bool> UpdateAsync(
            int id,
            string ownerId,
            string title,
            bool isCompleted)
        {
            TaskItem? task = await _db.Tasks
                .FirstOrDefaultAsync(
                    t => t.Id == id &&
                         t.OwnerIdentityId == ownerId
                );

            if (task == null)
            {
                return false;
            }

            task.Title = title;
            task.IsCompleted = isCompleted;

            await _db.SaveChangesAsync();

            return true;
        }

        // 刪除自己的任務
        public async Task<bool> DeleteAsync(
            int id,
            string ownerId)
        {
            TaskItem? task = await _db.Tasks
                .FirstOrDefaultAsync(
                    t => t.Id == id &&
                         t.OwnerIdentityId == ownerId
                );

            if (task == null)
            {
                return false;
            }

            _db.Tasks.Remove(task);

            await _db.SaveChangesAsync();

            return true;
        }
    }
}

修改 TasksController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
using TaskFlow3.Api.DTOs;
using TaskFlow3.Api.Models;
using TaskFlow3.Api.Services;

namespace TaskFlow3.Api.Controllers
{
    [ApiController]
    [Authorize]
    [Route("api/[controller]")]
    public class TasksController : ControllerBase
    {
        private readonly ITaskService _taskService;

        public TasksController(ITaskService taskService)
        {
            _taskService = taskService;
        }

        // 從已驗證身分取得登入者 ID
        private string? GetOwnerId()
        {
            return User.FindFirstValue(
                ClaimTypes.NameIdentifier
            );
        }

        // GET /api/tasks
        [HttpGet]
        public async Task<ActionResult<List<TaskDto>>> GetTasks(
            [FromQuery] bool? completed = null,
            [FromQuery] string? keyword = null,
            [FromQuery] string sort = "asc")
        {
            string? ownerId = GetOwnerId();

            if (string.IsNullOrEmpty(ownerId))
            {
                return Unauthorized();
            }

            var tasks = await _taskService.GetAllAsync(
                ownerId,
                completed,
                keyword,
                sort
            );

            var result = tasks.Select(ToDto).ToList();

            return Ok(result);
        }

        // GET /api/tasks/1
        [HttpGet("{id:int}")]
        public async Task<ActionResult<TaskDto>> GetTask(int id)
        {
            string? ownerId = GetOwnerId();

            if (string.IsNullOrEmpty(ownerId))
            {
                return Unauthorized();
            }

            TaskItem? task = await _taskService.GetByIdAsync(
                id,
                ownerId
            );

            if (task == null)
            {
                return NotFound("找不到這筆任務");
            }

            return Ok(ToDto(task));
        }

        // POST /api/tasks
        [HttpPost]
        public async Task<ActionResult<TaskDto>> CreateTask(
            [FromBody] CreateTaskDto input)
        {
            string? ownerId = GetOwnerId();

            if (string.IsNullOrEmpty(ownerId))
            {
                return Unauthorized();
            }

            if (string.IsNullOrWhiteSpace(input.Title))
            {
                ModelState.AddModelError(
                    nameof(input.Title),
                    "任務名稱不可空白"
                );

                return ValidationProblem(ModelState);
            }

            string title = input.Title.Trim();

            if (title.Length < 2 || title.Length > 100)
            {
                ModelState.AddModelError(
                    nameof(input.Title),
                    "任務名稱必須介於 2 到 100 個字元"
                );

                return ValidationProblem(ModelState);
            }

            TaskItem task = await _taskService.CreateAsync(
                title,
                ownerId
            );

            return CreatedAtAction(
                nameof(GetTask),
                new { id = task.Id },
                ToDto(task)
            );
        }

        // PUT /api/tasks/1
        [HttpPut("{id:int}")]
        public async Task<IActionResult> UpdateTask(
            int id,
            [FromBody] UpdateTaskDto input)
        {
            string? ownerId = GetOwnerId();

            if (string.IsNullOrEmpty(ownerId))
            {
                return Unauthorized();
            }

            if (string.IsNullOrWhiteSpace(input.Title))
            {
                ModelState.AddModelError(
                    nameof(input.Title),
                    "任務名稱不可空白"
                );

                return ValidationProblem(ModelState);
            }

            string title = input.Title.Trim();

            if (title.Length < 2 || title.Length > 100)
            {
                ModelState.AddModelError(
                    nameof(input.Title),
                    "任務名稱必須介於 2 到 100 個字元"
                );

                return ValidationProblem(ModelState);
            }

            bool success = await _taskService.UpdateAsync(
                id,
                ownerId,
                title,
                input.IsCompleted
            );

            if (!success)
            {
                return NotFound("找不到這筆任務");
            }

            return NoContent();
        }

        // DELETE /api/tasks/1
        [HttpDelete("{id:int}")]
        public async Task<IActionResult> DeleteTask(int id)
        {
            string? ownerId = GetOwnerId();

            if (string.IsNullOrEmpty(ownerId))
            {
                return Unauthorized();
            }

            bool success = await _taskService.DeleteAsync(
                id,
                ownerId
            );

            if (!success)
            {
                return NotFound("找不到這筆任務");
            }

            return NoContent();
        }

        // Model 轉 DTO
        private static TaskDto ToDto(TaskItem task)
        {
            return new TaskDto
            {
                Id = task.Id,
                Title = task.Title,
                IsCompleted = task.IsCompleted
            };
        }
    }
}

編譯與測試

在 VS Code 終端機執行:dotnet build
再啟動:dotnet run
在第二個 PowerShell 終端機設定:$base = "http://localhost:5130"
測試 1:沒有登入

try {
    Invoke-WebRequest -Uri "$base/api/tasks" -UseBasicParsing
}
catch {
    Write-Host "HTTP 狀態碼:" ([int]$_.Exception.Response.StatusCode)
}

https://ithelp.ithome.com.tw/upload/images/20261024/20184430GGJEkcHUP7.png
登入 Alice:

$login = @{
    email = "student@example.com"
    password = "DemoOnly_2026!"
} | ConvertTo-Json

Invoke-RestMethod `
    -Uri "$base/auth/login?useCookies=true" `
    -Method Post `
    -ContentType "application/json" `
    -Body $login `
    -SessionVariable aliceSession

新增 Alice 的任務:

$body = @{
    title = "Alice 的 C# 作業"
} | ConvertTo-Json

$created = Invoke-RestMethod `
    -Uri "$base/api/tasks" `
    -Method Post `
    -ContentType "application/json" `
    -Body $body `
    -WebSession $aliceSession

$created

https://ithelp.ithome.com.tw/upload/images/20261024/20184430ONhJXzSdzA.png
接著查看自己的任務:

Invoke-RestMethod `
    -Uri "$base/api/tasks" `
    -WebSession $aliceSession

登入 Bob:

$bobLogin = @{
    email = "bob@example.com"
    password = "DemoOnly_2026!"
} | ConvertTo-Json

Invoke-RestMethod `
    -Uri "$base/auth/login?useCookies=true" `
    -Method Post `
    -ContentType "application/json" `
    -Body $bobLogin `
    -SessionVariable bobSession

https://ithelp.ithome.com.tw/upload/images/20261024/20184430v1oSuz43u9.png
以 Bob 的登入狀態查詢 Alice 剛新增的任務:

Invoke-RestMethod `
    -Uri "$base/api/tasks/$($created.id)" `
    -WebSession $bobSession

*提醒邦友,使用第三方服務/API 時,請務必評估資安風險與隱私保護
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言