iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Modern Web

不只是登入畫面!一起打造現代化登入系統系列 第 20

屋內安全[ 9 / 9 ]:權限配置與 Redux 管理登入狀態|角色控制 / 動態功能鎖定

  • 分享至 

  • xImage
  •  

在前面幾節,我們已經完成:

  • Token 驗證(Express / Firebase Functions / MSW)
  • Token 儲存(LocalStorage / Cookie)
  • Protected Route + AuthContext

下一步就來處理實務上最常見的需求:「角色與權限管理」
例如:

使用者角色 能進的頁面 能做的操作
admin 所有頁面 刪除、修改、審核
editor /dashboard, /posts 編輯但不能刪除
user /profile 只能看自己資料

如果你打算做中大型專案,那麼用 Redux 來集中管理 Auth 狀態與權限會讓架構更清楚。


為什麼要改用 Redux 管理登入狀態?

功能 AuthContext Redux Toolkit
小專案好上手
全域共享狀態
可拆 slice / middleware
可支援角色、權限、loading 狀態等複雜管理 ✅ ✅ ✅
DevTools 可追蹤狀態變化
適合大型專案 ✅ ✅ ✅

建立 authSlice

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { loginAndSetCookie, logout, getProtectedData } from "../utils/auth";

// 異步登入
export const loginThunk = createAsyncThunk(
  "auth/login",
  async ({ email, password }) => {
    const data = await loginAndSetCookie(email, password);
    return data; // { uid, email }
  }
);

// 驗證當前使用者(從 Cookie)
export const fetchCurrentUser = createAsyncThunk(
  "auth/me",
  async () => {
    const data = await getProtectedData(); // 後端從 Cookie 驗證
    return data.user; // { uid, email, role }
  }
);

const authSlice = createSlice({
  name: "auth",
  initialState: {
    user: null,
    role: null, // admin / editor / user ...
    loading: false,
  },
  reducers: {
    clearAuth: (state) => {
      state.user = null;
      state.role = null;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(loginThunk.pending, (state) => {
        state.loading = true;
      })
      .addCase(loginThunk.fulfilled, (state, action) => {
        state.loading = false;
        state.user = action.payload;
        state.role = action.payload.role || "user";
      })
      .addCase(fetchCurrentUser.fulfilled, (state, action) => {
        state.user = action.payload;
        state.role = action.payload.role || "user";
      });
  },
});

export const { clearAuth } = authSlice.actions;
export default authSlice.reducer;

建立 Redux Store

import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./authSlice";

export const store = configureStore({
  reducer: {
    auth: authReducer,
  },
});

在 App 最外層包住 Provider

import { Provider } from "react-redux";
import { store } from "./store";

ReactDOM.createRoot(document.getElementById("root")).render(
  <Provider store={store}>
    <AppRoutes />
  </Provider>
);

ProtectedRoute 加上「角色判斷」

import { Navigate } from "react-router-dom";
import { useSelector } from "react-redux";

export default function ProtectedRoute({ children, roles = [] }) {
  const { user, role, loading } = useSelector((state) => state.auth);

  if (loading) return <p>驗證中...</p>;
  if (!user) return <Navigate to="/login" replace />;

  if (roles.length > 0 && !roles.includes(role)) {
    return <p>你沒有權限(需要: {roles.join(", ")})</p>;
  }

  return children;
}

配置不同權限的路由

<Route
  path="/admin"
  element={
    <ProtectedRoute roles={["admin"]}>
      <AdminPanel />
    </ProtectedRoute>
  }
/>

<Route
  path="/editor"
  element={
    <ProtectedRoute roles={["admin", "editor"]}>
      <EditorPage />
    </ProtectedRoute>
  }
/>

上一篇
屋內安全[ 8 / 8 ]:正式環境 Token 儲存:用 HttpOnly Cookie 才是真防禦力
下一篇
安全再進化[1 / 5 ]:Firebase Email 驗證信
系列文
不只是登入畫面!一起打造現代化登入系統23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言