All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.dev33.satoken.sso.SaSsoTemplate Maven / Gradle / Ivy

There is a newer version: 1.39.0
Show newest version
/*
 * 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); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy