目標:
在上一次https://www.cnblogs.com/webor2006/p/17533745.html我們已經完成了文檔管理的功能模塊開發,接下來則開啟新模塊的學習---用戶登錄,這塊還是有不少知識點值得學習的,先來看一下整體的效果,關于效果官網有一個體驗地址:wiki.courseimooc.com,如下:
其效果也是人人熟知的,下面直接開擼。
用戶表設計與持久層代碼生成:
用戶表設計:
一個模塊的開始通常就是從表的設計開始,這里先來將用戶表的sql貼出來,當然實際對于表設計肯定不會直接給出個sql,表的設計其實也是非常有學問的,這里重點是操練功能,所以直接貼sql了:
-- 用戶表
drop table if exists `user`;
create table `user`
(`id` bigint not null comment 'ID',`login_name` varchar(50) not null comment '登陸名',`name` varchar(50) comment '昵稱',`password` char(32) not null comment '密碼',primary key (`id`),unique key `login_name_unique` (`login_name`)
) engine = innodb default charset = utf8mb4 comment ='用戶';
然后執行一下sql:
此時查看一下數據庫中用戶表有木有生成?
妥妥的,然后我們默認生插入一個用戶數據:
看一下表數據:
持久層代碼生成:
接下來則來生成持久層的代碼,這塊在之前已經用得很熟練了,就不過多說明:
然后執行此命令開始生成:
此時看一下本地生成的文件是否正常生成?
在這里我其實一直有一個反思:對于一個初學者來說,這樣逃避手寫sql層的代碼是不是不利于自己的學習呀,其實我看了下公司Java后端的代碼是沒有使用這種自動生成的方式的,但是,多學一種“高效率”的方式有利無害呀,畢竟對于實際做項目來說效率是非常重要的,哪怕公司里沒用到,到時寫自己的項目是有可能用到的呀,總之,擁抱變化,任何學到的新知識,在未來總會發揮它的余熱的~~
完成用戶表基本增刪改查功能:
如之前實現電子書和文檔的增刪改查功能一樣,使用非常厲害的CV大法就可以了,這邊不厭其煩地再來走一遍流程,下面開始。
后端代碼:
1、UserController:
這里從EbookController拷貝過來:
然后再替換一下里面的內容,兩個步驟,還記得么?
此時代碼中一堆紅,不要理,之后隨著全局替換完都會自動消失了。
2、UserService:
同樣拷貝至EbookService:
然后里面的內容也是那兩步進行全局替換,這里就不說明了,替換完成之后,有一個點這里需要修改一下:
3、UserQueryReq:
這里查詢只需根據用戶名查,這里直接貼內容:
package com.cexo.wiki.req;public class UserQueryReq extends PageReq {private String loginName;public String getLoginName() {return loginName;}public void setLoginName(String loginName) {this.loginName = loginName;}@Overridepublic String toString() {return "UserQueryReq{" +"loginName='" + loginName + '\'' +"} " + super.toString();}
}
4、UserQueryResp:
這個直接從domain中的User類中拷貝既可:
5、UserSaveReq:
它也可以拷至EbookSaveReq,不過表單校驗規則需要改一下,這里直接將內容貼出:
package com.cexo.wiki.req;import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;public class UserSaveReq {private Long id;@NotNull(message = "【用戶名】不能為空")private String loginName;@NotNull(message = "【昵稱】不能為空")private String name;@NotNull(message = "【密碼】不能為空")// @Length(min = 6, max = 20, message = "【密碼】6~20位")@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密碼】至少包含 數字和英文,長度6-32")private String password;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getLoginName() {return loginName;}public void setLoginName(String loginName) {this.loginName = loginName;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append(getClass().getSimpleName());sb.append(" [");sb.append("Hash = ").append(hashCode());sb.append(", id=").append(id);sb.append(", loginName=").append(loginName);sb.append(", name=").append(name);sb.append(", password=").append(password);sb.append("]");return sb.toString();}
}
其中有個小細節需要說明一下:
最后需要解決一個報錯:
對于用戶來說只需要根據用戶名稱來查詢,所以需要改一下條件,如下:
前端代碼:
1、 admin-user.vue:
它的內容同樣可以拷貝至電子書的,里面的內容需要做一些減法,因為它沒有像電子書中的樹形分類數據,這里直接把內容貼一下:
<template><a-layout><a-layout-content:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"><p><a-form layout="inline" :model="param"><a-form-item><a-input v-model:value="param.loginName" placeholder="登陸名"></a-input></a-form-item><a-form-item><a-button type="primary" @click="handleQuery({page: 1, size: pagination.pageSize})">查詢</a-button></a-form-item><a-form-item><a-button type="primary" @click="add()">新增</a-button></a-form-item></a-form></p><a-table:columns="columns":row-key="record => record.id":data-source="users":pagination="pagination":loading="loading"@change="handleTableChange"><template v-slot:action="{ text, record }"><a-space size="small"><a-button type="primary" @click="edit(record)">編輯</a-button><a-popconfirmtitle="刪除后不可恢復,確認刪除?"ok-text="是"cancel-text="否"@confirm="handleDelete(record.id)"><a-button type="danger">刪除</a-button></a-popconfirm></a-space></template></a-table></a-layout-content></a-layout><a-modaltitle="用戶表單"v-model:visible="modalVisible":confirm-loading="modalLoading"@ok="handleModalOk"><a-form :model="user" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }"><a-form-item label="登陸名"><a-input v-model:value="user.loginName" :disabled="!!user.id"/></a-form-item><a-form-item label="昵稱"><a-input v-model:value="user.name"/></a-form-item><a-form-item label="密碼" v-show="!user.id"><a-input v-model:value="user.password"/></a-form-item></a-form></a-modal>
</template><script lang="ts">
import {defineComponent, onMounted, ref} from 'vue';
import axios from 'axios';
import {message} from 'ant-design-vue';
import {Tool} from "@/util/tool";export default defineComponent({name: 'AdminUser',setup() {const param = ref();param.value = {};const users = ref();const pagination = ref({current: 1,pageSize: 10,total: 0});const loading = ref(false);const columns = [{title: '登陸名',dataIndex: 'loginName'},{title: '名稱',dataIndex: 'name'},{title: '密碼',dataIndex: 'password'},{title: 'Action',key: 'action',slots: {customRender: 'action'}}];/*** 數據查詢**/const handleQuery = (params: any) => {loading.value = true;// 如果不清空現有數據,則編輯保存重新加載數據后,再點編輯,則列表顯示的還是編輯前的數據users.value = [];axios.get("/user/list", {params: {page: params.page,size: params.size,loginName: param.value.loginName}}).then((response) => {loading.value = false;const data = response.data;if (data.success) {users.value = data.content.list;// 重置分頁按鈕pagination.value.current = params.page;pagination.value.total = data.content.total;} else {message.error(data.message);}});};/*** 表格點擊頁碼時觸發*/const handleTableChange = (pagination: any) => {console.log("看看自帶的分頁參數都有啥:" + pagination);handleQuery({page: pagination.current,size: pagination.pageSize});};// -------- 表單 ---------const user = ref();const modalVisible = ref(false);const modalLoading = ref(false);const handleModalOk = () => {modalLoading.value = true;axios.post("/user/save", user.value).then((response) => {modalLoading.value = false;const data = response.data; // data = commonRespif (data.success) {modalVisible.value = false;// 重新加載列表handleQuery({page: pagination.value.current,size: pagination.value.pageSize,});} else {message.error(data.message);}});};/*** 編輯*/const edit = (record: any) => {modalVisible.value = true;user.value = Tool.copy(record);};/*** 新增*/const add = () => {modalVisible.value = true;user.value = {};};const handleDelete = (id: number) => {axios.delete("/user/delete/" + id).then((response) => {const data = response.data; // data = commonRespif (data.success) {// 重新加載列表handleQuery({page: pagination.value.current,size: pagination.value.pageSize,});} else {message.error(data.message);}});};onMounted(() => {handleQuery({page: 1,size: pagination.value.pageSize,});});return {param,users,pagination,columns,loading,handleTableChange,handleQuery,edit,add,user,modalVisible,modalLoading,handleModalOk,handleDelete,}}
});
</script><style scoped>
img {width: 50px;height: 50px;
}
</style>
里面的內容都是之前學過了,沒有任何難點,所以不過多解釋。
2、index.ts添加路由信息:
3、頭部增加用戶管理菜單入口:
4、整體運行:
用戶名重復校驗與自定義異常:
概述:
對于用戶名,在后端我們表設計時是設置了它的唯一性,不能重復的:
而目前我們并沒有做重復名稱的校驗,看一下:
界面一直轉圈,此時后端報異常了:
所以接下來咱們做一下重復用戶名的校驗邏輯。
實現:
1、后端插入進行用戶名的校驗:
目前在插入邏輯中并沒有根據用戶名參數到數據庫中查詢是否存在:
咱們先提供一個根據用戶名查詢的方法:
然后插入邏輯就可以修改為:
2、新建業務異常:
接下來這個用戶名已存在的業務異常,則采用自定義異常的方式來處理,先來新建一個自定義的異常類:
package com.cexo.wiki.exception;public enum BusinessExceptionCode {USER_LOGIN_NAME_EXIST("登錄名已存在"),;private String desc;BusinessExceptionCode(String desc) {this.desc = desc;}public String getDesc() {return desc;}public void setDesc(String desc) {this.desc = desc;}
}
異常code是通過枚舉來定義的,接下來這個異常類的內容為:
package com.cexo.wiki.exception;public class BusinessException extends RuntimeException{private BusinessExceptionCode code;public BusinessException (BusinessExceptionCode code) {super(code.getDesc());this.code = code;}public BusinessExceptionCode getCode() {return code;}public void setCode(BusinessExceptionCode code) {this.code = code;}/*** 不寫入堆棧信息,提高性能*/@Overridepublic Throwable fillInStackTrace() {return this;}
}
其中:
它的意思是不拋出一堆的異常堆棧信息了,因為這個屬于業務異常,并非系統異常,通過業務異常的code其實就知道此異常的問題,它是純業務邏輯,并非是因為程序的缺陷,所以這里就重寫一下fillInStackTrace()。
3、拋出異常:
接下來咱們就可以在用戶名重復的處理處這樣來拋出業務異常了:
4、統一異常處理:
還記得在之前https://www.cnblogs.com/webor2006/p/17238571.html使用過SpingBoot的全局異常處理么?對于這個自定義的異常處理也類似,處理如下:
5、運行:
接下來咱們運行看一下效果:
6、編輯用戶名問題:
還有一個細節需要再說明一下,就是編輯用戶時,是不允許編輯用戶名的,目前控制它的地方在這:
也就是當用戶的id不為空,說明是編輯操作,此時登陸名是只讀的不允許編輯,而當用戶的id為空,則說明是新增操作,當然登錄名是可寫入的,另外目前密碼不可以編輯,可以把這個條件暫且先去掉,因為接下來就要對密碼進行進一步處理,目前密碼名文存在庫中肯定是不合理的:
此時編輯的時候就可以編輯密碼了:
這個不是說的重點,重點是它:
為啥要加“!!”兩個嘆號呢?那將它去掉看會有什么影響就知道了:
此時就可以使用“!!”來繞過語法檢查,這個算是一個小技巧。
另外目前貌似前端用戶名已經禁用輸入來防止更改用戶名已經完美了,但是!!!對于前端的東東用戶都可以繞過去的,最典型的是通過瀏覽器的調試來繞過,如下:
所以有必要在后端針對這種從前端繞過去來修改用戶名的情況進行一下處理,那如何處理呢?有一種簡單的改法,就是在后端忽略用戶名的更新既可,也就是不管前端針對用戶名做何等操作,都忽略,具體可以這樣來做:
其實它有另一個方法可以滿足咱們目前的場景:
而它的功能其實跟進去看一下它的sql定義就知道了:
也就是只有有值的情況下才會更新,為空則就不會更新了,那么思路來了,我們在調它之前,主動將loginName給置空不就行了,如下:
好,此時再運行看一下效果:
完美解決。
關于密碼的兩層加密處理:
概述:
接下來咱們來處理密碼加密的問題了,如上面也提到過,目前咱們的用戶名的密碼都是明文存儲的:
這是一個非常危險的事情,假如數據被泄漏了,所有用戶的密碼也就知道了,所以必須得加密存儲,下面來處理下。
密碼加密存儲:
修改也比較簡單,在后端保存的時候,使用springframework的md5進行一下加密既可,如下:
此時運行看一下:
此時看一下庫里的密碼是否已經加密了:
但是!!!現在加密之后在前端編輯時就會有一些問題了,每次編輯,如果不想改密碼只改昵稱,貌似密碼也每次都會變:
關于這個問題之后再來解決,目前先解決保存時加密的問題。?
密碼加密傳輸:
看似目前編輯加密沒問題,其實在前端這塊傳輸還是有問題的,這里看一下:
發現問題了么?前端的密碼其實還是明文的,那當然也得加密嘍,其加密方式也是用MD5,下面來實現一下。
1、拷貝一個md5加密的js:
而此md5的文件地址為:https://blog-static.cnblogs.com/files/webor2006/md5.js?t=1691852396&download=true,其中我們需要調用的函數就是:
2、引入到頁面中:
接下來咱們就可以將這個js引入到用戶管理的頁面中了,其引入方法如下:
然后在用戶管理頁面在保存時對用戶的密碼進行一下加密處理:
加入這么一句:
但是!!!報錯了呀,其中hexMd5就是調用我們新引入的md5.js中的函數,因為它是全局的,哪個頁面都可以調用,而KEY也是定義在md5.js中的:
這個叫“鹽值”,俗稱的“加鹽”,為啥要加一個鹽值呢?關于這塊可以網上搜一下,比如密碼“123”,如果不加鹽值,它的md5是固定的,根據md5可能很容易知道它是"123",但是如果這個密碼加了一串特殊字符再進行md5,此時用戶就很難逆推出來原密了。好,還是回到解決這個報錯問題上來,其實是因為我們頁面上使用的typescript,它默認是無法直接識別javascript的這個方法和變量,需要這樣聲明一下就可以了:
此時咱們再來運行看一下:
這就是密碼的兩層加密,一層是前端的md5,另一層是后端的md5。
增加重置密碼功能:
修改用戶時,不能修改密碼:
對于加了密的密碼,在編輯時是不應該再讓用戶能進行修改的,畢竟加了密的密文再編輯是沒有意義的,所以這里還是在編輯時將密碼表單欄隱藏,但是在新增用戶時是需要顯示的,如下:
與v-show功能類似的還有一個v-if,關于這倆的區別其實在之前的學習中也提到過了,可以參考它:https://www.cnblogs.com/webor2006/p/17510360.html,運行看一下:
同樣的,對于后端也需要做一下處理,避免繞過前端能對密碼進行修改,如下:
單獨開發重置密碼表單和接口:
概述:
對于用戶的密碼有可能會有忘記的情況,此時就應該有一個重置用戶密碼的功能。
1、準備接口:
這里先來準備重置的接口,如下:
其中UserResetPasswordReq新建了一個,因為它里面的入參只需要一個密碼既可,跟用戶保存的入參是不一樣的:
package com.cexo.wiki.req;import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;public class UserResetPasswordReq {private Long id;@NotNull(message = "【密碼】不能為空")// @Length(min = 6, max = 20, message = "【密碼】6~20位")@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,32}$", message = "【密碼】至少包含 數字和英文,長度6-32")private String password;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append(getClass().getSimpleName());sb.append(" [");sb.append("Hash = ").append(hashCode());sb.append(", id=").append(id);sb.append(", password=").append(password);sb.append("]");return sb.toString();}
}
接下來再來實現service層的代碼:
2、準備重置入口:
先在列表操作按鈕上新增一個重置:
此時運行看一下:
其中點擊事件定義如下:
3、實現重置:
接下來則來實現重置的功能。
1、準備重置的模態框:
這個模態框可以copy至編輯時的模態框,只是表單內容不一樣,比較簡單,這里細節就略過了,直接貼相關的代碼:
代碼:
<a-modaltitle="重置密碼"v-model:visible="resetModalVisible":confirm-loading="resetModalLoading"@ok="handleResetModalOk"><a-form :model="user" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }"><a-form-item label="新密碼"><a-input v-model:value="user.password"/></a-form-item></a-form></a-modal>
然后定義相關的變量及函數實現,這塊也沒有任何新的技術點,也直接貼出來了:
代碼:
// -------- 重置密碼 ---------const resetModalVisible = ref(false);const resetModalLoading = ref(false);const handleResetModalOk = () => {resetModalLoading.value = true;user.value.password = hexMd5(user.value.password + KEY);axios.post("/user/reset-password", user.value).then((response) => {resetModalLoading.value = false;const data = response.data;if (data.success) {resetModalVisible.value = false;// 重新加載列表handleQuery({page: pagination.value.current,size: pagination.value.pageSize,});} else {message.error(data.message);}});};
2、重置密碼點擊事件處理:
接下來則來實現密碼重置點擊事件的邏輯,也就是將咱們準備的重置模態框給展示出來:
?
4、運行:
接下來咱們運行看一下效果:
可以看到,我新建了一個賬號,將密碼設置成123之后,跟test2這個用戶密碼也是123最后生成的md5是同一個值:
證明密碼重置之后的密碼是好使的,當然最終得要實現了登錄功能再來進行這塊密碼修改功能是否一切正常,用戶登錄模塊后續做到時再來驗證。
單點登錄token與JWT介紹:
目前為止,咱們已經將用戶的管理功能實現了,那接下來就可以來進行用戶登錄功能的開發了,這里在開發之前,先理論化了解登錄的一些概念,剛好也篇末了,埋個伏筆。
登錄流程:
這里先來了解一下通常登錄的整個流程,總體分為兩大塊:登錄和校驗。
登錄:
1、前端輸入用戶名和密碼。
2、校驗用戶名和密碼。(包含基本的用戶名和密碼的格式校驗、用戶名和密碼是否匹配校驗)。
3、生成token,也稱令牌、登錄標識,其實也就是一串“唯一”的字符串(既使是同一個用戶,登錄多次,每次的token也是不一樣的)。
4、后端保存token(最終會保存到redis中)。
5、前端保存token。
校驗:
1、前端請求時,帶上token,但并非所有的接口都需要校驗,一般就是管理類的接口(增刪改)是需要校驗token的。(通常是token是放在header請求頭里)
2、登錄攔截器,校驗token。(到redis獲取token)?
3、校驗成功則繼續后面的業務。
4、校驗失敗則跳回到登錄界面。
單點登錄系統:【了解】
在上面的登錄流程中可以看到其流程還是很多的, 如果有很多系統都需要來自己實現一遍,那成本比較高,也不好維護,所以可以將它做成一個登錄系統,以后所有產品需要登錄功能都跳到這個系統里,當然做法有兩種:一種是該系統已經帶登錄界面及接口,第二種是各個產品自己維護登錄界面,這套系統只維護登錄相關的接口。而通常這套系統包含如下功能:用戶管理、登錄、登錄校驗、退出登錄,簡單了解一下,不同公司對于它的定義也不一樣。
token與JWT:【了解】
再來了解一下概念,對于token+redis,其實這個token,只要保證它唯一,可以用md5字符串、時間戳等,它的特點就是其值是無意義的,因為它不能代表任何業務意義;但是對于JWT就不一樣了,說到不一樣當然它的token是有意義的嘍,是的,因為它是將用戶的業務信息通過加密手段而生成的token,所以通過token就可以解出來用戶的信息,說了這么多,先度娘一下JWT,在這篇https://blog.csdn.net/weixin_45410366/article/details/125031959文章里是這么說明的:
也一知半解,它其實有一個官網https://jwt.io/,打開了解一下:
哦,原來JWT的全稱是JSON Web Tokens,官網這句描述其實也不知道它是干嘛的,往下翻,官網直接給出了一個在線的效果:
其中加密的算法有很多種,可以根據實際情況來選擇:
也就是這個token信息是有意義的,通過這個token是可以解密的,而要使用它,則需要添加如下依賴:
而核心使用就是這兩個:
這里僅當做個知識了解,待未來真正使用到它時再進一步了解,下篇就正式進入用戶登錄功能的開發,這篇先這樣了。