mirror of
https://gitee.com/dromara/sa-token.git
synced 2026-05-13 20:32:08 +08:00
feat(sso): 补全最新版 SSO NoSdk 模式实现
This commit is contained in:
@@ -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:
|
||||
# 应用名称
|
||||
|
||||
@@ -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 版本示例");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 = "<h2>Sa-Token SSO-Client 应用端</h2>" +
|
||||
"<p>当前会话登录账号:" + session.getAttribute("userId") + "</p>" +
|
||||
"<p><a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">登录</a>" +
|
||||
" <a href='/sso/logout?back=' + + encodeURIComponent(location.href);>注销</a>" +
|
||||
Object userId = session.getAttribute("userId");
|
||||
String str = "<h2>Sa-Token SSO-Client 应用端 (模式三-NoSdk)</h2>" +
|
||||
"<p>当前会话是否登录:" + (userId != null) + " (" + userId + ")</p>" +
|
||||
"<p><a href=\"javascript:location.href='/sso/login?back=' + encodeURIComponent(location.href);\">登录</a>" +
|
||||
" <a href='/sso/logout?back=' + + encodeURIComponent(location.href);>注销</a>" +
|
||||
" <a href='/sso/myInfo' target=\"_blank\">获取资料</a></p>";
|
||||
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<String, String> 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<String, String> 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<String, String>
|
||||
Map<String, String> params = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String[]> 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<String, String> 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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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<String, String> params) {
|
||||
StringBuilder sb = new StringBuilder(baseUrl).append("?");
|
||||
for (Map.Entry<String, String> 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编码
|
||||
|
||||
@@ -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<String, String> params) {
|
||||
TreeMap<String, String> sorted = new TreeMap<>(params);
|
||||
sorted.remove("sign");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, String> 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<String, String> 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<String, String> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 内部实现较为熟悉,才可以驾驭。
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user