From c0f781a9fbbdd31d64564f6afd7a3a209d82e103 Mon Sep 17 00:00:00 2001
From: click33 <2393584716@qq.com>
Date: Wed, 1 Apr 2026 05:25:48 +0800
Subject: [PATCH] =?UTF-8?q?feat(sso):=20=E8=A1=A5=E5=85=A8=E6=9C=80?=
=?UTF-8?q?=E6=96=B0=E7=89=88=20SSO=20NoSdk=20=E6=A8=A1=E5=BC=8F=E5=AE=9E?=
=?UTF-8?q?=E7=8E=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/main/resources/application.yml | 12 +
.../com/pj/SaSsoClientNoSdkApplication.java | 3 -
.../java/com/pj/sso/SsoClientController.java | 236 +++++++++---------
.../main/java/com/pj/sso/SsoRequestUtil.java | 115 ++-------
.../src/main/java/com/pj/sso/SsoSignUtil.java | 102 ++++++++
sa-token-doc/sso/sso-nosdk.md | 20 +-
6 files changed, 270 insertions(+), 218 deletions(-)
create mode 100644 sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoSignUtil.java
diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/application.yml b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/application.yml
index cbae7ecb..5c5a1512 100644
--- a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/application.yml
+++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso-server/src/main/resources/application.yml
@@ -47,6 +47,18 @@ sa-token:
push-url: http://sa-sso-client1.com:9003/sso/pushC
# 接口调用秘钥 (如果不配置则使用全局默认秘钥)
secret-key: SSO-C3-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
+ # 应用 sso-client3-nosdk:采用 NoSdk 模式对接 (不依赖 Sa-Token 客户端 SDK,手动实现协议)
+ sso-client3-nosdk:
+ # 应用名称
+ client: sso-client3-nosdk
+ # 允许授权地址
+ allow-url: "*"
+ # 是否接收消息推送
+ is-push: true
+ # 消息推送地址
+ push-url: http://sa-sso-client1.com:9004/sso/pushC
+ # 接口调用秘钥 (如果不配置则使用全局默认秘钥)
+ secret-key: SSO-C3-NoSdk-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor
# 应用 sso-client3-resdk:采用 ReSdk 模式对接
sso-client3-resdk:
# 应用名称
diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/SaSsoClientNoSdkApplication.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/SaSsoClientNoSdkApplication.java
index ed3dd2ec..a5d67798 100644
--- a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/SaSsoClientNoSdkApplication.java
+++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/SaSsoClientNoSdkApplication.java
@@ -16,9 +16,6 @@ public class SaSsoClientNoSdkApplication {
System.out.println("测试访问应用端三: http://sa-sso-client3.com:9004");
System.out.println("测试前需要根据官网文档修改hosts文件,测试账号密码:sa / 123456");
System.out.println();
-
- System.err.println("自 v1.43.0 版本起,Sa-Token SSO 不再维护 NoSdk 示例,此项目仅做留档");
- System.err.println("如您需要非 Sa-Token 技术栈项目接入 SSO-Server 认证中心,请参考 ReSdk 版本示例");
}
}
\ No newline at end of file
diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoClientController.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoClientController.java
index 15098acc..f9f329a5 100644
--- a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoClientController.java
+++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoClientController.java
@@ -1,22 +1,22 @@
package com.pj.sso;
-import java.io.IOException;
-import java.util.Objects;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-
+import com.pj.sso.util.AjaxJson;
+import com.pj.sso.util.MyHttpSessionHolder;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
-import com.pj.sso.util.AjaxJson;
-import com.pj.sso.util.MyHttpSessionHolder;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
/**
- * SSO Client端 Controller
+ * SSO Client端 Controller
* @author click33
*/
@RestController
@@ -25,162 +25,166 @@ public class SsoClientController {
// SSO-Client端:首页
@RequestMapping("/")
public String index(HttpSession session) {
- String str = "
Sa-Token SSO-Client 应用端
" +
- "当前会话登录账号:" + session.getAttribute("userId") + "
" +
- "登录" +
- " 注销" +
+ Object userId = session.getAttribute("userId");
+ String str = "
Sa-Token SSO-Client 应用端 (模式三-NoSdk)
" +
+ "当前会话是否登录:" + (userId != null) + " (" + userId + ")
" +
+ "登录" +
+ " 注销" +
" 获取资料
";
return str;
}
- // SSO-Client端:单点登录地址
+ // SSO-Client端:单点登录地址
@RequestMapping("/sso/login")
- public Object ssoLogin(String ticket, @RequestParam(defaultValue = "/") String back,
+ public Object ssoLogin(String ticket, @RequestParam(defaultValue = "/") String back,
HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
-
- // 如果已经登录,则直接返回
- if(session.getAttribute("userId") != null) {
+
+ // 如果已经登录,则直接返回
+ if (session.getAttribute("userId") != null) {
response.sendRedirect(back);
return null;
}
-
+
/*
- * 此时有两种情况:
- * 情况1:ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心
- * 情况2:ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录
+ * 此时有两种情况:
+ * 情况1:ticket无值,说明此请求是Client端访问,需要重定向至SSO认证中心
+ * 情况2:ticket有值,说明此请求从SSO认证中心重定向而来,需要根据ticket进行登录
*/
- if(ticket == null) {
- String currUrl = request.getRequestURL().toString();
- String clientLoginUrl = currUrl + "?back=" + SsoRequestUtil.encodeUrl(back);
- String serverAuthUrl = SsoRequestUtil.authUrl + "?redirect=" + clientLoginUrl;
+ if (ticket == null) {
+ // ------- 情况 1
+ // 当前 url,形如:http://sso-client.com/sso/login?back=xxx
+ String clientLoginUrl = request.getRequestURL().toString() + "?back=" + SsoRequestUtil.encodeUrl(back);
+ // 最终授权地址,形如:http://sso-server.com/sso/auth?client=xxx&redirect=http://sso-client.com/sso/login?back=xxx
+ String serverAuthUrl = SsoRequestUtil.authUrl
+ + "?client=" + SsoRequestUtil.clientId
+ + "&redirect=" + clientLoginUrl;
response.sendRedirect(serverAuthUrl);
return null;
+
} else {
- // 获取当前 client 端的单点注销回调地址
- String ssoLogoutCall = "";
- if(SsoRequestUtil.isSlo) {
- ssoLogoutCall = request.getRequestURL().toString().replace("/sso/login", "/sso/logoutCall");
- }
-
- // 校验 ticket
- String timestamp = String.valueOf(System.currentTimeMillis()); // 时间戳
- String nonce = SsoRequestUtil.getRandomString(20); // 随机字符串
- String sign = SsoRequestUtil.getSignByTicket(ticket, ssoLogoutCall, timestamp, nonce); // 参数签名
- String checkUrl = SsoRequestUtil.checkTicketUrl +
- "?timestamp=" + timestamp +
- "&nonce=" + nonce +
- "&sign=" + sign +
- "&ticket=" + ticket +
- "&ssoLogoutCall=" + ssoLogoutCall;
- AjaxJson result = SsoRequestUtil.request(checkUrl);
-
- // 200 代表校验成功
- if(result.getCode() == 200 && SsoRequestUtil.isEmpty(result.getData()) == false) {
- // 登录上
- Object loginId = result.getData();
- session.setAttribute("userId", loginId);
+ // ------- 情况 2
+ // 构建 checkTicket 请求参数,以 ticket 查询 userId
+ Map params = new LinkedHashMap<>();
+ params.put("msgType", "checkTicket");
+ params.put("client", SsoRequestUtil.clientId);
+ params.put("ticket", ticket);
+ SsoSignUtil.addSignParams(params);
+ String pushUrl = SsoRequestUtil.buildUrl(SsoRequestUtil.pushSUrl, params);
+ AjaxJson result = SsoRequestUtil.request(pushUrl);
+
+ // 200 代表校验成功
+ if (result.getCode() == 200 && !SsoRequestUtil.isEmpty(result.getData())) {
+ // 登录上
+ session.setAttribute("userId", result.getData());
// 返回 back 地址
response.sendRedirect(back);
return null;
-
} else {
- // 将 sso-server 回应的消息作为异常抛出
+ // 将 sso-server 回应的消息作为异常抛出
throw new RuntimeException(result.getMsg());
}
}
}
-
+
// SSO-Client端:单点注销地址
@RequestMapping("/sso/logout")
- public Object ssoLogout(@RequestParam(defaultValue = "/") String back,
+ public Object ssoLogout(@RequestParam(defaultValue = "/") String back,
HttpServletResponse response, HttpSession session) throws IOException {
-
- // 如果未登录,则无需注销
- if(session.getAttribute("userId") == null) {
+
+ // 如果未登录,则无需注销
+ if (session.getAttribute("userId") == null) {
response.sendRedirect(back);
return null;
- }
-
- // 调用 sso-server 认证中心单点注销API
- Object loginId = session.getAttribute("userId"); // 账号id
- String timestamp = String.valueOf(System.currentTimeMillis()); // 时间戳
- String nonce = SsoRequestUtil.getRandomString(20); // 随机字符串
- String sign = SsoRequestUtil.getSign(loginId, timestamp, nonce); // 参数签名
-
- String url = SsoRequestUtil.sloUrl +
- "?loginId=" + loginId +
- "×tamp=" + timestamp +
- "&nonce=" + nonce +
- "&sign=" + sign;
- AjaxJson result = SsoRequestUtil.request(url);
-
- // 校验响应状态码,200 代表成功
- if(result.getCode() == 200) {
-
- // 极端场景下,sso-server 中心的单点注销可能并不会通知到此 client 端,所以这里需要再补一刀
+ }
+
+ // 调用 sso-server 认证中心单点注销 API
+ Object loginId = session.getAttribute("userId");
+ Map params = new LinkedHashMap<>();
+ params.put("msgType", "signout");
+ params.put("client", SsoRequestUtil.clientId);
+ params.put("loginId", String.valueOf(loginId));
+ SsoSignUtil.addSignParams(params);
+ String pushUrl = SsoRequestUtil.buildUrl(SsoRequestUtil.pushSUrl, params);
+ AjaxJson result = SsoRequestUtil.request(pushUrl);
+
+ // 校验响应状态码,200 代表成功
+ if (result.getCode() == 200) {
+ // 极端场景下,sso-server 中心的单点注销可能并不会通知到此 client 端,所以这里需要再补一刀
session.removeAttribute("userId");
// 返回 back 地址
response.sendRedirect(back);
return null;
-
} else {
- // 将 sso-server 回应的消息作为异常抛出
+ // 将 sso-server 回应的消息作为异常抛出
throw new RuntimeException(result.getMsg());
}
}
-
- // SSO-Client端:单点注销回调地址
- @RequestMapping("/sso/logoutCall")
- public Object ssoLogoutCall(String loginId, String autoLogout, String timestamp, String nonce, String sign) {
-
- // 校验签名
- String calcSign = SsoRequestUtil.getSignByLogoutCall(loginId, autoLogout, timestamp, nonce);
- if(calcSign.equals(sign) == false) {
- System.out.println("无效签名,拒绝应答:" + sign);
- return AjaxJson.getError("无效签名,拒绝应答" + sign);
+
+ // SSO-Server 端消息推送接收地址(单点注销回调等)
+ @RequestMapping("/sso/pushC")
+ public Object ssoPushC(HttpServletRequest request) {
+
+ // 将请求参数收集为 Map
+ Map params = new LinkedHashMap<>();
+ for (Map.Entry entry : request.getParameterMap().entrySet()) {
+ params.put(entry.getKey(), entry.getValue()[0]);
}
-
- // 注销这个账号id
- for (HttpSession session: MyHttpSessionHolder.sessionList) {
- Object userId = session.getAttribute("userId");
- if(Objects.equals(String.valueOf(userId), loginId)) {
- session.removeAttribute("userId");
+
+ // 校验签名
+ if (!SsoSignUtil.verifySign(params)) {
+ return AjaxJson.getError("无效签名,拒绝应答");
+ }
+
+ // 按 msgType 分发处理
+ String msgType = params.get("msgType");
+
+ // 单点注销回调
+ if ("logoutCall".equals(msgType)) {
+ // 注销这个账号 id 在本 client 端的所有会话
+ String loginId = params.get("loginId");
+ for (HttpSession session : MyHttpSessionHolder.sessionList) {
+ Object userId = session.getAttribute("userId");
+ if (Objects.equals(String.valueOf(userId), loginId)) {
+ session.removeAttribute("userId");
+ }
}
+ return AjaxJson.getSuccess("账号id=" + loginId + " 注销成功");
}
-
- return AjaxJson.getSuccess("账号id=" + loginId + " 注销成功");
+
+ // 其它消息类型
+// if("xxx".equals(msgType)) {
+// // 处理 xxx 消息
+// }
+
+ return AjaxJson.getError("未知消息类型:" + msgType);
}
- // 查询我的账号信息 (调用此接口的前提是 sso-server 端开放了 /sso/userinfo 路由)
+ // 查询我的账号信息(调用 sso-server 端 userinfo 消息处理器)
@RequestMapping("/sso/myInfo")
public Object myInfo(HttpSession session) {
- // 如果尚未登录
- if(session.getAttribute("userId") == null) {
+ // 如果尚未登录
+ if (session.getAttribute("userId") == null) {
return "尚未登录,无法获取";
}
- // 组织 url 参数
- Object loginId = session.getAttribute("userId"); // 账号id
- String timestamp = String.valueOf(System.currentTimeMillis()); // 时间戳
- String nonce = SsoRequestUtil.getRandomString(20); // 随机字符串
- String sign = SsoRequestUtil.getSign(loginId, timestamp, nonce); // 参数签名
-
- String url = SsoRequestUtil.getDataUrl +
- "?loginId=" + loginId +
- "×tamp=" + timestamp +
- "&nonce=" + nonce +
- "&sign=" + sign;
- AjaxJson result = SsoRequestUtil.request(url);
-
- // 返回给前端
+ Object loginId = session.getAttribute("userId");
+ Map params = new LinkedHashMap<>();
+ params.put("msgType", "userinfo");
+ params.put("client", SsoRequestUtil.clientId);
+ params.put("loginId", String.valueOf(loginId));
+ SsoSignUtil.addSignParams(params);
+ String pushUrl = SsoRequestUtil.buildUrl(SsoRequestUtil.pushSUrl, params);
+ AjaxJson result = SsoRequestUtil.request(pushUrl);
+
+ // 返回给前端
return result;
}
- // 全局异常拦截
+ // 全局异常拦截
@ExceptionHandler
public AjaxJson handlerException(Exception e) {
- e.printStackTrace();
+ e.printStackTrace();
return AjaxJson.getError(e.getMessage());
}
-
+
}
diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoRequestUtil.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoRequestUtil.java
index 0fc8c3ee..3a7a22ae 100644
--- a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoRequestUtil.java
+++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoRequestUtil.java
@@ -5,61 +5,47 @@ import com.pj.sso.util.AjaxJson;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
-import java.security.MessageDigest;
import java.util.Map;
-import java.util.Random;
/**
- * 封装一些 sso 共用方法
- *
+ * 封装一些 sso 共用方法
+ *
* @author click33
* @since 2022-4-30
*/
public class SsoRequestUtil {
/**
- * SSO-Server端主机地址
+ * SSO-Server 端主机地址
*/
public static String serverUrl = "http://sa-sso-server.com:9000";
/**
- * SSO-Server端 统一认证地址
+ * SSO-Server 端统一认证地址
*/
public static String authUrl = serverUrl + "/sso/auth";
/**
- * SSO-Server端 ticket校验地址
+ * SSO-Server 端统一消息推送地址(ticket校验、单点注销、获取用户信息等均通过此入口)
*/
- public static String checkTicketUrl = serverUrl + "/sso/checkTicket";
+ public static String pushSUrl = serverUrl + "/sso/pushS";
/**
- * 单点注销地址
+ * 当前应用的客户端标识(需与 sso-server 端 clients 配置一致)
*/
- public static String sloUrl = serverUrl + "/sso/signout";
+ public static String clientId = "sso-client3-nosdk";
/**
- * SSO-Server端 查询userinfo地址
+ * 接口调用秘钥(需与 sso-server 端对应 client 配置一致)
*/
- public static String getDataUrl = serverUrl + "/sso/getData";
+ public static String secretKey = "SSO-C3-NoSdk-kQwIOrYvnXmSDkwEiFngrKidMcdrgKor";
+
+ // -------------------------- 工具方法
/**
- * 打开单点注销功能
- */
- public static boolean isSlo = true;
-
- /**
- * 接口调用秘钥
- */
- public static String secretKey = "kQwIOrYvnXmSDkwEiFngrKidMcdrgKor";
-
-
-
- // -------------------------- 工具方法
-
- /**
- * 发出请求,并返回 SaResult 结果
- * @param url 请求地址
- * @return 返回的结果
+ * 发出请求,并返回 AjaxJson 结果
+ * @param url 请求地址(含查询参数)
+ * @return 返回的结果
*/
public static AjaxJson request(String url) {
Map map = Forest.post(url).executeAsMap();
@@ -67,75 +53,28 @@ public class SsoRequestUtil {
}
/**
- * 根据参数计算签名
- * @param loginId 账号id
- * @param timestamp 当前时间戳,13位
- * @param nonce 随机字符串
- * @return 签名
+ * 将参数 Map 拼接到 baseUrl 后面(值进行 URL 编码),返回完整 URL
+ * @param baseUrl 基础 URL
+ * @param params 请求参数
+ * @return 拼接后的完整 URL
*/
- public static String getSign(Object loginId, String timestamp, String nonce) {
- return md5("loginId=" + loginId + "&nonce=" + nonce + "×tamp=" + timestamp + "&key=" + secretKey);
- }
- // 单点注销回调时构建签名
- public static String getSignByLogoutCall(Object loginId, String autoLogout, String timestamp, String nonce) {
- return md5("autoLogout=" + autoLogout + "&loginId=" + loginId + "&nonce=" + nonce + "×tamp=" + timestamp + "&key=" + secretKey);
- }
- // 校验ticket 时构建签名
- public static String getSignByTicket(String ticket, String ssoLogoutCall, String timestamp, String nonce) {
- return md5("nonce=" + nonce + "&ssoLogoutCall=" + ssoLogoutCall + "&ticket=" + ticket + "×tamp=" + timestamp + "&key=" + secretKey);
+ public static String buildUrl(String baseUrl, Map params) {
+ StringBuilder sb = new StringBuilder(baseUrl).append("?");
+ for (Map.Entry entry : params.entrySet()) {
+ sb.append(entry.getKey()).append("=").append(encodeUrl(entry.getValue())).append("&");
+ }
+ sb.deleteCharAt(sb.length() - 1);
+ return sb.toString();
}
/**
* 指定元素是否为null或者空字符串
- * @param str 指定元素
+ * @param str 指定元素
* @return 是否为null或者空字符串
*/
public static boolean isEmpty(Object str) {
return str == null || "".equals(str);
}
-
- /**
- * md5加密
- * @param str 指定字符串
- * @return 加密后的字符串
- */
- public static String md5(String str) {
- str = (str == null ? "" : str);
- char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
- try {
- byte[] btInput = str.getBytes();
- MessageDigest mdInst = MessageDigest.getInstance("MD5");
- mdInst.update(btInput);
- byte[] md = mdInst.digest();
- int j = md.length;
- char[] strA = new char[j * 2];
- int k = 0;
- for (byte byte0 : md) {
- strA[k++] = hexDigits[byte0 >>> 4 & 0xf];
- strA[k++] = hexDigits[byte0 & 0xf];
- }
- return new String(strA);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * 生成指定长度的随机字符串
- *
- * @param length 字符串的长度
- * @return 一个随机字符串
- */
- public static String getRandomString(int length) {
- String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
- Random random = new Random();
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < length; i++) {
- int number = random.nextInt(62);
- sb.append(str.charAt(number));
- }
- return sb.toString();
- }
/**
* URL编码
diff --git a/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoSignUtil.java b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoSignUtil.java
new file mode 100644
index 00000000..188b0807
--- /dev/null
+++ b/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk/src/main/java/com/pj/sso/SsoSignUtil.java
@@ -0,0 +1,102 @@
+package com.pj.sso;
+
+import java.security.MessageDigest;
+import java.util.Map;
+import java.util.Random;
+import java.util.TreeMap;
+
+/**
+ * SSO 签名工具类:签名生成、注入与校验
+ *
+ * @author click33
+ */
+public class SsoSignUtil {
+
+
+
+ // -------------------------- 签名方法
+
+ /**
+ * 计算签名:将 params(排除 sign 字段)按 key 字典序升序排列,
+ * 拼接为 k=v&k=v 后追加 &key={secretKey},整体 MD5
+ * @param params 请求参数(不含 sign)
+ * @return 签名值
+ */
+ public static String computeSign(Map params) {
+ TreeMap sorted = new TreeMap<>(params);
+ sorted.remove("sign");
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry entry : sorted.entrySet()) {
+ if (sb.length() > 0) sb.append("&");
+ sb.append(entry.getKey()).append("=").append(entry.getValue());
+ }
+ sb.append("&key=").append(SsoRequestUtil.secretKey);
+ return md5(sb.toString());
+ }
+
+ /**
+ * 向参数 Map 中注入 timestamp、nonce、sign 三个签名参数
+ * @param params 请求参数(已填好业务参数,此方法自动追加签名参数)
+ */
+ public static void addSignParams(Map params) {
+ params.put("timestamp", String.valueOf(System.currentTimeMillis()));
+ params.put("nonce", getRandomString(20));
+ params.put("sign", computeSign(params));
+ }
+
+ /**
+ * 校验请求中的 sign 参数是否合法
+ * @param params 包含 sign 的请求参数
+ * @return 签名是否合法
+ */
+ public static boolean verifySign(Map params) {
+ String sign = params.get("sign");
+ if (sign == null) return false;
+ return sign.equals(computeSign(params));
+ }
+
+
+ // -------------------------- 基础工具
+
+ /**
+ * MD5 加密
+ * @param str 指定字符串
+ * @return 加密后的字符串
+ */
+ public static String md5(String str) {
+ str = (str == null ? "" : str);
+ char[] hexDigits = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+ try {
+ byte[] btInput = str.getBytes();
+ MessageDigest mdInst = MessageDigest.getInstance("MD5");
+ mdInst.update(btInput);
+ byte[] md = mdInst.digest();
+ int j = md.length;
+ char[] strA = new char[j * 2];
+ int k = 0;
+ for (byte byte0 : md) {
+ strA[k++] = hexDigits[byte0 >>> 4 & 0xf];
+ strA[k++] = hexDigits[byte0 & 0xf];
+ }
+ return new String(strA);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 生成指定长度的随机字符串
+ * @param length 字符串的长度
+ * @return 一个随机字符串
+ */
+ public static String getRandomString(int length) {
+ String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ Random random = new Random();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ sb.append(str.charAt(random.nextInt(62)));
+ }
+ return sb.toString();
+ }
+
+}
diff --git a/sa-token-doc/sso/sso-nosdk.md b/sa-token-doc/sso/sso-nosdk.md
index 196c0a85..89d703c8 100644
--- a/sa-token-doc/sso/sso-nosdk.md
+++ b/sa-token-doc/sso/sso-nosdk.md
@@ -12,14 +12,13 @@ NoSdk 模式(不使用SDK):通过 http 工具类调用接口的方式来
参考 demo:[sa-token-demo-sso3-client-nosdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-nosdk)
-该 demo 假设应用端没有使用任何“权限认证框架”,使用最基础的 ServletAPI 进行会话管理,模拟了 `/sso/login`、 `/sso/logout`、 `/sso/logoutCall` 三个接口的处理逻辑。
+该 demo 假设应用端没有使用任何“权限认证框架”,使用最基础的 ServletAPI 进行会话管理,模拟了 `/sso/login`、 `/sso/logout`、 `/sso/pushC` 三个接口的处理逻辑。
+
+> [!INFO| label:NoSdk 模式优缺点]
+> - 1、支持客户端使用任意技术栈。
+> - 2、代码简单易懂,流程直观清晰。
+> - 3、用 http 工具类模拟 Sa-Token SSO 内部实现,样版代码较多,略显冗余。
-> [!WARNING| label:NoSdk 示例不再主维护]
-> 基于以下原因:
-> - 1、NoSdk demo 相当于通过 http 工具类再次重写了一遍 Sa-Token SSO 模块代码,繁琐且冗余。
-> - 2、重写的代码无法拥有 Sa-Token SSO 模块全部能力,仅能完成基本对接,算是一个简化版 SDK。
->
-> 自 v1.43.0 版本起,不再主维护 NoSdk 模式,仓库示例仅做留档参考,大家可以转为 ReSdk 模式。
### ReSdk 模式
@@ -28,11 +27,10 @@ ReSdk 模式(重写SDK部分方法):通过重写框架关键步骤点,
参考 demo:[sa-token-demo-sso3-client-resdk](https://gitee.com/dromara/sa-token/tree/master/sa-token-demo/sa-token-demo-sso/sa-token-demo-sso3-client-resdk)
-> [!INFO| label:ReSdk 模式优点]
-> - 1、依然支持客户端使用任意技术栈。
+> [!INFO| label:ReSdk 模式优缺点]
+> - 1、支持客户端使用任意技术栈。
> - 2、仅重写少量部分关键代码,即可完成对接。几乎可以得到 Sa-Token SSO 模块全量能力。
-
-建议新项目首选 ReSdk 模式作为参考。
+> - 3、此模式需要对 Sa-Token SSO 内部实现较为熟悉,才可以驾驭。