SpringBoot使用Sa-Token實(shí)現(xiàn)登錄認(rèn)證
一、設(shè)計(jì)思路
對(duì)于一些登錄之后才能訪問的接口(例如:查詢我的賬號(hào)資料),我們通常的做法是增加一層接口校驗(yàn):
- 如果校驗(yàn)通過,則:正常返回?cái)?shù)據(jù)。
- 如果校驗(yàn)未通過,則:拋出異常,告知其需要先進(jìn)行登錄。
那么,判斷會(huì)話是否登錄的依據(jù)是什么?我們先來簡(jiǎn)單分析一下登錄訪問流程:
- 用戶提交 name + password 參數(shù),調(diào)用登錄接口。
- 登錄成功,返回這個(gè)用戶的 Token 會(huì)話憑證。
- 用戶后續(xù)的每次請(qǐng)求,都攜帶上這個(gè) Token。
- 服務(wù)器根據(jù) Token 判斷此會(huì)話是否登錄成功。
所謂登錄認(rèn)證,指的就是服務(wù)器校驗(yàn)賬號(hào)密碼,為用戶頒發(fā) Token 會(huì)話憑證的過程,這個(gè) Token 也是我們后續(xù)判斷會(huì)話是否登錄的關(guān)鍵所在。
動(dòng)態(tài)圖演示:
接下來,我們將介紹在 SpringBoot 中如何使用 Sa-Token 完成登錄認(rèn)證操作。
Sa-Token 是一個(gè) java 權(quán)限認(rèn)證框架,主要解決登錄認(rèn)證、權(quán)限認(rèn)證、單點(diǎn)登錄、OAuth2、微服務(wù)網(wǎng)關(guān)鑒權(quán) 等一系列權(quán)限相關(guān)問題。
Gitee 開源地址:https://gitee.com/dromara/sa-token
首先在項(xiàng)目中引入 Sa-Token 依賴:
<!-- Sa-Token 權(quán)限認(rèn)證 --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.34.0</version> </dependency>
注:如果你使用的是 SpringBoot 3.x,只需要將 sa-token-spring-boot-starter 修改為 sa-token-spring-boot3-starter 即可。
二、登錄與注銷
根據(jù)以上思路,我們需要一個(gè)會(huì)話登錄的函數(shù):
// 會(huì)話登錄:參數(shù)填寫要登錄的賬號(hào)id,建議的數(shù)據(jù)類型:long | int | String, 不可以傳入復(fù)雜類型,如:User、Admin 等等 StpUtil.login(Object id);
只此一句代碼,便可以使會(huì)話登錄成功,實(shí)際上,Sa-Token 在背后做了大量的工作,包括但不限于:
- 檢查此賬號(hào)是否之前已有登錄
- 為賬號(hào)生成 Token 憑證與 Session 會(huì)話
- 通知全局偵聽器,xx 賬號(hào)登錄成功
- 將 Token 注入到請(qǐng)求上下文
- 等等其它工作……
你暫時(shí)不需要完整的了解整個(gè)登錄過程,你只需要記住關(guān)鍵一點(diǎn):Sa-Token 為這個(gè)賬號(hào)創(chuàng)建了一個(gè)Token憑證,且通過 Cookie 上下文返回給了前端。
所以一般情況下,我們的登錄接口代碼,會(huì)大致類似如下:
// 會(huì)話登錄接口 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 第一步:比對(duì)前端提交的賬號(hào)名稱、密碼 if("zhang".equals(name) && "123456".equals(pwd)) { // 第二步:根據(jù)賬號(hào)id,進(jìn)行登錄 StpUtil.login(10001); return SaResult.ok("登錄成功"); } return SaResult.error("登錄失敗"); }
如果你對(duì)以上代碼閱讀沒有壓力,你可能會(huì)注意到略顯奇怪的一點(diǎn):此處僅僅做了會(huì)話登錄,但并沒有主動(dòng)向前端返回 Token 信息。
是因?yàn)椴恍枰獑???yán)格來講是需要的,只不過 StpUtil.login(id) 方法利用了 Cookie 自動(dòng)注入的特性,省略了你手寫返回 Token 的代碼。
如果你對(duì) Cookie 功能還不太了解,也不用擔(dān)心,我們會(huì)在之后的 [ 前后端分離 ] 章節(jié)中詳細(xì)的闡述 Cookie 功能,現(xiàn)在你只需要了解最基本的兩點(diǎn):
- Cookie 可以從后端控制往瀏覽器中寫入 Token 值。
- Cookie 會(huì)在前端每次發(fā)起請(qǐng)求時(shí)自動(dòng)提交 Token 值。
因此,在 Cookie 功能的加持下,我們可以僅靠 StpUtil.login(id) 一句代碼就完成登錄認(rèn)證。
除了登錄方法,我們還需要:
// 當(dāng)前會(huì)話注銷登錄 StpUtil.logout(); // 獲取當(dāng)前會(huì)話是否已經(jīng)登錄,返回true=已登錄,false=未登錄 StpUtil.isLogin(); // 檢驗(yàn)當(dāng)前會(huì)話是否已經(jīng)登錄, 如果未登錄,則拋出異常:`NotLoginException` StpUtil.checkLogin();
異常 NotLoginException 代表當(dāng)前會(huì)話暫未登錄,可能的原因有很多:
前端沒有提交 Token、前端提交的 Token 是無效的、前端提交的 Token 已經(jīng)過期 …… 等等。
Sa-Token 未登錄場(chǎng)景值參照表:
場(chǎng)景值 | 對(duì)應(yīng)常量 | 含義說明 |
---|---|---|
-1 | NotLoginException.NOT_TOKEN | 未能從請(qǐng)求中讀取到 Token |
-2 | NotLoginException.INVALID_TOKEN | 已讀取到 Token,但是 Token無效 |
-3 | NotLoginException.TOKEN_TIMEOUT | 已讀取到 Token,但是 Token已經(jīng)過期 |
-4 | NotLoginException.BE_REPLACED | 已讀取到 Token,但是 Token 已被頂下線 |
-5 | NotLoginException.KICK_OUT | 已讀取到 Token,但是 Token 已被踢下線 |
那么,如何獲取場(chǎng)景值呢?廢話少說直接上代碼:
// 全局異常攔截(攔截項(xiàng)目中的NotLoginException異常) @ExceptionHandler(NotLoginException.class) public SaResult handlerNotLoginException(NotLoginException nle) ?? ??? ?throws Exception { ?? ?// 打印堆棧,以供調(diào)試 ?? ?nle.printStackTrace();? ?? ? ?? ?// 判斷場(chǎng)景值,定制化異常信息? ?? ?String message = ""; ?? ?if(nle.getType().equals(NotLoginException.NOT_TOKEN)) { ?? ??? ?message = "未提供token"; ?? ?} ?? ?else if(nle.getType().equals(NotLoginException.INVALID_TOKEN)) { ?? ??? ?message = "token無效"; ?? ?} ?? ?else if(nle.getType().equals(NotLoginException.TOKEN_TIMEOUT)) { ?? ??? ?message = "token已過期"; ?? ?} ?? ?else if(nle.getType().equals(NotLoginException.BE_REPLACED)) { ?? ??? ?message = "token已被頂下線"; ?? ?} ?? ?else if(nle.getType().equals(NotLoginException.KICK_OUT)) { ?? ??? ?message = "token已被踢下線"; ?? ?} ?? ?else { ?? ??? ?message = "當(dāng)前會(huì)話未登錄"; ?? ?} ?? ? ?? ?// 返回給前端 ?? ?return SaResult.error(message); }
注意:以上代碼并非處理邏輯的最佳方式,只為以最簡(jiǎn)單的代碼演示出場(chǎng)景值的獲取與應(yīng)用,大家可以根據(jù)自己的項(xiàng)目需求來定制化處理
三、會(huì)話查詢
// 獲取當(dāng)前會(huì)話賬號(hào)id, 如果未登錄,則拋出異常:`NotLoginException` StpUtil.getLoginId(); // 類似查詢API還有: StpUtil.getLoginIdAsString(); ? ?// 獲取當(dāng)前會(huì)話賬號(hào)id, 并轉(zhuǎn)化為`String`類型 StpUtil.getLoginIdAsInt(); ? ? ? // 獲取當(dāng)前會(huì)話賬號(hào)id, 并轉(zhuǎn)化為`int`類型 StpUtil.getLoginIdAsLong(); ? ? ?// 獲取當(dāng)前會(huì)話賬號(hào)id, 并轉(zhuǎn)化為`long`類型 // ---------- 指定未登錄情形下返回的默認(rèn)值 ---------- // 獲取當(dāng)前會(huì)話賬號(hào)id, 如果未登錄,則返回null? StpUtil.getLoginIdDefaultNull(); // 獲取當(dāng)前會(huì)話賬號(hào)id, 如果未登錄,則返回默認(rèn)值 (`defaultValue`可以為任意類型) StpUtil.getLoginId(T defaultValue);
四、Token 查詢
// 獲取當(dāng)前會(huì)話的token值 StpUtil.getTokenValue(); // 獲取當(dāng)前`StpLogic`的token名稱 StpUtil.getTokenName(); // 獲取指定token對(duì)應(yīng)的賬號(hào)id,如果未登錄,則返回 null StpUtil.getLoginIdByToken(String tokenValue); // 獲取當(dāng)前會(huì)話剩余有效期(單位:s,返回-1代表永久有效) StpUtil.getTokenTimeout(); // 獲取當(dāng)前會(huì)話的token信息參數(shù) StpUtil.getTokenInfo();
TokenInfo 是 Token 信息 Model,用來描述一個(gè) Token 的常用參數(shù):
{ "tokenName": "satoken", // token名稱 "tokenValue": "e67b99f1-3d7a-4a8d-bb2f-e888a0805633", // token值 "isLogin": true, // 此token是否已經(jīng)登錄 "loginId": "10001", // 此token對(duì)應(yīng)的LoginId,未登錄時(shí)為null "loginType": "login", // 賬號(hào)類型標(biāo)識(shí) "tokenTimeout": 2591977, // token剩余有效期 (單位: 秒) "sessionTimeout": 2591977, // User-Session剩余有效時(shí)間 (單位: 秒) "tokenSessionTimeout": -2, // Token-Session剩余有效時(shí)間 (單位: 秒) (-2表示系統(tǒng)中不存在這個(gè)緩存) "tokenActivityTimeout": -1, // token剩余無操作有效時(shí)間 (單位: 秒) "loginDevice": "default-device" // 登錄設(shè)備類型 }
五、來個(gè)小測(cè)試,加深一下理解
新建 LoginAuthController,復(fù)制以下代碼
package com.pj.cases.use; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.dev33.satoken.stp.SaTokenInfo; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; /** ?* Sa-Token 登錄認(rèn)證示例? ?*? ?* @author kong ?* @since 2022-10-13 ?*/ @RestController @RequestMapping("/acc/") public class LoginAuthController { ?? ?// 會(huì)話登錄接口 ?---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 ?? ?@RequestMapping("doLogin") ?? ?public SaResult doLogin(String name, String pwd) { ?? ??? ? ?? ??? ?// 第一步:比對(duì)前端提交的 賬號(hào)名稱 & 密碼 是否正確,比對(duì)成功后開始登錄? ?? ??? ?// ?? ??? ?此處僅作模擬示例,真實(shí)項(xiàng)目需要從數(shù)據(jù)庫(kù)中查詢數(shù)據(jù)進(jìn)行比對(duì)? ?? ??? ?if("zhang".equals(name) && "123456".equals(pwd)) { ?? ??? ??? ? ?? ??? ??? ?// 第二步:根據(jù)賬號(hào)id,進(jìn)行登錄? ?? ??? ??? ?// ?? ??? ?此處填入的參數(shù)應(yīng)該保持用戶表唯一,比如用戶id,不可以直接填入整個(gè) User 對(duì)象? ?? ??? ??? ?StpUtil.login(10001); ?? ??? ??? ? ?? ??? ??? ?// SaResult 是 Sa-Token 中對(duì)返回結(jié)果的簡(jiǎn)單封裝,下面的示例將不再贅述? ?? ??? ??? ?return SaResult.ok("登錄成功"); ?? ??? ?} ?? ??? ? ?? ??? ?return SaResult.error("登錄失敗"); ?? ?} ?? ?// 查詢當(dāng)前登錄狀態(tài) ?---- http://localhost:8081/acc/isLogin ?? ?@RequestMapping("isLogin") ?? ?public SaResult isLogin() { ?? ??? ?// StpUtil.isLogin() 查詢當(dāng)前客戶端是否登錄,返回 true 或 false? ?? ??? ?boolean isLogin = StpUtil.isLogin(); ?? ??? ?return SaResult.ok("當(dāng)前客戶端是否登錄:" + isLogin); ?? ?} ?? ?// 校驗(yàn)當(dāng)前登錄狀態(tài) ?---- http://localhost:8081/acc/checkLogin ?? ?@RequestMapping("checkLogin") ?? ?public SaResult checkLogin() { ?? ??? ?// 檢驗(yàn)當(dāng)前會(huì)話是否已經(jīng)登錄, 如果未登錄,則拋出異常:`NotLoginException` ?? ??? ?StpUtil.checkLogin(); ?? ??? ?// 拋出異常后,代碼將走入全局異常處理(GlobalException.java),如果沒有拋出異常,則代表通過了登錄校驗(yàn),返回下面信息? ?? ??? ?return SaResult.ok("校驗(yàn)登錄成功,這行字符串是只有登錄后才會(huì)返回的信息"); ?? ?} ?? ?// 獲取當(dāng)前登錄的賬號(hào)是誰(shuí) ?---- http://localhost:8081/acc/getLoginId ?? ?@RequestMapping("getLoginId") ?? ?public SaResult getLoginId() { ?? ??? ?// 需要注意的是,StpUtil.getLoginId() 自帶登錄校驗(yàn)效果 ?? ??? ?// 也就是說如果在未登錄的情況下調(diào)用這句代碼,框架就會(huì)拋出 `NotLoginException` 異常,效果和 StpUtil.checkLogin() 是一樣的? ?? ??? ?Object userId = StpUtil.getLoginId(); ?? ??? ?System.out.println("當(dāng)前登錄的賬號(hào)id是:" + userId); ?? ??? ? ?? ??? ?// 如果不希望 StpUtil.getLoginId() 觸發(fā)登錄校驗(yàn)效果,可以填入一個(gè)默認(rèn)值 ?? ??? ?// 如果會(huì)話未登錄,則返回這個(gè)默認(rèn)值,如果會(huì)話已登錄,將正常返回登錄的賬號(hào)id? ?? ??? ?Object userId2 = StpUtil.getLoginId(0); ?? ??? ?System.out.println("當(dāng)前登錄的賬號(hào)id是:" + userId2); ?? ??? ? ?? ??? ?// 或者使其在未登錄的時(shí)候返回 null? ?? ??? ?Object userId3 = StpUtil.getLoginIdDefaultNull(); ?? ??? ?System.out.println("當(dāng)前登錄的賬號(hào)id是:" + userId3); ?? ??? ? ?? ??? ?// 類型轉(zhuǎn)換: ?? ??? ?// StpUtil.getLoginId() 返回的是 Object 類型,你可以使用以下方法指定其返回的類型? ?? ??? ?int userId4 = StpUtil.getLoginIdAsInt(); ?// 將返回值轉(zhuǎn)換為 int 類型? ?? ??? ?long userId5 = StpUtil.getLoginIdAsLong(); ?// 將返回值轉(zhuǎn)換為 long 類型? ?? ??? ?String userId6 = StpUtil.getLoginIdAsString(); ?// 將返回值轉(zhuǎn)換為 String 類型? ?? ??? ? ?? ??? ?// 疑問:數(shù)據(jù)基本類型不是有八個(gè)嗎,為什么只封裝以上三種類型的轉(zhuǎn)換? ?? ??? ?// 因?yàn)榇蠖鄶?shù)項(xiàng)目都是拿 int、long 或 String 聲明 UserId 的類型的,實(shí)在沒見過哪個(gè)項(xiàng)目用 double、float、boolean 之類來聲明 UserId? ?? ??? ?System.out.println("當(dāng)前登錄的賬號(hào)id是:" + userId4 + " --- " + userId5 + " --- " + userId6); ?? ??? ? ?? ??? ?// 返回給前端? ?? ??? ?return SaResult.ok("當(dāng)前客戶端登錄的賬號(hào)id是:" + userId); ?? ?} ?? ?// 查詢 Token 信息 ?---- http://localhost:8081/acc/tokenInfo ?? ?@RequestMapping("tokenInfo") ?? ?public SaResult tokenInfo() { ?? ??? ?// TokenName 是 Token 名稱的意思,此值也決定了前端提交 Token 時(shí)應(yīng)該使用的參數(shù)名稱? ?? ??? ?String tokenName = StpUtil.getTokenName(); ?? ??? ?System.out.println("前端提交 Token 時(shí)應(yīng)該使用的參數(shù)名稱:" + tokenName); ?? ??? ? ?? ??? ?// 使用 StpUtil.getTokenValue() 獲取前端提交的 Token 值? ?? ??? ?// 框架默認(rèn)前端可以從以下三個(gè)途徑中提交 Token: ?? ??? ?// ?? ??? ?Cookie ?? ??? ?(瀏覽器自動(dòng)提交) ?? ??? ?// ?? ??? ?Header頭?? ?(代碼手動(dòng)提交) ?? ??? ?// ?? ??? ?Query 參數(shù)?? ?(代碼手動(dòng)提交) 例如: /user/getInfo?satoken=xxxx-xxxx-xxxx-xxxx? ?? ??? ?// 讀取順序?yàn)椋?Query 參數(shù) --> Header頭 -- > Cookie? ?? ??? ?// 以上三個(gè)地方都讀取不到 Token 信息的話,則視為前端沒有提交 Token? ?? ??? ?String tokenValue = StpUtil.getTokenValue(); ?? ??? ?System.out.println("前端提交的Token值為:" + tokenValue); ?? ??? ? ?? ??? ?// TokenInfo 包含了此 Token 的大多數(shù)信息? ?? ??? ?SaTokenInfo info = StpUtil.getTokenInfo(); ?? ??? ?System.out.println("Token 名稱:" + info.getTokenName()); ?? ??? ?System.out.println("Token 值:" + info.getTokenValue()); ?? ??? ?System.out.println("當(dāng)前是否登錄:" + info.getIsLogin()); ?? ??? ?System.out.println("當(dāng)前登錄的賬號(hào)id:" + info.getLoginId()); ?? ??? ?System.out.println("當(dāng)前登錄賬號(hào)的類型:" + info.getLoginType()); ?? ??? ?System.out.println("當(dāng)前登錄客戶端的設(shè)備類型:" + info.getLoginDevice()); ?? ??? ?System.out.println("當(dāng)前 Token 的剩余有效期:" + info.getTokenTimeout()); // 單位:秒,-1代表永久有效,-2代表值不存在 ?? ??? ?System.out.println("當(dāng)前 Token 的剩余臨時(shí)有效期:" + info.getTokenActivityTimeout()); // 單位:秒,-1代表永久有效,-2代表值不存在 ?? ??? ?System.out.println("當(dāng)前 User-Session 的剩余有效期" + info.getSessionTimeout()); // 單位:秒,-1代表永久有效,-2代表值不存在 ?? ??? ?System.out.println("當(dāng)前 Token-Session 的剩余有效期" + info.getTokenSessionTimeout()); // 單位:秒,-1代表永久有效,-2代表值不存在 ?? ??? ? ?? ??? ?// 返回給前端? ?? ??? ?return SaResult.data(StpUtil.getTokenInfo()); ?? ?} ?? ? ?? ?// 會(huì)話注銷 ?---- http://localhost:8081/acc/logout ?? ?@RequestMapping("logout") ?? ?public SaResult logout() { ?? ??? ?// 退出登錄會(huì)清除三個(gè)地方的數(shù)據(jù): ?? ??? ?// ?? ??? ?1、Redis中保存的 Token 信息 ?? ??? ?// ?? ??? ?2、當(dāng)前請(qǐng)求上下文中保存的 Token 信息? ?? ??? ?// ?? ??? ?3、Cookie 中保存的 Token 信息(如果未使用Cookie模式則不會(huì)清除) ?? ??? ?StpUtil.logout(); ?? ??? ? ?? ??? ?// StpUtil.logout() 在未登錄時(shí)也是可以調(diào)用成功的, ?? ??? ?// 也就是說,無論客戶端有沒有登錄,執(zhí)行完 StpUtil.logout() 后,都會(huì)處于未登錄狀態(tài)? ?? ??? ?System.out.println("當(dāng)前是否處于登錄狀態(tài):" + StpUtil.isLogin()); ?? ??? ? ?? ??? ?// 返回給前端? ?? ??? ?return SaResult.ok("退出登錄成功"); ?? ?} ?? ? }
代碼注釋已針對(duì)每一步操作做出詳細(xì)解釋,大家可根據(jù)可參照注釋中的訪問鏈接進(jìn)行逐步測(cè)試。
本示例代碼已上傳至 Gitee,可參考:
參考資料
Gitee 倉(cāng)庫(kù)地址:https://gitee.com/dromara/sa-token
GitHub 倉(cāng)庫(kù)地址:https://github.com/dromara/sa-token
Sa-Token 在線文檔:https://sa-token.dev33.cn/
到此這篇關(guān)于SpringBoot使用Sa-Token實(shí)現(xiàn)登錄認(rèn)證的文章就介紹到這了,更多相關(guān)SpringBoot Sa-Token登錄認(rèn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 基于SpringBoot整合oauth2實(shí)現(xiàn)token認(rèn)證
- springboot+jwt實(shí)現(xiàn)token登陸權(quán)限認(rèn)證的實(shí)現(xiàn)
- SpringBoot和Redis實(shí)現(xiàn)Token權(quán)限認(rèn)證的實(shí)例講解
- SpringBoot整合Sa-Token實(shí)現(xiàn)登錄認(rèn)證的示例代碼
- SpringBoot整合token實(shí)現(xiàn)登錄認(rèn)證的示例代碼
- SpringBoot使用Sa-Token實(shí)現(xiàn)權(quán)限認(rèn)證
- 在SpringBoot中使用jwt實(shí)現(xiàn)token身份認(rèn)證的實(shí)例代碼
- Springboot微服務(wù)分布式框架Rouyi Cloud權(quán)限認(rèn)證(登錄流程之token解析)
- Springboot 如何使用 SaToken 進(jìn)行登錄認(rèn)證、權(quán)限管理及路由規(guī)則接口攔截
- springBoot整合jwt實(shí)現(xiàn)token令牌認(rèn)證的示例代碼
相關(guān)文章
你應(yīng)該知道的這些Mybatis-Plus使用技巧(小結(jié))
這篇文章主要介紹了你應(yīng)該知道的這些Mybatis-Plus使用技巧(小結(jié)),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08通過MyBatis讀取數(shù)據(jù)庫(kù)數(shù)據(jù)并提供rest接口訪問
這篇文章主要介紹了通過MyBatis讀取數(shù)據(jù)庫(kù)數(shù)據(jù)并提供rest接口訪問 的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-08-08IDEA2022創(chuàng)建Maven Web項(xiàng)目教程(圖文)
本文主要介紹了IDEA2022創(chuàng)建Maven Web項(xiàng)目教程,文中通過圖文介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07Java獲取當(dāng)?shù)氐娜粘鋈章鋾r(shí)間代碼分享
這篇文章主要介紹了Java獲取當(dāng)?shù)氐娜粘鋈章鋾r(shí)間代碼分享,國(guó)外猿友寫的一個(gè)類,需要的朋友可以參考下2014-06-06java設(shè)計(jì)模式理解依賴于抽象不依賴具體的分析
這篇文章主要為大家介紹了java設(shè)計(jì)模式的規(guī)則,理解依賴于抽象不依賴具體的示例分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-10-10java通過控制鼠標(biāo)實(shí)現(xiàn)屏幕廣播的方法
這篇文章主要介紹了java通過控制鼠標(biāo)實(shí)現(xiàn)屏幕廣播的方法,針對(duì)前面一篇Java屏幕共享功能進(jìn)行了改進(jìn),實(shí)現(xiàn)了鼠標(biāo)控制功能,具有一定的實(shí)用價(jià)值,需要的朋友可以參考下2014-12-12