单点登录:CAS模式

This commit is contained in:
click33
2021-06-24 18:00:54 +08:00
parent 6c874e6737
commit dd2665ceb1
30 changed files with 1309 additions and 9 deletions

View File

@@ -0,0 +1,112 @@
package cn.dev33.satoken.config;
import cn.dev33.satoken.util.SaFoxUtil;
/**
* Sa-Token-SSO 单点登录模块 配置Model
* @author kong
*
*/
public class SaSsoConfig {
/**
* Ticket有效期 (单位: 秒)
*/
public long ticketTimeout = 60 * 5;
/**
* 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
*/
public String allowUrl = "*";
/**
* 调用秘钥
*/
public String secretkey;
/**
* SSO-Server端授权地址
*/
public String serverUrl;
/**
* @return SSO-Server端授权地址
*/
public String getServerUrl() {
return serverUrl;
}
/**
* @param serverUrl SSO-Server端授权地址
*/
public void setServerUrl(String serverUrl) {
this.serverUrl = serverUrl;
}
/**
* @return Ticket有效期 (单位: 秒)
*/
public long getTicketTimeout() {
return ticketTimeout;
}
/**
* @param ticketTimeout Ticket有效期 (单位: 秒)
* @return 对象自身
*/
public SaSsoConfig setTicketTimeout(long ticketTimeout) {
this.ticketTimeout = ticketTimeout;
return this;
}
/**
* @return 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
*/
public String getAllowUrl() {
return allowUrl;
}
/**
* @param allowUrl 所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
* @return 对象自身
*/
public SaSsoConfig setAllowUrl(String allowUrl) {
this.allowUrl = allowUrl;
return this;
}
/**
* @return 调用秘钥
*/
public String getSecretkey() {
return secretkey;
}
/**
* @param secretkey 调用秘钥
* @return 对象自身
*/
public SaSsoConfig setSecretkey(String secretkey) {
this.secretkey = secretkey;
return this;
}
@Override
public String toString() {
return "SaSsoConfig [ticketTimeout=" + ticketTimeout + ", allowUrl=" + allowUrl + ", secretkey=" + secretkey
+ ", serverUrl=" + serverUrl + "]";
}
/**
* 以数组形式写入允许的授权回调地址
* @param url 所有集合
* @return 对象自身
*/
public SaSsoConfig setAllow(String ...url) {
this.allowUrl = SaFoxUtil.arrayJoin(url);
return this;
}
}

View File

@@ -66,6 +66,11 @@ public class SaTokenConfig {
*/
private String jwtSecretKey;
/**
* SSO单点登录配置对象
*/
public SaSsoConfig sso = new SaSsoConfig();
/**
* @return token名称 (同时也是cookie名称)
@@ -343,6 +348,22 @@ public class SaTokenConfig {
return this;
}
/**
* @return SSO单点登录配置对象
*/
public SaSsoConfig getSso() {
return sso;
}
/**
* @param sso SSO单点登录配置对象
*/
public void setSso(SaSsoConfig sso) {
this.sso = sso;
}
/**
* toString()
*/
@@ -354,10 +375,12 @@ public class SaTokenConfig {
+ tokenStyle + ", dataRefreshPeriod=" + dataRefreshPeriod + ", tokenSessionCheckLogin="
+ tokenSessionCheckLogin + ", autoRenew=" + autoRenew + ", cookieDomain=" + cookieDomain
+ ", tokenPrefix=" + tokenPrefix + ", isPrint=" + isPrint + ", isLog=" + isLog + ", jwtSecretKey="
+ jwtSecretKey + "]";
+ jwtSecretKey + ", sso=" + sso + "]";
}
/**
* <h1> 本函数设计已过时,未来版本可能移除此函数,请及时更换为 setIsConcurrent() ,使用方式保持不变 </h1>
* @param allowConcurrentLogin see note
@@ -369,7 +392,6 @@ public class SaTokenConfig {
return this;
}
/**
* <h1> 本函数设计已过时,未来版本可能移除此函数,请及时更换为 setIsConcurrent() ,使用方式保持不变 </h1>
* @param isV see note

View File

@@ -40,6 +40,12 @@ public interface SaRequest {
*/
public String getRequestPath();
/**
* 返回当前请求的urlhttp://xxx.com/?id=127
* @return see note
*/
public String getUrl();
/**
* 返回当前请求的类型
* @return see note

View File

@@ -0,0 +1,19 @@
package cn.dev33.satoken.sso;
/**
* Sa-Token-SSO模块相关常量
* @author kong
*
*/
public class SaSsoConsts {
/** redirect参数名称 */
public static final String REDIRECT_NAME = "redirect";
/** ticket参数名称 */
public static final String TICKET_NAME = "ticket";
/** back参数名称 */
public static final String BACK_NAME = "back";
}

View File

@@ -0,0 +1,180 @@
package cn.dev33.satoken.sso;
import java.util.Arrays;
import java.util.List;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.util.SaFoxUtil;
/**
* Sa-Token-SSO 单点登录接口
* @author kong
*
*/
public interface SaSsoInterface {
/**
* 创建一个 Ticket码
* @param loginId 账号id
* @return 票据
*/
public default String createTicket(Object loginId) {
// 随机一个ticket
String ticket = SaFoxUtil.getRandomString(64);
// 保存入库
long ticketTimeout = SaManager.getConfig().getSso().getTicketTimeout();
SaManager.getSaTokenDao().set(splicingKeyTicketToId(ticket), String.valueOf(loginId), ticketTimeout);
SaManager.getSaTokenDao().set(splicingKeyIdToTicket(loginId), String.valueOf(ticket), ticketTimeout);
// 返回
return ticket;
}
/**
* 删除一个 Ticket码
* @param ticket Ticket码
*/
public default void deleteTicket(String ticket) {
SaManager.getSaTokenDao().delete(splicingKeyTicketToId(ticket));
}
/**
* 根据 账号id & 重定向地址,构建[SSO-Client端-重定向地址]
* @param loginId 账号id
* @param redirect 重定向地址
* @return [SSO-Client端-重定向地址]
*/
public default String buildRedirectUrl(Object loginId, String redirect) {
// 校验授权地址
checkAuthUrl(redirect);
// 删掉旧ticket
String oldTicket = SaManager.getSaTokenDao().get(splicingKeyIdToTicket(loginId));
if(oldTicket != null) {
deleteTicket(oldTicket);
}
// 获取新ticket
String ticket = createTicket(loginId);
// 构建 授权重定向地址
redirect = encodeBackParam(redirect);
String redirectUrl = SaFoxUtil.joinParam(redirect, SaSsoConsts.TICKET_NAME + "=" + ticket);
return redirectUrl;
}
/**
* 根据 Ticket码 获取账号id如果Ticket码无效则返回null
* @param ticket Ticket码
* @return 账号id
*/
public default Object getLoginId(String ticket) {
if(SaFoxUtil.isEmpty(ticket)) {
return null;
}
return SaManager.getSaTokenDao().get(splicingKeyTicketToId(ticket));
}
/**
* 根据 Ticket码 获取账号id并转换为指定类型
* @param ticket Ticket码
* @return 账号id
*/
public default <T> T getLoginId(String ticket, Class<T> cs) {
return SaFoxUtil.getValueByType(getLoginId(ticket), cs);
}
/**
* 校验url合法性
* @param url 地址
*/
public default void checkAuthUrl(String url) {
// 1、是否是一个有效的url
if(SaFoxUtil.isUrl(url) == false) {
throw new SaTokenException("无效回调地址:" + url);
}
// 2、是否在[允许地址列表]之中
String authUrl = SaManager.getConfig().getSso().getAllowUrl();
List<String> authUrlList = Arrays.asList(authUrl.split(","));
if(SaManager.getSaTokenAction().hasElement(authUrlList, url) == false) {
throw new SaTokenException("非法回调地址:" + url);
}
// 验证通过
return;
}
/**
* 根据 Client端登录地址 & back地址 ,构建[SSO-Server端-认证地址]
* @param clientLoginUrl Client端登录地址
* @param back 回调路径
* @return [SSO-Server端-认证地址 ]
*/
public default String buildServerAuthUrl(String clientLoginUrl, String back) {
// 服务端认证地址
String serverUrl = SaManager.getConfig().getSso().getServerUrl();
// 对back地址编码
back = (back == null ? "" : back);
back = SaFoxUtil.encodeUrl(back);
// 拼接最终地址格式serverAuthUrl = http://xxx.com?redirectUrl=xxx.com?back=xxx.com
clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, SaSsoConsts.BACK_NAME + "=" + back);
String serverAuthUrl = SaFoxUtil.joinParam(serverUrl, SaSsoConsts.REDIRECT_NAME + "=" + clientLoginUrl);
// 返回
return serverAuthUrl;
}
/**
* 对url中的back参数进行URL编码, 解决超链接重定向后参数丢失的bug
* @param url url
* @return 编码过后的url
*/
public default String encodeBackParam(String url) {
// 获取back参数所在位置
int index = url.indexOf("?" + SaSsoConsts.BACK_NAME + "=");
if(index == -1) {
index = url.indexOf("&" + SaSsoConsts.BACK_NAME + "=");
if(index == -1) {
return url;
}
}
// 开始编码
int length = SaSsoConsts.BACK_NAME.length() + 2;
String back = url.substring(index + length);
back = SaFoxUtil.encodeUrl(back);
// 放回url中
url = url.substring(0, index + length) + back;
return url;
}
// ------------------- 返回相应key -------------------
/**
* 拼接keyTicket 查 账号Id
* @param ticket
* @return key
*/
public default String splicingKeyTicketToId(String ticket) {
return SaManager.getConfig().getTokenName() + ":ticket:" + ticket;
}
/**
* 拼接key账号Id 反查 Ticket
* @param id 账号id
* @return key
*/
public default String splicingKeyIdToTicket(Object id) {
return SaManager.getConfig().getTokenName() + ":id-ticket:" + id;
}
}

View File

@@ -0,0 +1,78 @@
package cn.dev33.satoken.sso;
/**
* Sa-Token-SSO 单点登录工具类
* @author kong
*
*/
public class SaSsoUtil {
/**
* 底层 SaSsoServerInterface 对象
*/
public static SaSsoInterface saSsoInterface = new SaSsoInterface() {};
/**
* 创建一个 Ticket票据
* @param loginId 账号id
* @return 票据
*/
public static String createTicket(Object loginId) {
return saSsoInterface.createTicket(loginId);
}
/**
* 删除一个 Ticket码
* @param ticket Ticket码
*/
public static void deleteTicket(String ticket) {
saSsoInterface.deleteTicket(ticket);
}
/**
* 根据 账号id & 重定向地址,构建[SSO-Client端-重定向地址]
* @param loginId 账号id
* @param redirect 重定向地址
* @return [SSO-Client端-重定向地址]
*/
public static String buildRedirectUrl(Object loginId, String redirect) {
return saSsoInterface.buildRedirectUrl(loginId, redirect);
}
/**
* 根据 Ticket码 获取账号id如果Ticket码无效则返回null
* @param ticket Ticket码
* @return 账号id
*/
public static Object getLoginId(String ticket) {
return saSsoInterface.getLoginId(ticket);
}
/**
* 根据 Ticket码 获取账号id并转换为指定类型
* @param ticket Ticket码
* @return 账号id
*/
public static <T> T getLoginId(String ticket, Class<T> cs) {
return saSsoInterface.getLoginId(ticket, cs);
}
/**
* 校验url合法性
* @param url 地址
*/
public static void checkAuthUrl(String url) {
saSsoInterface.checkAuthUrl(url);
}
/**
* 根据 Client端登录地址 & back地址 ,构建[SSO-Server端-认证地址]
* @param clientLoginUrl Client端登录地址
* @param back 回调路径
* @return [SSO-Server端-认证地址 ]
*/
public static String buildServerAuthUrl(String clientLoginUrl, String back) {
return saSsoInterface.buildServerAuthUrl(clientLoginUrl, back);
}
}

View File

@@ -1,5 +1,8 @@
package cn.dev33.satoken.util;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
@@ -9,6 +12,8 @@ import java.util.List;
import java.util.Random;
import java.util.regex.Pattern;
import cn.dev33.satoken.exception.SaTokenException;
/**
* Sa-Token 内部工具类
*
@@ -147,7 +152,6 @@ public class SaFoxUtil {
return Pattern.matches(patt.replaceAll("\\*", ".*"), str);
}
/**
* 将指定值转化为指定类型
* @param <T> 泛型
@@ -186,5 +190,104 @@ public class SaFoxUtil {
return (T)obj3;
}
/**
* 在url上拼接上kv参数并返回
* @param url url
* @param parameStr 参数, 例如 id=1001
* @return 拼接后的url字符串
*/
public static String joinParam(String url, String parameStr) {
// 如果参数为空, 直接返回
if(parameStr == null || parameStr.length() == 0) {
return url;
}
if(url == null) {
url = "";
}
int index = url.indexOf('?');
// ? 不存在
if(index == -1) {
return url + '?' + parameStr;
}
// ? 是最后一位
if(index == url.length() - 1) {
return url + parameStr;
}
// ? 是其中一位
if(index > -1 && index < url.length() - 1) {
String separatorChar = "&";
// 如果最后一位是 不是&, 且 parameStr 第一位不是 &, 就增送一个 &
if(url.lastIndexOf(separatorChar) != url.length() - 1 && parameStr.indexOf(separatorChar) != 0) {
return url + separatorChar + parameStr;
} else {
return url + parameStr;
}
}
// 正常情况下, 代码不可能执行到此
return url;
}
/**
* 将数组的所有元素使用逗号拼接在一起
* @param arr 数组
* @return 字符串,例: a,b,c
*/
public static String arrayJoin(String[] arr) {
if(arr == null) {
return "";
}
String str = "";
for (int i = 0; i < arr.length; i++) {
str += arr[i];
if(i != arr.length - 1) {
str += ",";
}
}
return str;
}
/**
* 验证URL的正则表达式
*/
public static final String URL_REGEX = "(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]";
/**
* 使用正则表达式判断一个字符串是否为URL
* @param str 字符串
* @return 拼接后的url字符串
*/
public static boolean isUrl(String str) {
if(str == null) {
return false;
}
return str.toLowerCase().matches(URL_REGEX);
}
/**
* 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 SaTokenException(e);
}
}
/**
* URL解码
* @param url see note
* @return see note
*/
public static String decoderUrl(String url) {
try {
return URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new SaTokenException(e);
}
}
}