尚品甄選電商SpringBoot-Web開發之用戶登入與角色管理

相關資源

尚品甄選教程

官方資料(百度網盤)

SSM教程

網友整理的完整程式碼

後端程式碼

前端程式碼

前置作業

使用開源模板

vue3 和 element-plus UI 框架,vite 建構工具、pinia 狀態管理、vue-router 路由管理、mockjs 數據模擬,並整合了 typescript,功能由 Vue Element Admin 移植而来。

  • 使用node 16.9.0

Redis建立

原文是有一個虛擬機來放置docker,而我是直接使用windows的docker,所以在設定的時候和原文不太一樣

原文命令

docker run -d -p 6379:6379 --restart=always -v redis-config:/etc/redis/config -v redis-data:/data --name redis redis redis-server /etc/redis/config/redis.conf

各命令的解釋(來源chatgpt)

docker run: This command is used to run a Docker container.
-d: It stands for “detached” mode, which means the container runs in the background.
-p 6379:6379: This option maps port 6379 on the host to port 6379 in the container. This allows external processes to communicate with Redis through port 6379.
--restart=always: This option specifies that the container should always restart automatically if it stops for any reason.
-v redis-config:/etc/redis/config: This option creates a volume named “redis-config” and mounts it to the “/etc/redis/config” directory inside the container. This allows you to provide custom Redis configuration files.
-v redis-data:/data: This option creates a volume named “redis-data” and mounts it to the “/data” directory inside the container. This is used to persist Redis data, allowing it to survive container restarts or deletion.
--name redis: This option assigns the name “redis” to the container.
redis: This is the name of the Docker image used to create the container. In this case, it’s the official Redis image from Docker Hub.
redis-server /etc/redis/config/redis.conf: This command instructs the Redis server to use the configuration file located at “/etc/redis/config/redis.conf” inside the container.

唯一有問題的就是redis.config不知道要放哪,根據-v redis-config:/etc/redis/config,是映射關係,也就是redis-config這個volume映射到後面那個路徑,於是我建立了一個資料夾,然後照樣建立redis.conf,然後映射過去變為以下指令

docker run -d -p 6379:6379 --restart=always -v D:/SimonLai/shangpinzhenxuan/spzx-parent/redis:/etc/redis/config -v redis-data:/data --name redis redis:7.0.10 redis-server /etc/redis/config/redis.conf

應該要弄一個linux系統的虛擬機,這樣會比較方便,不過教程沒有細講,所以就直接用了win10,之後太麻煩的話就考慮全部搬到虛擬機去了

啟動的問題

直接執行會出現Getting java.lang.IllegalStateException: Logback configuration error detected error...

原因是logback的設定檔案logback-spring.xml裡面輸出路徑要改成自己的

<property name="log.path" value="D:\SimonLai\shangpinzhenxuan\log" />

用戶登入-異常處理

為了統一遇到異常(錯誤)時回傳的資訊,定義新的異常,並且建立global異常處理器來統一管理

於是新增了GlobalExceptionHandler裡面目前定義了異常的部分就是回傳enumLOGIN_ERROR這些是原先定義好的部分

以及自訂SimonException,定義好其constructor,這樣被throw的時候就可以傳入指定的值,exception就會被拋出,因為GlobalExceptionHandler裡已經定義好,如下

@ExceptionHandler(SimonException.class)
@ResponseBody
public Result error(SimonException e) {
    return Result.build(null, e.getResultCodeEnum());
}

用戶登入(前端接入)

直接在vscodecmd輸入npm start可能會看到以下錯誤

npm ERR! enoent Could not read package.json: Error: ENOENT: no such file or directory, open 'D:\SimonLai\shangpinzhenxuan\spzx-admin\package.json'

實際上只是因為在workspacecmd的初始位置是在上一層的資料夾,而我們需要到程式的前端資料夾內執行

切換路徑

使用前端記得先執行後端,這樣才能調用後端資料庫

跨域問題

前端程式,目前把請求導向port 8501(也就是後端的port),但是這樣會出現跨域請求的問題

請求8501

跨域請求錯誤

解決方法: 在後端設定開啟跨域(前端也可以設定代理等等解決,不過此教程為後端導向)

先前有登入過的話可能再次開啟port 3001會是空白的,因為沒有設定跳轉頁面,可以使用這個網址進到登入頁面

使用帳號admin,密碼111111登入,此時如果按照步驟來,會在控制台看到404 not found因為還沒有設定拉取userinfo資料的路徑

圖片驗證碼產生

這部分沒什麼問題,照著做就可以成功了,步驟如下

  1. 編寫後端邏輯,建立API
  2. 調用hutool的功能建立驗證碼圖片,使用Base64編碼
  3. 前端調用後端API,並且修改htmlAjax調用API顯示圖片

圖片驗證碼驗證

SysUserServiceImpl.java

  1. 驗證帳號密碼之前先驗證圖片驗證碼是否正確

取得當前登入用戶的資料

有兩種從request拿到token的方式:

第一種
// 取得當前登入用戶資料
@GetMapping(value = "/getUserInfo")
public Result getUserInfo(HttpServletRequest request) {
    // 取得token
    String token = request.getHeader("token");
    // 根據token查詢redis以取得用戶資料
    SysUser sysUser = sysUserService.getUserInfo(token);
    
    return Result.build(sysUser, ResultCodeEnum.SUCCESS);
}
第二種
// 取得當前登入用戶資料
@GetMapping(value = "/getUserInfo")
public Result getUserInfo(@RequestHeader(name = "token") String token /* 取得token */) {
    // 根據token查詢redis以取得用戶資料
    SysUser sysUser = sysUserService.getUserInfo(token);

    return Result.build(sysUser, ResultCodeEnum.SUCCESS);
}

同時這部分也改了好幾個前端的code,一直出錯,最後找到原因是前端接後端的getUserInfoAPI沒有寫對

這裡主要就是後端邏輯寫好,前端連一下API就搞定

用戶登出

  1. 點退出登入
  2. 根據headertoken呼叫service中的logout方法
  3. logout方法直接依照token查詢,登出時直接把token也刪除,完成登出
  4. 前端的部分就是呼叫後端方法

登入驗證

如何做到?


flowchart TD

A[攔截器Interceptors] --> B{當前路徑需要驗證登入}
B -->|是| C[從header嘗試取得token,並查詢redis]
B -->|否| 通過
C --> D[redis是否有用戶資訊]
D --> |是| E[取得用戶資訊,存到ThreadLocal]
D --> |否| F[不是登入狀態,回傳提示訊息]
E --> 更新redis資料過期時間

  • 需要把攔截器加入到Spring MVC中,設定攔截的路徑

延伸文章: Java - ThreadLocal 類的使用

優化

  1. 把排除的路徑寫入configuration,減少冗長code
  2. 因為上面把用戶資料放入threadLocal,所以不需要從資料庫去撈,直接取threadLocal的即可
  3. 前端: 判斷如果狀態碼是208(表示未登入),直接跳轉到登入頁面

把排除的路徑寫入configuration

  • 寫一個configurationProperty並且加入到Spring啟動檔案
  • 把路徑寫入設定檔案

從threadLocal取得用戶資料

// 取得當前登入用戶資料
@GetMapping(value = "/getUserInfo")
public Result getUserInfo(@RequestHeader(name = "token") String token /* 取得token */) {
    // 根據token查詢redis以取得用戶資料
    SysUser sysUser = sysUserService.getUserInfo(token);

    return Result.build(sysUser, ResultCodeEnum.SUCCESS);

// 變為
public Result getUserInfo() {
    return Result.build(AuthContextUtil.get(), ResultCodeEnum.SUCCESS);
}

狀態碼208跳轉登入頁面

response => { // service.interceptors.response.use第一个参数
    const res = response.data
    if (res.code == 208) {
        const redirect = encodeURIComponent(window.location.href)  // 当前地址栏的url
        router.push(`/login?redirect=${redirect}`)
        return Promise.reject(new Error(res.message || 'Error'))
    }
    return res 
}

權限管理

例如:

Lucy是總經理,可以使用menu所有tabs
Mary是Sales,僅可使用商品管理menu

大致有三張表:

  • 用戶表
  • 角色表
  • 選單表

用戶與角色是多對多關係,為了實做這個部分,需要一張角色用戶關係表(類似map)
角色與選單也是多對多關係

關係表

Project資料庫表格關係

小練習:

  • 查詢id1的用戶的角色資料
SELECT sr.* FROM
sys_role sr inner join sys_user_role sur
on sr.id = sur.role_id 
WHERE sur.user_id = 5
  • 查詢id1的用戶的menu資料

這個比較複雜一點因為需要關聯三張表,把他們的id透過關係表連接起來

SELECT DISTINCT m.* FROM sys_menu m
INNER JOIN sys_role_menu rm ON rm.menu_id = m.id
INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id
WHERE ur.user_id=5

準備工作

  • 前端新增角色定向
    • router中新增定向
  • 建立角色頁面
    • views中新增介面給menu, role以及user
  • 後端角色相關controller, service, mapper建立

需要實作的功能:

  1. 根據角色名稱條件搜尋
  2. 分頁顯示

後端介面

使用pagehelper plugin

  • SysRoleController: 加入findByPage方法,並傳入三個參數(current, limit, sysRoleDto),調用sysRoleService中的findByPage
@PostMapping("/findByPage/{current}/{limit}")
public Result findByPage(@PathVariable("current") Integer current,
                            @PathVariable("limit") Integer limit,
                            @RequestBody SysRoleDto sysRoleDto) {
    // pageHelper plugin實作分頁
    PageInfo<SysRole> pageInfo = sysRoleService.findByPage(sysRoleDto, current, limit);
    return Result.build(pageInfo, ResultCodeEnum.SUCCESS);
}
  • SysRoleServiceImpl: 加入findByPage方法,也是同樣的三個參數,設定分頁參數(起始分頁)以及根據條件搜尋資料並且封裝成pageInfo物件並回傳
@Override
public PageInfo<SysRole> findByPage(SysRoleDto sysRoleDto, Integer current, Integer limit) {
    // 設定分頁參數(因為使用了pagehelper plugin)
    PageHelper.startPage(current, limit);
    // 根據條件查詢所有資料
    List<SysRole> list = sysRoleMapper.findByPage(sysRoleDto);
    // 封裝pageInfo物件
    PageInfo<SysRole> pageInfo = new PageInfo<>(list);
    return pageInfo;
}
  • SysRoleMapper.xml(SysRoleMapper調用): 加入sql語句,根據條件搜尋,如果roleName為空就代表無條件搜尋
<!--  映射搜尋到的 -->
<resultMap id="sysRoleMap" type="com.simonlai.spzx.model.entity.system.SysRole" autoMapping="true"></resultMap>

<!-- 用於select -->
<sql id="columns">
    id,role_name,role_code,description,create_time,update_time,is_deleted
</sql>

<select id="findByPage" resultMap="sysRoleMap">
    select <include refid="columns"/>
    from sys_role
    <where>
        <if test="roleName != null and roleName != ''">
            and role_name like concat('%', #{roleName}, '%')
        </if>
    </where>
    order by id desc
</select>

前端介面

修改並完善sysRole頁面

  • sysRole.js: 介面Post請求後端資料
// 角色列表
export const GetSysRoleListByPage = (current, limit, queryDto) => {
    return request({
        // ``模板字串
        url: `${base_api}/findByPage/${current}/${limit}`, // 路徑
        method: 'post', // 提交方式
        // 接口@RequestBody 前端 data:名稱,以JSON格式傳遞
        // 接口沒有註解,前端 params:名稱,以URL格式傳遞
        data: queryDto, // 其他參數
    })
}
  • sysRole.vue頁面: 修改按鈕觸發method,並且顯示資料
<script setup>
import {ref, onMounted} from 'vue'
import {GetSysRoleListByPage} from '@/api/sysRole'

// 定義資料模型
let list = ref([]) // 角色列表

let total = ref(0) // 紀錄總數

const pageParamsForm = {
  page: 1, // 當前頁
  limit: 3, // 每頁紀錄量
}
const pageParams = ref(pageParamsForm)

const queryDto = ref({"roleName":""}) // 條件封裝資料

// 鉤子函數
onMounted(() => {
  fetchData()
})

// 操作方法: 列表方法和搜尋方法
// 列表方法: axios請求後端數據
const fetchData = async () => {
  const {data, code, message} = await GetSysRoleListByPage(pageParams.value.page, pageParams.value.limit, queryDto.value)
  list.value = data.list
  total.value = data.total
}

// 搜尋方法
const searchSysRole = () => {
  fetchData()
}
</script>

總結一下上述這兩個前端與後端的操作,大致上的步驟如下:

  1. 用戶在頁面上輸入角色條件點搜索
  2. 會觸發sysRole.vue的按鈕,這個按鈕會調用searchSysRole方法(在script裡)
  3. searchSysRole方法會調用fetchData
  4. fetchData會調用sysRole.js中的GetSysRoleListByPage方法嘗試取得資料
  5. GetSysRoleListByPage方法會透過api請求後端資料
  6. 後端SysRoleController接收到請求,調用SysRoleServicefindByPage
  7. SysRoleService再調用SysRoleMapper進行資料庫請求,執行sql語句
  8. 資料取得後SysRoleController再把資料傳給前端
  9. 前端收到資料後顯示

角色新增介面

前端的部分利用element-plus快速構建一個彈出提示框,可以輸入角色名稱、角色Code,並且提交

提交後通過js請求後端,後端再通過controller->service->mapper加入資料並回傳,如果加入成功,code200,就關掉提示框,顯示添加成功並且重新載入數據以顯示更新後的

角色修改界面

基本上是和新增大同小異,以下幾點需特別注意

  • 修改時彈框中顯示的數據可以使用{...row},否則直接使用row會造成邊修改,介面上的資料也會跟著修改,即便還沒提交
  • 修改與添加同樣都是透過submit函式處理,所以可以藉由判斷sysRole是否有id來判斷要呼叫哪個api
  • 後端部分也是比較簡單的controller->service->mapper
  • 更新時要判斷每個值是否存在,否則就不更新
<!-- 角色修改方法 -->
<update id="update">
    update sys_role set
    <if test="roleName != null and roleName != ''">
        role_name = #{roleName},
    </if>
    <if test="roleCode != null and roleCode != ''">
        role_code = #{roleCode},
    </if>
    <if test="description != null and description != ''">
        description = #{description},
    </if>
    update_time = now()
    where
    id = #{id}
</update>

角色刪除功能

  • 根據id刪除角色
  • 使用邏輯刪除而非物理刪除,也就是利用is_deleted這個參數,刪除時把該筆資料標記為1,代表已刪除,查詢時查找is_deleted0的所有數據
<!-- 角色刪除方法 -->
<update id="delete">
    update sys_role set is_deleted=1 where id=#{roleId}
</update>

<!-- 角色列表方法 -->
<select id="findByPage" resultMap="sysRoleMap">
    select <include refid="columns"/>
    from sys_role
    <where>
        <if test="roleName != null and roleName != ''">
            and role_name like concat('%', #{roleName}, '%')
        </if>
        <!-- 查找尚未被刪除的 -->
        and is_deleted=0
    </where>
    order by id desc
</select>

尚品甄選電商SpringBoot-Web開發之用戶登入與角色管理
https://f88083.github.io/2024/04/17/尚品甄選電商SpringBoot-Web開發之用戶登入與角色管理/
作者
Simon Lai
發布於
2024年4月17日
許可協議