手把手擼一個前後端分離Admin系統(一)由使用者登入而來的JWT鑑權以

solution發表於2021-09-09

01 - 由使用者登入而來的 JWT 鑑權,許可權管理

前言

當一個新手學習 React 想要找一個練手的專案,結果市面上開源的都是各種 TODO App,甚至於 Real World 系列也差強人意。付費課程大多也很難詳盡地講清楚方方面面。同時,Admin, dashboard 之類的中後臺的需求日愈旺盛,值得前端開發者多多關注。於是就有了的產生,本文是其系列文章手把手帶你擼一個前後端分離的 Admin 專案的開篇文章

誰適合食用?

  • 想要掌握 JWT 鑑權,許可權管理(前後端)
  • 想了解真實專案上線
  • 不適合全棧

技術棧

本專案前端基於 CRA 搭建,技術棧涉及 es6+, react, react-router, react-redux(以及 redux-devTools, redux-thunk, react-persist 等中介軟體),axios 和 ant design of react,sass, CSS Module 等等,後端基於 nest-cli 搭建,技術棧涉及 , nest.js 及其中介軟體,mongodb, mongoose, rxjs 等等,運維涉及 MEAN 環境搭建(涉及到 Nginx 的偏多),PM2 部署 node 應用。

為什麼不用 antd pro

  • 不喜歡 antd pro + umi + dva 的繫結(儘管它們很優秀)
  • antd pro 內容太多,初學者拿來學習中後臺開發難以找到頭緒
  • 從零打造一個 admin 系統,一點點最佳化處來,有成就感,也順手一些

為什麼使用 Nest.js

  • 全面支援
  • 基於 Express.js,方便使用其豐富的中介軟體生態
  • 受 Angular 啟發的架構

    while plenty of superb libraries, helpers, and tools exist for Node (and server-side JavaScript), none of them effectively solve the main problem of - Architecture. Nest provides an out-of-the-box application architecture which allows developers and teams to create highly testable, scalable, loosely coupled, and easily maintainable applications - By Nest.js Official

一分鐘瞭解 Nest.js 核心概念

熟悉 Angular 的朋友看下 Nest.js 官方的 Overview 就可以愉快地 CRUD 了,不熟悉的朋友瞭解一下幾點就可以了:

  • 透過模組樹來組織應用結構
    圖片描述
  • controller, module, service 是 Nest 三劍客,是基礎要素,controller 負責處理入站請求,返回相應到客戶端,module 實現某功能封裝,把業務邏輯放到 service 裡進行處理,service 可以透過依賴注入到 controller 和其他 service 中去
  • Express.js 是一個基於 middleware 的中介軟體,所以 Nest 也是。像 Guards, Filters, Interceptors 等等實際上都是 middleware,只是具有不用功能,且執行順序不同
    圖片描述

學習 Nest Tips:

  1. 反覆閱讀,因為它的資料不算多
  2. 程式碼示例可以參考,搭配 Octotree 食用更佳,非常全面
  3. 遇到問題,可以依照 stackoverflow --> issues 區 —> Nest Discord 伺服器,尋找解決辦法

步驟

  1. 前後端專案初始化

    # 強烈建議換源頭,同時安裝nrm方便切換registry
    # 建議安裝nvm,實現多版本node安裝
    
    # 透過cra腳手架安裝
    npx create-react-app admin-fe
    
    # 安裝nest li
    npm i -g @nestjs/cli
    
    # 建立新的專案
    nest new admin-be
    
    
  2. 前端刪除包括 serviceWorker 在內的多餘檔案

  3. 前端

  4. 前端新增 Antd

    因為 antd v3 中打包會把所有圖示都搭進去,造成 bundle 會特別大,所以建議安裝 v4 版本,從 v3 升級到 v4 讓人一言難盡。

  5. 前端使用 reset-css 重置樣式,樣式方案選擇 Sass + CSS module

    關於 react 的樣式方案有很多,Vanilla Css, CSS module, Css in JS,選擇 CSS module 的原因是使用非常簡單,可以看這篇文章 -

前端路由分類以及實際應用

根據 react-router 官方的說法,他們把前端路由分為 Static Routing 和 如下:

In these frameworks, you declare your routes as part of your app’s initialization before any rendering takes place.

所以,Static Routing 指的是在 rendering 發生前定義 routes,給個 express.js 示例:

```
app.get("/", handleIndex);
app.get("/invoices", handleInvoices);
app.get("/invoices/:id", handleInvoice);
app.get("/invoices/:id/edit", handleInvoiceEdit);

app.listen();
```

Dynamic Routing 意味著 Router 作為元件存在,在 rendering 過程中生效,示例如下:

````
import { BrowserRouter as Router } from "react-router-dom";

ReactDOM.render(
    <Router>
        <App />
    </Router>, el);

const App = () => (
    <div>
        <nav>
        <Link to="/dashboard">Dashboard</Link>
        </nav>
    </div>
);
```

關於 Nested Routes 和 Responsive Routes,動態路由都有非常好的表現。如果使用過 umijs 的朋友都會知道,umi 中支援配置式路由和約定式路由,其中配置式路由也就是動態路由,只不過把 router config 抽出到一個單獨的 js 檔案中,約定式路由也叫檔案路由,就是不需要手寫配置,檔案系統即路由,透過目錄和檔案及其命名分析出路由配置,應該是借鑑了 next.js 的。

  1. 建立路由配置檔案 router.config.js

     import React from "react";
     import { Link } from "react-router-dom";
     import Loadable from "react-loadable";
     import Loading from "./Loading";
    
     // 使用react-Loadable來做程式碼拆分
     const Home = Loadable({
         loader: () => import("pages/Home/Home"),
         loading: Loading,
         delay: 300
     });
     const Charts = Loadable({
         loader: () => import("pages/Charts/Charts"),
         loading: Loading,
         delay: 300
     });
    
     ...
    
    

    為什麼使用 react-Loadabl 而不用 suspense, lazy 做 code splitting?使用 suspense,當網速足夠快, 資料立馬就獲取到了,頁面會閃一下,這是因為載入 loading 了,而maxDuration 屬性只有在 Concurrent Mode下才能使用

  2. 前端配置路由守衛

     // src/router/RouteGuard.jsx
     import React from "react";
     import { Route, Redirect, useLocation } from "react-router-dom";
     import { checkPermission } from "./checkPermission";
    
     function PrivateRoute({ children, ...rest }) {
     let location = useLocation();
     let { isAuth, definedRoles } = { ...rest };
    
     // 許可權不足跳轉(已登入)
     checkPermission(isAuth, definedRoles, location.pathname);
    
     // 未認證跳轉
     return (
         <Route
         {...rest}
         render={({ location }) =>
             isAuth ? (
             children
             ) : (
             <Redirect
                 to={{
                 pathname: "/login",
                 state: { from: location }
                 }}
             />
             )
         }
         />
     );
     }
    
     export default PrivateRoute;
    
     // src/router/checkPermission.js
     import React from "react";
     import { Redirect } from "react-router-dom";
     import { getRoles } from "utils/storage";
    
     export const checkPermission = (isAuth, definedRoles, pathname) => {
         if (isAuth && getAuthorizedState(definedRoles, pathname)) {
             return <Redirect to={{ pathname: "/not-allow" }} />;
         }
     };
    
         const getAuthorizedState = (definedRoles, pathname) => {
         // 不需要認證路徑
         const isNotAuthedPath = ["/login", "/not-found", "/not-allow"].find(
             path => path === pathname
         );
    
         let roles = getRoles() || [];
    
         // 條件為認證路徑下,前端獲取的roles陣列非空,並且包含在路由定義的陣列中
         return (
             !isNotAuthedPath &&
             (!roles || !roles.every(role => !!definedRoles.find(item => item === role)))
         )
     };
    
  3. 使用者註冊

    // user.controller.ts 處理/user/signup的POST請求
    import { Controller, Post } from '@nestjs/common';
    
    @Controller('user')
    export class UserController {
    
         // 註冊
         @Post("signup")
         async signup(@Body() Body) {
             const data = await this.userService.createUser(Body);
             return data;
         }
    }
    
    // user.service.ts 註冊邏輯
    export class UserService {
     constructor(
         @InjectModel("User") private readonly userModel: Model<User>
     ){}
    
         // 註冊處理邏輯
     async createUser(userDto: UserDto): Promise<any> {
         const { username, password, roles = ["user"], isEnabled = false } = userDto;
         // 從mongodb根據username查詢user
         const user = await this.findUser(username);
         console.log("user", user);
         // 驗證使用者是否存在(不能為admin)
         if (user) {
         throw new HttpException(
             {
             status: HttpStatus.FORBIDDEN,
             error: "使用者已經存在"
             },
             403
         );
         } else if (username === "admin") {
         throw new HttpException(
             {
             status: HttpStatus.FORBIDDEN,
             error: "無權註冊admin"
             },
             403
         );
         }
    
         // 給使用者加密
         const hashPwd = await this.encryptService.getEncrypted(password);
         // 建立user(admin使用者此方法無法建立)
         const newUser = new this.userModel({
         username: username,
         password: hashPwd,
         roles: roles,
         isEnabled: isEnabled
         });
         await newUser.save();
         return `註冊${username}成功!`;
     }
    }
    

使用者登入流程圖

圖片描述

應用上線流程圖

圖片描述

未完待續,有空繼續補上…

參考文件

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2459/viewspace-2825242/,如需轉載,請註明出處,否則將追究法律責任。

相關文章