SSO 模式三的无 sdk 对接demo

This commit is contained in:
click33 2022-04-30 01:20:49 +08:00
parent de39d91b71
commit ed18af72b8
10 changed files with 698 additions and 4 deletions

View File

@ -11,13 +11,13 @@ import java.util.Map;
public interface SaJsonTemplate {
/**
* 将任意对象转换为 json 字符串
*
* @param obj 对象
* 将任意对象转换为 json 字符串
*
* @param obj 对象
* @return 转换后的 json 字符串
*/
public String toJsonString(Object obj);
/**
* json 字符串解析为 Map
*

View File

@ -17,6 +17,7 @@ public class SaJsonTemplateDefaultImpl implements SaJsonTemplate {
/**
* 将任意对象转换为 json 字符串
*/
@Override
public String toJsonString(Object obj) {
throw new ApiDisabledException(ERROR_MESSAGE);
}
@ -24,6 +25,7 @@ public class SaJsonTemplateDefaultImpl implements SaJsonTemplate {
/**
* json 字符串解析为 Map
*/
@Override
public Map<String, Object> parseJsonToMap(String jsonStr) {
throw new ApiDisabledException(ERROR_MESSAGE);
};

View File

@ -0,0 +1,12 @@
target/
node_modules/
bin/
.settings/
unpackage/
.classpath
.project
.idea/
.factorypath

View File

@ -0,0 +1,36 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-demo-sso3-client-nosdk</artifactId>
<version>0.0.1-SNAPSHOT</version>
<!-- SpringBoot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- SpringBoot依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Http请求工具 -->
<dependency>
<groupId>com.ejlchina</groupId>
<artifactId>okhttps</artifactId>
<version>3.1.1</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,14 @@
package com.pj;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SaSsoClientApplication {
public static void main(String[] args) {
SpringApplication.run(SaSsoClientApplication.class, args);
System.out.println("\nSa-Token SSO模式三 Client端 无SDK版本 启动成功");
}
}

View File

@ -0,0 +1,177 @@
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 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;
/**
* SSO Client端 Controller
* @author kong
*/
@RestController
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>" +
" <a href='/sso/myinfo' target=\"_blank\">获取资料</a></p>";
return str;
}
// SSO-Client端单点登录地址
@RequestMapping("/sso/login")
public Object ssoLogin(String ticket, @RequestParam(defaultValue = "/") String back,
HttpServletRequest request, HttpServletResponse response, HttpSession session) throws IOException {
// 如果已经登录则直接返回
if(session.getAttribute("userId") != null) {
response.sendRedirect(back);
return null;
}
/*
* 此时有两种情况:
* 情况1ticket无值说明此请求是Client端访问需要重定向至SSO认证中心
* 情况2ticket有值说明此请求从SSO认证中心重定向而来需要根据ticket进行登录
*/
if(ticket == null) {
String currUrl = request.getRequestURL().toString();
String clientLoginUrl = currUrl + "?back=" + SsoRequestUtil.encodeUrl(back);
String serverAuthUrl = SsoRequestUtil.authUrl + "?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 checkUrl = SsoRequestUtil.checkTicketUrl + "?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);
// 返回 back 地址
response.sendRedirect(back);
return null;
} else {
// sso-server 回应的消息作为异常抛出
throw new RuntimeException(result.getMsg());
}
}
}
// SSO-Client端单点注销地址
@RequestMapping("/sso/logout")
public Object ssoLogout(@RequestParam(defaultValue = "/") String back,
HttpServletResponse response, HttpSession session) throws IOException {
// 如果未登录则无需注销
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, SsoRequestUtil.secretkey); // 参数签名
String url = SsoRequestUtil.sloUrl +
"?loginId=" + loginId +
"&timestamp=" + timestamp +
"&nonce=" + nonce +
"&sign=" + sign;
AjaxJson result = SsoRequestUtil.request(url);
// 校验响应状态码200 代表成功
if(result.getCode() == 200) {
// 极端场景下sso-server 中心的单点注销可能并不会通知到此 client 所以这里需要再补一刀
session.removeAttribute("userId");
// 返回 back 地址
response.sendRedirect(back);
return null;
} else {
// sso-server 回应的消息作为异常抛出
throw new RuntimeException(result.getMsg());
}
}
// SSO-Client端单点注销回调地址
@RequestMapping("/sso/logoutCall")
public Object ssoLogoutCall(String loginId, String timestamp, String nonce, String sign) {
// 校验签名
String calcSign = SsoRequestUtil.getSign(loginId, timestamp, nonce, SsoRequestUtil.secretkey);
if(calcSign.equals(sign) == false) {
return AjaxJson.getError("无效签名,拒绝应答");
}
// 注销这个账号id
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 + " 注销成功");
}
// 查询我的账号信息 (调用此接口的前提是 sso-server 端开放了 /sso/userinfo 路由)
@RequestMapping("/sso/myinfo")
public Object myinfo(HttpSession session) {
// 如果尚未登录
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, SsoRequestUtil.secretkey); // 参数签名
String url = SsoRequestUtil.userinfoUrl +
"?loginId=" + loginId +
"&timestamp=" + timestamp +
"&nonce=" + nonce +
"&sign=" + sign;
AjaxJson result = SsoRequestUtil.request(url);
// 返回给前端
return result;
}
// 全局异常拦截
@ExceptionHandler
public AjaxJson handlerException(Exception e) {
e.printStackTrace();
return AjaxJson.getError(e.getMessage());
}
}

View File

@ -0,0 +1,156 @@
package com.pj.sso;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.util.Map;
import java.util.Random;
import com.ejlchina.okhttps.OkHttps;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pj.sso.util.AjaxJson;
/**
* 封装一些 sso 共用方法
*
* @author kong
* @date: 2022-4-30
*/
public class SsoRequestUtil {
/**
* SSO-Server端 统一认证地址
*/
public static String authUrl = "http://sa-sso-server.com:9000/sso/auth";
/**
* 使用 Http 请求校验ticket
*/
// public static boolean isHttp = true;
/**
* SSO-Server端 ticket校验地址
*/
public static String checkTicketUrl = "http://sa-sso-server.com:9000/sso/checkTicket";
/**
* 打开单点注销功能
*/
public static boolean isSlo = true;
/**
* 单点注销地址
*/
public static String sloUrl = "http://sa-sso-server.com:9000/sso/logout";
/**
* 接口调用秘钥
*/
public static String secretkey = "kQwIOrYvnXmSDkwEiFngrKidMcdrgKor";
/**
* SSO-Server端 查询userinfo地址
*/
public static String userinfoUrl = "http://sa-sso-server.com:9000/sso/userinfo";
// -------------------------- 工具方法
/**
* 发出请求并返回 SaResult 结果
* @param url 请求地址
* @return 返回的结果
*/
@SuppressWarnings("unchecked")
public static AjaxJson request(String url) {
String body = OkHttps.sync(url)
.post()
.getBody()
.toString();
try {
Map<String, Object> map = new ObjectMapper().readValue(body, Map.class);
return new AjaxJson(map);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 根据参数计算签名
* @param loginId 账号id
* @param timestamp 当前时间戳13位
* @param nonce 随机字符串
* @param secretkey 账号id
* @return 签名
*/
public static String getSign(Object loginId, String timestamp, String nonce, String secretkey) {
return md5("loginId=" + loginId + "&nonce=" + nonce + "&timestamp=" + timestamp + "&key=" + secretkey);
}
/**
* 指定元素是否为null或者空字符串
* @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编码
* @param url see note
* @return see note
*/
public static String encodeUrl(String url) {
try {
return URLEncoder.encode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,231 @@
package com.pj.sso.util;
import java.io.Serializable;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* ajax请求返回Json格式数据的封装 <br>
* 所有预留字段<br>
* code=状态码 <br>
* msg=描述信息 <br>
* data=携带对象 <br>
* pageNo=当前页 <br>
* pageSize=页大小 <br>
* startIndex=起始索引 <br>
* dataCount=数据总数 <br>
* pageCount=分页总数 <br>
* <p> 返回范例</p>
* <pre>
{
"code": 200, // 成功时=200, 失败时=500 msg=失败原因
"msg": "ok",
"data": {}
}
</pre>
*/
public class AjaxJson extends LinkedHashMap<String, Object> implements Serializable{
private static final long serialVersionUID = 1L; // 序列化版本号
public static final int CODE_SUCCESS = 200; // 成功状态码
public static final int CODE_ERROR = 500; // 错误状态码
public static final int CODE_WARNING = 501; // 警告状态码
public static final int CODE_NOT_JUR = 403; // 无权限状态码
public static final int CODE_NOT_LOGIN = 401; // 未登录状态码
public static final int CODE_INVALID_REQUEST = 400; // 无效请求状态码
// ============================ 写值取值 ==================================
/** 给code赋值连缀风格 */
public AjaxJson setCode(int code) {
this.put("code", code);
return this;
}
/** 返回code */
public Integer getCode() {
return (Integer)this.get("code");
}
/** 给msg赋值连缀风格 */
public AjaxJson setMsg(String msg) {
this.put("msg", msg);
return this;
}
/** 获取msg */
public String getMsg() {
return (String)this.get("msg");
}
/** 给data赋值连缀风格 */
public AjaxJson setData(Object data) {
this.put("data", data);
return this;
}
/** 获取data */
public Object getData() {
return this.get("data");
}
/** 将data还原为指定类型并返回 */
@SuppressWarnings("unchecked")
public <T> T getData(Class<T> cs) {
return (T) this.getData();
}
/** 给dataCount(数据总数)赋值,连缀风格 */
public AjaxJson setDataCount(Long dataCount) {
this.put("dataCount", dataCount);
// 如果提供了数据总数则尝试计算page信息
if(dataCount != null && dataCount >= 0) {
// 如果已有page信息
if(get("pageNo") != null) {
this.initPageInfo();
}
// // 或者是JavaWeb环境
// else if(SoMap.isJavaWeb()) {
// SoMap so = SoMap.getRequestSoMap();
// this.setPageNoAndSize(so.getKeyPageNo(), so.getKeyPageSize());
// this.initPageInfo();
// }
}
return this;
}
/** 获取dataCount(数据总数) */
public Long getDataCount() {
return (Long)this.get("dataCount");
}
/** 设置pageNo 和 pageSize并计算出startIndex于pageCount */
public AjaxJson setPageNoAndSize(long pageNo, long pageSize) {
this.put("pageNo", pageNo);
this.put("pageSize", pageSize);
return this;
}
/** 根据 pageSize dataCount计算startIndex 与 pageCount */
public AjaxJson initPageInfo() {
long pageNo = (long)this.get("pageNo");
long pageSize = (long)this.get("pageSize");
long dataCount = (long)this.get("dataCount");
this.set("startIndex", (pageNo - 1) * pageSize);
long pc = dataCount / pageSize;
this.set("pageCount", (dataCount % pageSize == 0 ? pc : pc + 1));
return this;
}
/** 写入一个值 自定义key, 连缀风格 */
public AjaxJson set(String key, Object data) {
this.put(key, data);
return this;
}
/** 写入一个Map, 连缀风格 */
public AjaxJson setMap(Map<String, ?> map) {
for (String key : map.keySet()) {
this.put(key, map.get(key));
}
return this;
}
// ============================ 构建 ==================================
public AjaxJson(int code, String msg, Object data, Long dataCount) {
this.setCode(code);
this.setMsg(msg);
this.setData(data);
if(dataCount != null) {
this.setDataCount(dataCount);
}
}
public AjaxJson(Map<String, Object> map) {
for (String key: map.keySet()) {
this.set(key, map.get(key));
}
}
/** 返回成功 */
public static AjaxJson getSuccess() {
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
}
public static AjaxJson getSuccess(String msg) {
return new AjaxJson(CODE_SUCCESS, msg, null, null);
}
public static AjaxJson getSuccess(String msg, Object data) {
return new AjaxJson(CODE_SUCCESS, msg, data, null);
}
public static AjaxJson getSuccessData(Object data) {
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
}
/** 返回失败 */
public static AjaxJson getError() {
return new AjaxJson(CODE_ERROR, "error", null, null);
}
public static AjaxJson getError(String msg) {
return new AjaxJson(CODE_ERROR, msg, null, null);
}
/** 返回警告 */
public static AjaxJson getWarning() {
return new AjaxJson(CODE_ERROR, "warning", null, null);
}
public static AjaxJson getWarning(String msg) {
return new AjaxJson(CODE_WARNING, msg, null, null);
}
/** 返回未登录 */
public static AjaxJson getNotLogin() {
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
}
/** 返回没有权限的 */
public static AjaxJson getNotJur(String msg) {
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
}
/** 返回一个自定义状态码的 */
public static AjaxJson get(int code, String msg){
return new AjaxJson(code, msg, null, null);
}
/** 返回分页和数据的 */
public static AjaxJson getPageData(Long dataCount, Object data){
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
}
/** 返回, 根据受影响行数的(大于0=ok小于0=error) */
public static AjaxJson getByLine(int line){
if(line > 0){
return getSuccess("ok", line);
}
return getError("error").setData(line);
}
/** 返回,根据布尔值来确定最终结果的 (true=okfalse=error) */
public static AjaxJson getByBoolean(boolean b){
return b ? getSuccess("ok") : getError("error");
}
// // 历史版本遗留代码
// public int code; // 状态码
// public String msg; // 描述信息
// public Object data; // 携带对象
// public Long dataCount; // 数据总数用于分页
}

View File

@ -0,0 +1,34 @@
package com.pj.sso.util;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.springframework.stereotype.Component;
/**
* 记录所有已创建的 HttpSession 对象
*
* <b> 此种方式有性能问题仅做demo示例真实项目中请更换为其它方案记录用户会话数据 </b>
*
* @author kong
* @date: 2022-4-30
*/
@Component
public class MyHttpSessionHolder implements HttpSessionListener {
public static List<HttpSession> sessionList = new ArrayList<>();
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
sessionList.add(httpSessionEvent.getSession());
}
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
HttpSession session = httpSessionEvent.getSession();
sessionList.remove(session);
}
}

View File

@ -0,0 +1,32 @@
# 端口
server:
port: 9001
spring:
# 配置Sa-Token单独使用的Redis连接 此处与SSO-Server端连接不同的Redis
redis:
# Redis数据库索引
database: 2
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码默认为空
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0