cn.dev33.satoken.sso.SaSsoTemplate Maven / Gradle / Ivy
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.sso;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.config.SaSsoConfig;
import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.sign.SaSignTemplate;
import cn.dev33.satoken.sso.error.SaSsoErrorCode;
import cn.dev33.satoken.sso.exception.SaSsoException;
import cn.dev33.satoken.sso.name.ApiName;
import cn.dev33.satoken.sso.name.ParamName;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaStrategy;
import cn.dev33.satoken.util.SaFoxUtil;
import cn.dev33.satoken.util.SaResult;
import java.util.*;
/**
* Sa-Token-SSO 单点登录模块
*
* @author click33
* @since 1.30.0
*/
public class SaSsoTemplate {
// ---------------------- 全局配置 ----------------------
/**
* 所有 API 名称
*/
public ApiName apiName = new ApiName();
/**
* 所有参数名称
*/
public ParamName paramName = new ParamName();
/**
* @param paramName 替换 paramName 对象
* @return 对象自身
*/
public SaSsoTemplate setParamName(ParamName paramName) {
this.paramName = paramName;
return this;
}
/**
* @param apiName 替换 apiName 对象
* @return 对象自身
*/
public SaSsoTemplate setApiName(ApiName apiName) {
this.apiName = apiName;
return this;
}
/**
* 获取底层使用的会话对象
* @return /
*/
public StpLogic getStpLogic() {
return StpUtil.stpLogic;
}
/**
* 获取底层使用的配置对象
* @return /
*/
public SaSsoConfig getSsoConfig() {
return SaSsoManager.getConfig();
}
/**
* 获取底层使用的 API 签名对象
* @return /
*/
public SaSignTemplate getSignTemplate() {
return SaManager.getSaSignTemplate();
}
// ---------------------- Ticket 操作 ----------------------
/**
* 保存 Ticket 关联的 loginId
* @param ticket ticket码
* @param loginId 账号id
*/
public void saveTicket(String ticket, Object loginId) {
// 保存 ticket -> loginId 的关系
long ticketTimeout = SaSsoManager.getConfig().getTicketTimeout();
SaManager.getSaTokenDao().set(splicingTicketSaveKey(ticket), String.valueOf(loginId), ticketTimeout);
}
/**
* 保存 Ticket 索引 (id 反查 ticket)
* @param ticket ticket码
* @param loginId 账号id
*/
public void saveTicketIndex(String ticket, Object loginId) {
long ticketTimeout = SaSsoManager.getConfig().getTicketTimeout();
SaManager.getSaTokenDao().set(splicingTicketIndexKey(loginId), String.valueOf(ticket), ticketTimeout);
}
/**
* 保存 Ticket 关联的 client
* @param ticket ticket码
* @param client 客户端标识
*/
public void saveTicketToClient(String ticket, String client) {
if(SaFoxUtil.isEmpty(client)) {
return;
}
long ticketTimeout = SaSsoManager.getConfig().getTicketTimeout();
SaManager.getSaTokenDao().set(splicingTicketToClientSaveKey(ticket), client, ticketTimeout);
}
/**
* 删除 Ticket
* @param ticket Ticket码
*/
public void deleteTicket(String ticket) {
if(ticket == null) {
return;
}
SaManager.getSaTokenDao().delete(splicingTicketSaveKey(ticket));
}
/**
* 删除 Ticket索引
* @param loginId 账号id
*/
public void deleteTicketIndex(Object loginId) {
if(loginId == null) {
return;
}
SaManager.getSaTokenDao().delete(splicingTicketIndexKey(loginId));
}
/**
* 删除 Ticket 关联的 client
* @param ticket Ticket码
*/
public void deleteTicketToClient(String ticket) {
if(ticket == null) {
return;
}
SaManager.getSaTokenDao().delete(splicingTicketToClientSaveKey(ticket));
}
/**
* 查询 ticket 指向的 loginId,如果 ticket 码无效则返回 null
* @param ticket Ticket码
* @return 账号id
*/
public Object getLoginId(String ticket) {
if(SaFoxUtil.isEmpty(ticket)) {
return null;
}
return SaManager.getSaTokenDao().get(splicingTicketSaveKey(ticket));
}
/**
* 查询 ticket 指向的 loginId,并转换为指定类型
* @param 要转换的类型
* @param ticket Ticket码
* @param cs 要转换的类型
* @return 账号id
*/
public T getLoginId(String ticket, Class cs) {
return SaFoxUtil.getValueByType(getLoginId(ticket), cs);
}
/**
* 查询 指定 loginId 其所属的 ticket 值
* @param loginId 账号id
* @return Ticket值
*/
public String getTicketValue(Object loginId) {
if(loginId == null) {
return null;
}
return SaManager.getSaTokenDao().get(splicingTicketIndexKey(loginId));
}
/**
* 查询 ticket 关联的 client,如果 ticket 码无效则返回 null
* @param ticket Ticket码
* @return 账号id
*/
public String getTicketToClient(String ticket) {
if(SaFoxUtil.isEmpty(ticket)) {
return null;
}
return SaManager.getSaTokenDao().get(splicingTicketToClientSaveKey(ticket));
}
//
/**
* 根据 账号id 创建一个 Ticket码
* @param loginId 账号id
* @param client 客户端标识
* @return Ticket码
*/
public String createTicket(Object loginId, String client) {
// 创建 Ticket
String ticket = randomTicket(loginId);
// 保存 Ticket
saveTicket(ticket, loginId);
saveTicketIndex(ticket, loginId);
saveTicketToClient(ticket, client);
// 返回 Ticket
return ticket;
}
/**
* 校验 Ticket 码,获取账号id,如果此ticket是有效的,则立即删除
* @param ticket Ticket码
* @return 账号id
*/
public Object checkTicket(String ticket) {
return checkTicket(ticket, getSsoConfig().getClient());
}
/**
* 校验 Ticket 码,获取账号id,如果此ticket是有效的,则立即删除
* @param ticket Ticket码
* @param client client 标识
* @return 账号id
*/
public Object checkTicket(String ticket, String client) {
// 读取 loginId
String loginId = SaManager.getSaTokenDao().get(splicingTicketSaveKey(ticket));
if(loginId != null) {
// 解析出这个 ticket 关联的 Client
String ticketClient = getTicketToClient(ticket);
// 如果指定了 client 标识,则校验一下 client 标识是否一致
if(SaFoxUtil.isNotEmpty(client) && SaFoxUtil.notEquals(client, ticketClient)) {
throw new SaSsoException("该 ticket 不属于 client=" + client + ", ticket 值: " + ticket)
.setCode(SaSsoErrorCode.CODE_30011);
}
// 删除 ticket 信息,使其只有一次性有效
deleteTicket(ticket);
deleteTicketIndex(loginId);
deleteTicketToClient(ticket);
}
//
return loginId;
}
/**
* 随机一个 Ticket码
* @param loginId 账号id
* @return Ticket码
*/
public String randomTicket(Object loginId) {
return SaFoxUtil.getRandomString(64);
}
/**
* 获取:所有允许的授权回调地址,多个用逗号隔开 (不在此列表中的URL将禁止下放ticket)
* @return see note
*/
public String getAllowUrl() {
// 默认从配置文件中返回
return SaSsoManager.getConfig().getAllowUrl();
}
/**
* 校验重定向url合法性
* @param url 下放ticket的url地址
*/
public void checkRedirectUrl(String url) {
// 1、是否是一个有效的url
if( ! SaFoxUtil.isUrl(url) ) {
throw new SaSsoException("无效redirect:" + url).setCode(SaSsoErrorCode.CODE_30001);
}
// 2、截取掉?后面的部分
int qIndex = url.indexOf("?");
if(qIndex != -1) {
url = url.substring(0, qIndex);
}
// 3、是否在[允许地址列表]之中
List authUrlList = Arrays.asList(getAllowUrl().replaceAll(" ", "").split(","));
if( ! SaStrategy.instance.hasElement.apply(authUrlList, url) ) {
throw new SaSsoException("非法redirect:" + url).setCode(SaSsoErrorCode.CODE_30002);
}
// 校验通过 √
}
// ------------------- SSO 模式三相关 -------------------
/**
* 为指定账号id注册单点注销回调URL
* @param loginId 账号id
* @param sloCallbackUrl 单点注销时的回调URL
*/
public void registerSloCallbackUrl(Object loginId, String sloCallbackUrl) {
if(SaFoxUtil.isEmpty(loginId) || SaFoxUtil.isEmpty(sloCallbackUrl)) {
return;
}
SaSession session = getStpLogic().getSessionByLoginId(loginId);
Set urlSet = session.get(SaSsoConsts.SLO_CALLBACK_SET_KEY, HashSet::new);
urlSet.add(sloCallbackUrl);
session.set(SaSsoConsts.SLO_CALLBACK_SET_KEY, urlSet);
}
/**
* 指定账号单点注销
* @param loginId 指定账号
*/
public void ssoLogout(Object loginId) {
// 如果这个账号尚未登录,则无操作
SaSession session = getStpLogic().getSessionByLoginId(loginId, false);
if(session == null) {
return;
}
// step.1 遍历通知 Client 端注销会话
SaSsoConfig cfg = SaSsoManager.getConfig();
Set urlSet = session.get(SaSsoConsts.SLO_CALLBACK_SET_KEY, HashSet::new);
for (String url : urlSet) {
url = joinLoginIdAndSign(url, loginId);
cfg.getSendHttp().apply(url);
}
// step.2 Server端注销
getStpLogic().logout(loginId);
}
/**
* 根据配置的 getData 地址,查询数据
* @param paramMap 查询参数
* @return 查询结果
*/
public Object getData(Map paramMap) {
String getDataUrl = SaSsoManager.getConfig().splicingGetDataUrl();
return getData(getDataUrl, paramMap);
}
/**
* 根据自定义 path 地址,查询数据 (此方法需要配置 sa-token.sso.server-url 地址)
* @param path 自定义 path
* @param paramMap 查询参数
* @return 查询结果
*/
public Object getData(String path, Map paramMap) {
String url = buildCustomPathUrl(path, paramMap);
return SaSsoManager.getConfig().getSendHttp().apply(url);
}
// ---------------------- 构建URL ----------------------
/**
* 构建URL:Server端 单点登录地址
* @param clientLoginUrl Client端登录地址
* @param back 回调路径
* @return [SSO-Server端-认证地址 ]
*/
public String buildServerAuthUrl(String clientLoginUrl, String back) {
// 服务端认证地址
String serverUrl = SaSsoManager.getConfig().splicingAuthUrl();
// 拼接客户端标识
String client = SaSsoManager.getConfig().getClient();
if(SaFoxUtil.isNotEmpty(client)) {
serverUrl = SaFoxUtil.joinParam(serverUrl, paramName.client, client);
}
// 对back地址编码
back = (back == null ? "" : back);
back = SaFoxUtil.encodeUrl(back);
// 开始拼接 sso 统一认证地址,形如:serverAuthUrl = http://xxx.com?redirectUrl=xxx.com?back=xxx.com
/*
* 部分 Servlet 版本 request.getRequestURL() 返回的 url 带有 query 参数,形如:http://domain.com?id=1,
* 如果不加判断会造成最终生成的 serverAuthUrl 带有双 back 参数 ,这个 if 判断正是为了解决此问题
*/
if( ! clientLoginUrl.contains(paramName.back + "=" + back) ) {
clientLoginUrl = SaFoxUtil.joinParam(clientLoginUrl, paramName.back, back);
}
// 返回
return SaFoxUtil.joinParam(serverUrl, paramName.redirect, clientLoginUrl);
}
/**
* 构建URL:Server端向Client下放ticket的地址
* @param loginId 账号id
* @param client 客户端标识
* @param redirect Client端提供的重定向地址
* @return see note
*/
public String buildRedirectUrl(Object loginId, String client, String redirect) {
// 校验 重定向地址 是否合法
checkRedirectUrl(redirect);
// 删掉 旧Ticket
deleteTicket(getTicketValue(loginId));
// 创建 新Ticket
String ticket = createTicket(loginId, client);
// 构建 授权重定向地址 (Server端 根据此地址向 Client端 下放Ticket)
return SaFoxUtil.joinParam(encodeBackParam(redirect), paramName.ticket, ticket);
}
/**
* 对url中的back参数进行URL编码, 解决超链接重定向后参数丢失的bug
* @param url url
* @return 编码过后的url
*/
public String encodeBackParam(String url) {
// 获取back参数所在位置
int index = url.indexOf("?" + paramName.back + "=");
if(index == -1) {
index = url.indexOf("&" + paramName.back + "=");
if(index == -1) {
return url;
}
}
// 开始编码
int length = paramName.back.length() + 2;
String back = url.substring(index + length);
back = SaFoxUtil.encodeUrl(back);
// 放回url中
url = url.substring(0, index + length) + back;
return url;
}
/**
* 构建URL:校验ticket的URL
* 在模式三下,Client端拿到Ticket后根据此地址向Server端发送请求,获取账号id
* @param ticket ticket码
* @param ssoLogoutCallUrl 单点注销时的回调URL
* @return 构建完毕的URL
*/
public String buildCheckTicketUrl(String ticket, String ssoLogoutCallUrl) {
// 裸地址
String url = SaSsoManager.getConfig().splicingCheckTicketUrl();
// 拼接 client 参数
String client = getSsoConfig().getClient();
if(SaFoxUtil.isNotEmpty(client)) {
url = SaFoxUtil.joinParam(url, paramName.client, client);
}
// 拼接ticket参数
url = SaFoxUtil.joinParam(url, paramName.ticket, ticket);
// 拼接单点注销时的回调URL
if(ssoLogoutCallUrl != null) {
url = SaFoxUtil.joinParam(url, paramName.ssoLogoutCall, ssoLogoutCallUrl);
}
// 返回
return url;
}
/**
* 构建URL:单点注销URL
* @param loginId 要注销的账号id
* @return 单点注销URL
*/
public String buildSloUrl(Object loginId) {
String url = SaSsoManager.getConfig().splicingSloUrl();
return joinLoginIdAndSign(url, loginId);
}
/**
* 构建URL:Server端 getData 地址,带签名等参数
* @param paramMap 查询参数
* @return /
*/
public String buildGetDataUrl(Map paramMap) {
String getDataUrl = SaSsoManager.getConfig().getGetDataUrl();
return buildCustomPathUrl(getDataUrl, paramMap);
}
/**
* 构建URL:Server 端自定义 path 地址,带签名等参数 (此方法需要配置 sa-token.sso.server-url 地址)
* @param paramMap 请求参数
* @return /
*/
public String buildCustomPathUrl(String path, Map paramMap) {
// 如果path不是以 http 开头,那么就拼接上 serverUrl
String url = path;
if( ! url.startsWith("http") ) {
String serverUrl = SaSsoManager.getConfig().getServerUrl();
SaSsoException.throwByNull(serverUrl, "请先配置 sa-token.sso.server-url 地址", SaSsoErrorCode.CODE_30012);
url = SaFoxUtil.spliceTwoUrl(serverUrl, path);
}
// 添加签名等参数,并序列化
return joinParamMapAndSign(url, paramMap);
}
// ------------------- 发起请求 -------------------
/**
* 发出请求,并返回 SaResult 结果
* @param url 请求地址
* @return 返回的结果
*/
public SaResult request(String url) {
String body = SaSsoManager.getConfig().getSendHttp().apply(url);
Map map = SaManager.getSaJsonTemplate().parseJsonToMap(body);
return new SaResult(map);
}
/**
* 给 paramMap 追加 sign 等参数,并序列化为kv字符串,拼接到url后面
* @param url 请求地址
* @param paramMap 请求原始参数列表
* @return 加工后的url
*/
public String joinParamMapAndSign(String url, Map paramMap) {
// 在参数列表中追加:时间戳、随机字符串、参数签名
SaManager.getSaSignTemplate().addSignParams(paramMap);
// 将参数列表序列化为kv字符串
String signParams = SaManager.getSaSignTemplate().joinParams(paramMap);
// 将kv字符串拼接到url后面
return SaFoxUtil.joinParam(url, signParams);
}
/**
* 给 url 拼接 loginId 参数,并拼接 sign 等参数
* @param url 链接
* @param loginId 账号id
* @return 加工后的url
*/
public String joinLoginIdAndSign(String url, Object loginId) {
Map paramMap = new LinkedHashMap<>();
paramMap.put(paramName.loginId, loginId);
return joinParamMapAndSign(url, paramMap);
}
// ------------------- 返回相应key -------------------
/**
* 拼接key:Ticket 查 账号Id
* @param ticket ticket值
* @return key
*/
public String splicingTicketSaveKey(String ticket) {
return getStpLogic().getConfigOrGlobal().getTokenName() + ":ticket:" + ticket;
}
/**
* 拼接key:Ticket 查 所属的 client
* @param ticket ticket值
* @return key
*/
public String splicingTicketToClientSaveKey(String ticket) {
return getStpLogic().getConfigOrGlobal().getTokenName() + ":ticket-client:" + ticket;
}
/**
* 拼接key:账号Id 反查 Ticket
* @param id 账号id
* @return key
*/
public String splicingTicketIndexKey(Object id) {
return getStpLogic().getConfigOrGlobal().getTokenName() + ":id-ticket:" + id;
}
// -------- 以下方法已废弃,仅为兼容旧版本而保留 --------
/**
* 构建URL:Server端 账号资料查询地址
* @param loginId 账号id
* @return Server端 账号资料查询地址
*/
@Deprecated
public String buildUserinfoUrl(Object loginId) {
String userinfoUrl = SaSsoManager.getConfig().splicingUserinfoUrl();
return joinLoginIdAndSign(userinfoUrl, loginId);
}
/**
* 获取:账号资料
* @param loginId 账号id
* @return 账号资料
*/
@Deprecated
public Object getUserinfo(Object loginId) {
String url = buildUserinfoUrl(loginId);
return request(url);
}
}