mirror of https://gitee.com/topiam/eiam
⚡ 增加CAS协议支持
parent
72b36178dd
commit
16bed0aa80
|
@ -17,9 +17,14 @@
|
|||
*/
|
||||
package cn.topiam.employee.application.cas;
|
||||
|
||||
import cn.topiam.employee.application.ApplicationService;
|
||||
import cn.topiam.employee.common.repository.app.AppCertRepository;
|
||||
import cn.topiam.employee.common.repository.app.AppRepository;
|
||||
import cn.topiam.employee.application.AbstractApplicationService;
|
||||
import cn.topiam.employee.application.CasApplicationService;
|
||||
import cn.topiam.employee.common.entity.app.po.AppCasConfigPO;
|
||||
import cn.topiam.employee.common.repository.app.*;
|
||||
import cn.topiam.employee.core.protocol.CasSsoModel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* CAS 应用配置
|
||||
|
@ -27,20 +32,52 @@ import cn.topiam.employee.common.repository.app.AppRepository;
|
|||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/8/23 20:58
|
||||
*/
|
||||
public abstract class AbstractCasApplicationService implements ApplicationService {
|
||||
public abstract class AbstractCasApplicationService extends AbstractApplicationService
|
||||
implements CasApplicationService {
|
||||
|
||||
private static final Logger logger = LoggerFactory
|
||||
.getLogger(AbstractCasApplicationService.class);
|
||||
|
||||
/**
|
||||
* AppCertRepository
|
||||
*/
|
||||
protected final AppCertRepository appCertRepository;
|
||||
/**
|
||||
* ApplicationRepository
|
||||
*/
|
||||
protected final AppRepository appRepository;
|
||||
protected final AppRepository appRepository;
|
||||
|
||||
protected final AppCasConfigRepository appCasConfigRepository;
|
||||
|
||||
protected AbstractCasApplicationService(AppCertRepository appCertRepository,
|
||||
AppRepository appRepository) {
|
||||
this.appCertRepository = appCertRepository;
|
||||
AppAccountRepository appAccountRepository,
|
||||
AppAccessPolicyRepository appAccessPolicyRepository,
|
||||
AppRepository appRepository,
|
||||
AppCasConfigRepository appCasConfigRepository) {
|
||||
super(appCertRepository, appAccountRepository, appAccessPolicyRepository, appRepository);
|
||||
this.appRepository = appRepository;
|
||||
this.appCasConfigRepository = appCasConfigRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CasSsoModel getSsoModel(Long appId) {
|
||||
AppCasConfigPO appCasConfigPO = appCasConfigRepository.getByAppId(appId);
|
||||
return CasSsoModel.builder().ssoCallbackUrl(appCasConfigPO.getSpCallbackUrl()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除应用
|
||||
*
|
||||
* @param appId {@link String} 应用ID
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(String appId) {
|
||||
//删除应用
|
||||
appRepository.deleteById(Long.valueOf(appId));
|
||||
//删除证书
|
||||
appCertRepository.deleteByAppId(Long.valueOf(appId));
|
||||
//删除应用账户
|
||||
appAccountRepository.deleteAllByAppId(Long.valueOf(appId));
|
||||
//删除应用权限策略
|
||||
appAccessPolicyRepository.deleteAllByAppId(Long.valueOf(appId));
|
||||
//删除配置
|
||||
appCasConfigRepository.deleteByAppId(Long.valueOf(appId));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,15 +17,34 @@
|
|||
*/
|
||||
package cn.topiam.employee.application.cas;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import cn.topiam.employee.application.cas.model.AppCasStandardConfigGetResult;
|
||||
import cn.topiam.employee.application.cas.model.AppCasStandardSaveConfigParam;
|
||||
import cn.topiam.employee.application.exception.AppNotExistException;
|
||||
import cn.topiam.employee.audit.context.AuditContext;
|
||||
import cn.topiam.employee.common.constants.ProtocolConstants;
|
||||
import cn.topiam.employee.common.entity.app.AppCasConfigEntity;
|
||||
import cn.topiam.employee.common.entity.app.AppEntity;
|
||||
import cn.topiam.employee.common.entity.app.po.AppCasConfigPO;
|
||||
import cn.topiam.employee.common.enums.app.AppProtocol;
|
||||
import cn.topiam.employee.common.enums.app.AppType;
|
||||
import cn.topiam.employee.common.repository.app.AppCertRepository;
|
||||
import cn.topiam.employee.common.repository.app.AppRepository;
|
||||
import cn.topiam.employee.common.enums.app.AuthorizationType;
|
||||
import cn.topiam.employee.common.enums.app.InitLoginType;
|
||||
import cn.topiam.employee.common.repository.app.*;
|
||||
import cn.topiam.employee.core.context.ServerContextHelp;
|
||||
import cn.topiam.employee.support.exception.TopIamException;
|
||||
import cn.topiam.employee.support.validation.ValidationHelp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.validation.ConstraintViolationException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static cn.topiam.employee.common.constants.ProtocolConstants.APP_CODE_VARIABLE;
|
||||
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
|
||||
|
||||
/**
|
||||
* Cas 用户应用
|
||||
|
@ -35,15 +54,72 @@ import cn.topiam.employee.common.repository.app.AppRepository;
|
|||
*/
|
||||
@Component
|
||||
public class CasStandardApplicationServiceImpl extends AbstractCasApplicationService {
|
||||
private final Logger logger = LoggerFactory
|
||||
.getLogger(CasStandardApplicationServiceImpl.class);
|
||||
|
||||
/**
|
||||
* AppCasConfigRepository
|
||||
*/
|
||||
protected final AppCasConfigRepository appCasConfigRepository;
|
||||
|
||||
public CasStandardApplicationServiceImpl(AppCertRepository appCertRepository,
|
||||
AppAccountRepository appAccountRepository,
|
||||
AppAccessPolicyRepository appAccessPolicyRepository,
|
||||
AppRepository appRepository,
|
||||
AppCasConfigRepository appCasConfigRepository) {
|
||||
super(appCertRepository, appAccountRepository, appAccessPolicyRepository, appRepository,
|
||||
appCasConfigRepository);
|
||||
this.appCasConfigRepository = appCasConfigRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新应用配置
|
||||
*
|
||||
* @param appId {@link String}
|
||||
* @param appId {@link String}
|
||||
* @param config {@link Map}
|
||||
*/
|
||||
@Override
|
||||
public void saveConfig(String appId, Map<String, Object> config) {
|
||||
AppCasStandardSaveConfigParam model;
|
||||
try {
|
||||
String value = mapper.writeValueAsString(config);
|
||||
// 指定序列化输入的类型
|
||||
mapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
model = mapper.readValue(value, AppCasStandardSaveConfigParam.class);
|
||||
} catch (Exception e) {
|
||||
throw new TopIamException(e.getMessage());
|
||||
}
|
||||
|
||||
ValidationHelp.ValidationResult<AppCasStandardSaveConfigParam> validationResult = ValidationHelp
|
||||
.validateEntity(model);
|
||||
if (validationResult.isHasErrors()) {
|
||||
throw new ConstraintViolationException(validationResult.getConstraintViolations());
|
||||
}
|
||||
|
||||
//1、修改基本信息
|
||||
Optional<AppEntity> optional = appRepository.findById(Long.valueOf(appId));
|
||||
if (optional.isEmpty()) {
|
||||
AuditContext.setContent("保存配置失败,应用 [" + appId + "] 不存在!");
|
||||
logger.error(AuditContext.getContent());
|
||||
throw new AppNotExistException();
|
||||
}
|
||||
AppEntity appEntity = optional.get();
|
||||
appEntity.setAuthorizationType(model.getAuthorizationType());
|
||||
appEntity.setInitLoginUrl(model.getInitLoginUrl());
|
||||
appEntity.setInitLoginType(model.getInitLoginType());
|
||||
appRepository.save(appEntity);
|
||||
|
||||
//2、修改cas配置
|
||||
Optional<AppCasConfigEntity> cas = appCasConfigRepository.findByAppId(Long.valueOf(appId));
|
||||
if (cas.isEmpty()) {
|
||||
AuditContext.setContent("保存配置失败,应用 [" + appId + "] 不存在!");
|
||||
logger.error(AuditContext.getContent());
|
||||
throw new AppNotExistException();
|
||||
}
|
||||
AppCasConfigEntity entity = cas.get();
|
||||
entity.setSpCallbackUrl(model.getSpCallbackUrl());
|
||||
appCasConfigRepository.save(entity);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,7 +130,19 @@ public class CasStandardApplicationServiceImpl extends AbstractCasApplicationSer
|
|||
*/
|
||||
@Override
|
||||
public Object getConfig(String appId) {
|
||||
return null;
|
||||
AppCasConfigPO po = appCasConfigRepository.getByAppId(Long.valueOf(appId));
|
||||
AppCasStandardConfigGetResult result = new AppCasStandardConfigGetResult();
|
||||
result.setAuthorizationType(po.getAuthorizationType());
|
||||
result.setInitLoginType(po.getInitLoginType());
|
||||
result.setInitLoginUrl(po.getInitLoginUrl());
|
||||
result.setSpCallbackUrl(po.getSpCallbackUrl());
|
||||
|
||||
String baseUrl = ServerContextHelp.getPortalPublicBaseUrl();
|
||||
// 服务端URL配置前缀
|
||||
result.setServerUrlPrefix(
|
||||
baseUrl + ProtocolConstants.CasEndpointConstants.CAS_AUTHORIZE_BASE_PATH
|
||||
.replace(APP_CODE_VARIABLE, po.getAppCode()));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -114,7 +202,7 @@ public class CasStandardApplicationServiceImpl extends AbstractCasApplicationSer
|
|||
*/
|
||||
@Override
|
||||
public List<Map> getFormSchema() {
|
||||
return null;
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -135,21 +223,27 @@ public class CasStandardApplicationServiceImpl extends AbstractCasApplicationSer
|
|||
*/
|
||||
@Override
|
||||
public String create(String name, String remark) {
|
||||
return "";
|
||||
//1、创建基础信息
|
||||
AppEntity appEntity = new AppEntity();
|
||||
appEntity.setName(name);
|
||||
appEntity.setCode(
|
||||
org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric(32).toLowerCase());
|
||||
appEntity.setTemplate(getCode());
|
||||
appEntity.setType(AppType.STANDARD);
|
||||
appEntity.setEnabled(true);
|
||||
appEntity.setProtocol(getProtocol());
|
||||
appEntity.setClientId(idGenerator.generateId().toString().replace("-", ""));
|
||||
appEntity.setClientSecret(idGenerator.generateId().toString().replace("-", ""));
|
||||
appEntity.setInitLoginType(InitLoginType.PORTAL_OR_APP);
|
||||
appEntity.setAuthorizationType(AuthorizationType.AUTHORIZATION);
|
||||
appEntity.setRemark(remark);
|
||||
appRepository.save(appEntity);
|
||||
|
||||
AppCasConfigEntity casEntity = new AppCasConfigEntity();
|
||||
casEntity.setAppId(appEntity.getId());
|
||||
casEntity.setSpCallbackUrl("");
|
||||
appCasConfigRepository.save(casEntity);
|
||||
return appEntity.getId().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除应用
|
||||
*
|
||||
* @param appId {@link String} 应用ID
|
||||
*/
|
||||
@Override
|
||||
public void delete(String appId) {
|
||||
|
||||
}
|
||||
|
||||
protected CasStandardApplicationServiceImpl(AppCertRepository appCertRepository,
|
||||
AppRepository appRepository) {
|
||||
super(appCertRepository, appRepository);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package cn.topiam.employee.application.cas.converter;
|
||||
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
/**
|
||||
* 配置转换
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 17:31
|
||||
*/
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface AppCasStandardConfigConverter {
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package cn.topiam.employee.application.cas.model;
|
||||
|
||||
import cn.topiam.employee.common.enums.app.AuthorizationType;
|
||||
import cn.topiam.employee.common.enums.app.InitLoginType;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/1/2 22:23
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "CAS 配置返回结果")
|
||||
public class AppCasStandardConfigGetResult {
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
@Schema(description = "授权类型")
|
||||
private AuthorizationType authorizationType;
|
||||
|
||||
/**
|
||||
* SSO 发起登录类型
|
||||
*/
|
||||
@Schema(description = "SSO 发起登录类型")
|
||||
private InitLoginType initLoginType;
|
||||
|
||||
/**
|
||||
* SSO 发起登录URL
|
||||
*/
|
||||
@Schema(description = "SSO 发起登录URL")
|
||||
private String initLoginUrl;
|
||||
|
||||
/**
|
||||
* 单点登录 SP 回调地址
|
||||
*/
|
||||
@Parameter(name = "单点登录 sp Callback Url")
|
||||
private String spCallbackUrl;
|
||||
|
||||
/**
|
||||
* Server端配置前缀
|
||||
*/
|
||||
private String serverUrlPrefix;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package cn.topiam.employee.application.cas.model;
|
||||
|
||||
import cn.topiam.employee.common.enums.app.AuthorizationType;
|
||||
import cn.topiam.employee.common.enums.app.InitLoginType;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/1/2 22:27
|
||||
*/
|
||||
@Data
|
||||
public class AppCasStandardSaveConfigParam implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1881187724713984421L;
|
||||
|
||||
/**
|
||||
* 应用ID
|
||||
*/
|
||||
@Schema(description = "授权类型")
|
||||
private AuthorizationType authorizationType;
|
||||
|
||||
/**
|
||||
* SSO 发起登录类型
|
||||
*/
|
||||
@Schema(description = "SSO 发起登录类型")
|
||||
private InitLoginType initLoginType;
|
||||
|
||||
/**
|
||||
* SSO 发起登录URL
|
||||
*/
|
||||
@Schema(description = "SSO 发起登录URL")
|
||||
private String initLoginUrl;
|
||||
|
||||
/**
|
||||
* 单点登录 SP 回调地址
|
||||
*/
|
||||
@Parameter(name = "单点登录 sp Callback Url")
|
||||
private String spCallbackUrl;
|
||||
}
|
|
@ -17,15 +17,6 @@
|
|||
*/
|
||||
package cn.topiam.employee.application;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import cn.topiam.employee.common.entity.app.AppCertEntity;
|
||||
import cn.topiam.employee.common.enums.app.AppCertUsingType;
|
||||
import cn.topiam.employee.common.repository.app.AppAccessPolicyRepository;
|
||||
|
@ -35,6 +26,18 @@ import cn.topiam.employee.common.repository.app.AppRepository;
|
|||
import cn.topiam.employee.support.exception.TopIamException;
|
||||
import cn.topiam.employee.support.util.CertUtils;
|
||||
import cn.topiam.employee.support.util.RsaUtils;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.AlternativeJdkIdGenerator;
|
||||
import org.springframework.util.IdGenerator;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
|
||||
import static cn.topiam.employee.support.util.CertUtils.encodePem;
|
||||
import static cn.topiam.employee.support.util.CertUtils.getX500Name;
|
||||
import static cn.topiam.employee.support.util.RsaUtils.getKeys;
|
||||
|
@ -46,13 +49,14 @@ import static cn.topiam.employee.support.util.RsaUtils.getKeys;
|
|||
* Created by support@topiam.cn on 2022/8/31 22:34
|
||||
*/
|
||||
public abstract class AbstractApplicationService implements ApplicationService {
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractApplicationService.class);
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractApplicationService.class);
|
||||
protected final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 创建证书
|
||||
*
|
||||
* @param appId {@link Long}
|
||||
* @param appCode {@link Long}
|
||||
* @param appCode {@link Long}
|
||||
* @param usingType {@link AppCertUsingType}
|
||||
*/
|
||||
public void createCertificate(Long appId, String appCode, AppCertUsingType usingType) {
|
||||
|
@ -115,7 +119,7 @@ public abstract class AbstractApplicationService implements ApplicationService {
|
|||
protected final AppAccountRepository appAccountRepository;
|
||||
|
||||
/**
|
||||
*AppAccessPolicyRepository
|
||||
* AppAccessPolicyRepository
|
||||
*/
|
||||
protected final AppAccessPolicyRepository appAccessPolicyRepository;
|
||||
|
||||
|
@ -124,6 +128,11 @@ public abstract class AbstractApplicationService implements ApplicationService {
|
|||
*/
|
||||
protected final AppRepository appRepository;
|
||||
|
||||
/**
|
||||
* IdGenerator
|
||||
*/
|
||||
protected final IdGenerator idGenerator;
|
||||
|
||||
protected AbstractApplicationService(AppCertRepository appCertRepository,
|
||||
AppAccountRepository appAccountRepository,
|
||||
AppAccessPolicyRepository appAccessPolicyRepository,
|
||||
|
@ -132,5 +141,6 @@ public abstract class AbstractApplicationService implements ApplicationService {
|
|||
this.appAccountRepository = appAccountRepository;
|
||||
this.appAccessPolicyRepository = appAccessPolicyRepository;
|
||||
this.appRepository = appRepository;
|
||||
this.idGenerator = new AlternativeJdkIdGenerator();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package cn.topiam.employee.application;
|
||||
|
||||
import cn.topiam.employee.core.protocol.CasSsoModel;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/1/2 11:50
|
||||
*/
|
||||
public interface CasApplicationService extends ApplicationService {
|
||||
|
||||
/**
|
||||
* 获取SSO Modal
|
||||
*
|
||||
* @param appId {@link String}
|
||||
* @return {@link CasSsoModel}
|
||||
*/
|
||||
CasSsoModel getSsoModel(Long appId);
|
||||
}
|
|
@ -26,9 +26,7 @@ import org.mapstruct.Mapping;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.AlternativeJdkIdGenerator;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.IdGenerator;
|
||||
|
||||
import cn.topiam.employee.application.AbstractApplicationService;
|
||||
import cn.topiam.employee.application.Saml2ApplicationService;
|
||||
|
@ -136,11 +134,6 @@ public abstract class AbstractSamlAppService extends AbstractApplicationService
|
|||
*/
|
||||
protected final AppSaml2ConfigRepository appSaml2ConfigRepository;
|
||||
|
||||
/**
|
||||
* IdGenerator
|
||||
*/
|
||||
protected final IdGenerator idGenerator;
|
||||
|
||||
protected AbstractSamlAppService(AppCertRepository appCertRepository,
|
||||
AppAccountRepository appAccountRepository,
|
||||
AppAccessPolicyRepository appAccessPolicyRepository,
|
||||
|
@ -148,7 +141,6 @@ public abstract class AbstractSamlAppService extends AbstractApplicationService
|
|||
AppSaml2ConfigRepository appSaml2ConfigRepository) {
|
||||
super(appCertRepository, appAccountRepository, appAccessPolicyRepository, appRepository);
|
||||
this.appSaml2ConfigRepository = appSaml2ConfigRepository;
|
||||
this.idGenerator = new AlternativeJdkIdGenerator();
|
||||
}
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
|
|
|
@ -71,7 +71,6 @@ public class Saml2StandardApplicationServiceImpl extends AbstractSamlAppService
|
|||
public void saveConfig(String appId, Map<String, Object> config) {
|
||||
AppSaml2StandardSaveConfigParam model;
|
||||
try {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String value = mapper.writeValueAsString(config);
|
||||
// 指定序列化输入的类型
|
||||
mapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
|
|
@ -35,6 +35,7 @@ public final class ConfigBeanNameConstants {
|
|||
public static final String SOCIAL_SECURITY_FILTER_CHAIN = "socialSecurityFilterChain";
|
||||
public static final String SAML2_PROTOCOL_SECURITY_FILTER_CHAIN = "saml2ProtocolSecurityFilterChain";
|
||||
public static final String OIDC_PROTOCOL_SECURITY_FILTER_CHAIN = "oidcProtocolSecurityFilterChain";
|
||||
public static final String CAS_PROTOCOL_SECURITY_FILTER_CHAIN = "casProtocolSecurityFilterChain";
|
||||
|
||||
/**
|
||||
* 默认密码策略管理器
|
||||
|
|
|
@ -18,10 +18,10 @@
|
|||
package cn.topiam.employee.common.constants;
|
||||
|
||||
import lombok.Data;
|
||||
import static com.nimbusds.openid.connect.sdk.op.OIDCProviderConfigurationRequest.OPENID_PROVIDER_WELL_KNOWN_PATH;
|
||||
|
||||
import static cn.topiam.employee.common.constants.AppConstants.APP_CACHE_NAME_PREFIX;
|
||||
import static cn.topiam.employee.common.constants.AuthorizeConstants.AUTHORIZE_PATH;
|
||||
import static com.nimbusds.openid.connect.sdk.op.OIDCProviderConfigurationRequest.OPENID_PROVIDER_WELL_KNOWN_PATH;
|
||||
|
||||
/**
|
||||
* Saml 常量
|
||||
|
@ -49,6 +49,11 @@ public final class ProtocolConstants {
|
|||
*/
|
||||
public static final String SAML2_CONFIG_CACHE_NAME = APP_CACHE_NAME_PREFIX + "saml";
|
||||
|
||||
/**
|
||||
* CAS 配置缓存名称
|
||||
*/
|
||||
public static final String CAS_CONFIG_CACHE_NAME = APP_CACHE_NAME_PREFIX + "cas";
|
||||
|
||||
/**
|
||||
* OIDC 配置缓存名称
|
||||
*/
|
||||
|
@ -70,19 +75,19 @@ public final class ProtocolConstants {
|
|||
*/
|
||||
public final static String OIDC_AUTHORIZE_BASE_PATH = AUTHORIZE_PATH + "/" + APP_CODE_VARIABLE;
|
||||
|
||||
public final static String OIDC_AUTHORIZE_PATH = OIDC_AUTHORIZE_BASE_PATH +"/oidc";
|
||||
public final static String OIDC_AUTHORIZE_PATH = OIDC_AUTHORIZE_BASE_PATH + "/oidc";
|
||||
|
||||
public final static String OAUTH2_AUTHORIZE_PATH = OIDC_AUTHORIZE_BASE_PATH +"/oauth2";
|
||||
public final static String OAUTH2_AUTHORIZE_PATH = OIDC_AUTHORIZE_BASE_PATH + "/oauth2";
|
||||
|
||||
/**
|
||||
* OpenID Provider metadata.
|
||||
*/
|
||||
public static final String WELL_KNOWN_OPENID_CONFIGURATION = OIDC_AUTHORIZE_PATH +OPENID_PROVIDER_WELL_KNOWN_PATH;
|
||||
public static final String WELL_KNOWN_OPENID_CONFIGURATION = OIDC_AUTHORIZE_PATH + OPENID_PROVIDER_WELL_KNOWN_PATH;
|
||||
|
||||
/**
|
||||
* Jwk Set Endpoint
|
||||
*/
|
||||
public static final String JWK_SET_ENDPOINT = OIDC_AUTHORIZE_PATH + "/jwks";
|
||||
public static final String JWK_SET_ENDPOINT = OIDC_AUTHORIZE_PATH + "/jwks";
|
||||
|
||||
/**
|
||||
* OIDC Client Registration Endpoint
|
||||
|
@ -92,27 +97,27 @@ public final class ProtocolConstants {
|
|||
/**
|
||||
* Authorization Endpoint
|
||||
*/
|
||||
public static final String AUTHORIZATION_ENDPOINT = OAUTH2_AUTHORIZE_PATH + "/auth";
|
||||
public static final String AUTHORIZATION_ENDPOINT = OAUTH2_AUTHORIZE_PATH + "/auth";
|
||||
|
||||
/**
|
||||
* Token Endpoint
|
||||
*/
|
||||
public static final String TOKEN_ENDPOINT = OAUTH2_AUTHORIZE_PATH + "/token";
|
||||
public static final String TOKEN_ENDPOINT = OAUTH2_AUTHORIZE_PATH + "/token";
|
||||
|
||||
/**
|
||||
* Jwk Revocation Endpoint
|
||||
*/
|
||||
public static final String TOKEN_REVOCATION_ENDPOINT = OAUTH2_AUTHORIZE_PATH + "/revoke";
|
||||
public static final String TOKEN_REVOCATION_ENDPOINT = OAUTH2_AUTHORIZE_PATH + "/revoke";
|
||||
|
||||
/**
|
||||
* Token Introspection Endpoint
|
||||
*/
|
||||
public static final String TOKEN_INTROSPECTION_ENDPOINT = OAUTH2_AUTHORIZE_PATH + "/introspect";
|
||||
public static final String TOKEN_INTROSPECTION_ENDPOINT = OAUTH2_AUTHORIZE_PATH + "/introspect";
|
||||
|
||||
/**
|
||||
* OIDC User Info Endpoint
|
||||
*/
|
||||
public static final String OIDC_USER_INFO_ENDPOINT = OAUTH2_AUTHORIZE_PATH + "/userinfo";
|
||||
public static final String OIDC_USER_INFO_ENDPOINT = OAUTH2_AUTHORIZE_PATH + "/userinfo";
|
||||
|
||||
//@formatter:on
|
||||
}
|
||||
|
@ -144,4 +149,27 @@ public final class ProtocolConstants {
|
|||
public static final String SAML_SSO_PATH = SAML2_AUTHORIZE_BASE_PATH + "/sso";
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CasEndpointConstants {
|
||||
/**
|
||||
* cas 根路径
|
||||
*/
|
||||
public final static String CAS_AUTHORIZE_BASE_PATH = AUTHORIZE_PATH + "/cas/"
|
||||
+ APP_CODE_VARIABLE;
|
||||
/*
|
||||
* cas 登陆地址
|
||||
*/
|
||||
public final static String CAS_LOGIN_PATH = CAS_AUTHORIZE_BASE_PATH + "/login";
|
||||
/*
|
||||
* cas ticket校验地址
|
||||
*/
|
||||
public final static String CAS_VALIDATE_PATH = CAS_AUTHORIZE_BASE_PATH + "/validate";
|
||||
|
||||
public final static String CAS_VALIDATE_V2_PATH = CAS_AUTHORIZE_BASE_PATH
|
||||
+ "/serviceValidate";
|
||||
|
||||
public final static String CAS_VALIDATE_V3_PATH = CAS_AUTHORIZE_BASE_PATH
|
||||
+ "/p3/serviceValidate";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* eiam-common - Employee Identity and Access Management Program
|
||||
* Copyright © 2020-2022 TopIAM (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.common.entity.app;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import org.hibernate.annotations.TypeDef;
|
||||
|
||||
import com.vladmihalcea.hibernate.type.json.JsonStringType;
|
||||
|
||||
import cn.topiam.employee.support.repository.domain.BaseEntity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* APP CAS 配置
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/30 22:31
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@Entity
|
||||
@Accessors(chain = true)
|
||||
@Table(name = "app_cas_config")
|
||||
@TypeDef(name = "json", typeClass = JsonStringType.class)
|
||||
public class AppCasConfigEntity extends BaseEntity<Long> {
|
||||
/**
|
||||
* APP ID
|
||||
*/
|
||||
@Column(name = "app_id")
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* SP 接受回调地址
|
||||
*/
|
||||
@Column(name = "sp_callback_url")
|
||||
private String spCallbackUrl;
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* eiam-common - Employee Identity and Access Management Program
|
||||
* Copyright © 2020-2022 TopIAM (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.common.entity.app.po;
|
||||
|
||||
import cn.topiam.employee.common.entity.app.AppCasConfigEntity;
|
||||
import cn.topiam.employee.common.enums.app.AuthorizationType;
|
||||
import cn.topiam.employee.common.enums.app.InitLoginType;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* CAS 配置 po
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 22:46
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AppCasConfigPO extends AppCasConfigEntity {
|
||||
/**
|
||||
* APP CODE
|
||||
*/
|
||||
private String appCode;
|
||||
|
||||
/**
|
||||
* 模版
|
||||
*/
|
||||
private String appTemplate;
|
||||
|
||||
/**
|
||||
* 客户端ID
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 客户端秘钥
|
||||
*/
|
||||
private String clientSecret;
|
||||
|
||||
/**
|
||||
* SSO 发起方
|
||||
*/
|
||||
private InitLoginType initLoginType;
|
||||
|
||||
/**
|
||||
* SSO 登录链接
|
||||
*/
|
||||
private String initLoginUrl;
|
||||
|
||||
/**
|
||||
* 授权范围
|
||||
*/
|
||||
private AuthorizationType authorizationType;
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* eiam-common - Employee Identity and Access Management Program
|
||||
* Copyright © 2020-2022 TopIAM (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.common.repository.app;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.cache.annotation.CacheConfig;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import cn.topiam.employee.common.entity.app.AppCasConfigEntity;
|
||||
import static cn.topiam.employee.common.constants.ProtocolConstants.CAS_CONFIG_CACHE_NAME;
|
||||
|
||||
/**
|
||||
* AppCasConfigRepository
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 15:26
|
||||
*/
|
||||
@Repository
|
||||
@CacheConfig(cacheNames = { CAS_CONFIG_CACHE_NAME })
|
||||
public interface AppCasConfigRepository extends JpaRepository<AppCasConfigEntity, Long>,
|
||||
QuerydslPredicateExecutor<AppCasConfigEntity>,
|
||||
AppCasConfigRepositoryCustomized {
|
||||
|
||||
/**
|
||||
* 按应用 ID 删除
|
||||
*
|
||||
* @param appId {@link Long}
|
||||
*/
|
||||
@CacheEvict(allEntries = true)
|
||||
void deleteByAppId(Long appId);
|
||||
|
||||
/**
|
||||
* delete
|
||||
*
|
||||
* @param id must not be {@literal null}.
|
||||
*/
|
||||
@CacheEvict(allEntries = true)
|
||||
@Override
|
||||
void deleteById(@NotNull Long id);
|
||||
|
||||
/**
|
||||
* save
|
||||
*
|
||||
* @param entity must not be {@literal null}.
|
||||
* @param <S> {@link S}
|
||||
* @return {@link AppCasConfigEntity}
|
||||
*/
|
||||
@NotNull
|
||||
@Override
|
||||
@CacheEvict(allEntries = true)
|
||||
<S extends AppCasConfigEntity> S save(@NotNull S entity);
|
||||
|
||||
/**
|
||||
* 根据应用ID获取配置
|
||||
*
|
||||
* @param appId {@link Long}
|
||||
* @return {@link AppCasConfigEntity}
|
||||
*/
|
||||
Optional<AppCasConfigEntity> findByAppId(Long appId);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* eiam-common - Employee Identity and Access Management Program
|
||||
* Copyright © 2020-2022 TopIAM (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.common.repository.app;
|
||||
|
||||
import cn.topiam.employee.common.entity.app.po.AppCasConfigPO;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 15:28
|
||||
*/
|
||||
public interface AppCasConfigRepositoryCustomized {
|
||||
/**
|
||||
* 根据应用ID获取
|
||||
*
|
||||
* @param appId {@link Long}
|
||||
* @return {@link AppCasConfigPO}
|
||||
*/
|
||||
AppCasConfigPO getByAppId(Long appId);
|
||||
|
||||
/**
|
||||
* 根据应用code获取应用
|
||||
*
|
||||
* @param appCode {@link String}
|
||||
* @return {@link AppCasConfigPO}
|
||||
*/
|
||||
AppCasConfigPO findByAppCode(String appCode);
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* eiam-common - Employee Identity and Access Management Program
|
||||
* Copyright © 2020-2022 TopIAM (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.common.repository.app.impl;
|
||||
|
||||
import org.springframework.cache.annotation.CacheConfig;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import cn.topiam.employee.common.constants.ProtocolConstants;
|
||||
import cn.topiam.employee.common.entity.app.po.AppCasConfigPO;
|
||||
import cn.topiam.employee.common.entity.app.po.AppSaml2ConfigPO;
|
||||
import cn.topiam.employee.common.repository.app.AppCasConfigRepositoryCustomized;
|
||||
import cn.topiam.employee.common.repository.app.impl.mapper.AppCasConfigPoMapper;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* AppCasConfigRepositoryCustomizedImpl
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:37
|
||||
*/
|
||||
|
||||
@Repository
|
||||
@AllArgsConstructor
|
||||
@CacheConfig(cacheNames = { ProtocolConstants.CAS_CONFIG_CACHE_NAME })
|
||||
public class AppCasConfigRepositoryCustomizedImpl implements AppCasConfigRepositoryCustomized {
|
||||
/**
|
||||
* 根据应用ID获取
|
||||
*
|
||||
* @param appId {@link Long}
|
||||
* @return {@link AppSaml2ConfigPO}
|
||||
*/
|
||||
@Override
|
||||
@Cacheable(key = "#p0", unless = "#result==null")
|
||||
public AppCasConfigPO getByAppId(Long appId) {
|
||||
String sql = "select acc.*,app.init_login_url,app.init_login_type,app.authorization_type,app.template_,app.code_,client_id,client_secret from app left join app_cas_config acc on app.id_ = acc.app_id where 1=1"
|
||||
+ " AND app_id = " + appId;
|
||||
return jdbcTemplate.queryForObject(sql, new AppCasConfigPoMapper());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Cacheable(key = "#p0", unless = "#result==null")
|
||||
public AppCasConfigPO findByAppCode(String appCode) {
|
||||
String sql = "select acc.*,app.init_login_url,app.init_login_type,app.authorization_type,app.template_,app.code_,client_id,client_secret from app left join app_cas_config acc on app.id_ = acc.app_id where 1=1"
|
||||
+ " AND code_ = " + "'" + appCode + "'";
|
||||
return jdbcTemplate.queryForObject(sql, new AppCasConfigPoMapper());
|
||||
}
|
||||
|
||||
/**
|
||||
* JdbcTemplate
|
||||
*/
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* eiam-common - Employee Identity and Access Management Program
|
||||
* Copyright © 2020-2022 TopIAM (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.common.repository.app.impl.mapper;
|
||||
|
||||
import cn.topiam.employee.common.entity.app.po.AppCasConfigPO;
|
||||
import cn.topiam.employee.common.enums.app.InitLoginType;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* AppCasConfigPOPOMapper
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:31
|
||||
*/
|
||||
public class AppCasConfigPoMapper implements RowMapper<AppCasConfigPO> {
|
||||
|
||||
@Override
|
||||
public AppCasConfigPO mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
AppCasConfigPO configPO = new AppCasConfigPO();
|
||||
configPO.setAppId(rs.getLong("id_"));
|
||||
configPO.setAppId(rs.getLong("app_id"));
|
||||
configPO.setAppCode(rs.getString("code_"));
|
||||
configPO.setClientId(rs.getString("client_id"));
|
||||
configPO.setClientSecret(rs.getString("client_secret"));
|
||||
configPO.setInitLoginType(InitLoginType.getType(rs.getString("init_login_type")));
|
||||
configPO.setInitLoginUrl(rs.getString("init_login_url"));
|
||||
configPO.setAppTemplate(rs.getString("template_"));
|
||||
configPO.setCreateBy(rs.getString("create_by"));
|
||||
configPO.setCreateTime(rs.getObject("create_time", LocalDateTime.class));
|
||||
configPO.setUpdateBy(rs.getString("update_by"));
|
||||
configPO.setCreateTime(rs.getObject("update_time", LocalDateTime.class));
|
||||
configPO.setRemark(rs.getString("remark_"));
|
||||
configPO.setSpCallbackUrl(rs.getString("sp_callback_url"));
|
||||
return configPO;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package cn.topiam.employee.common.util;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/30 01:06
|
||||
*/
|
||||
public class CasUtils {
|
||||
private final static Logger logger = LoggerFactory.getLogger(CasUtils.class);
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
|
||||
eiam-common - Employee Identity and Access Management Program
|
||||
Copyright © 2020-2022 TopIAM (support@topiam.cn)
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
-->
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd
|
||||
http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/pro/liquibase-pro-4.3.xsd">
|
||||
<!--init-->
|
||||
<changeSet author="TopIAM" id="1653202564123-0">
|
||||
<createTable remarks="CAS 应用配置" tableName="app_cas_config">
|
||||
<column name="id_" type="BIGINT" remarks="主键ID">
|
||||
<constraints nullable="false" primaryKey="true"/>
|
||||
</column>
|
||||
<column name="app_id" remarks="应用ID" type="BIGINT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="sp_callback_url" remarks="客户端接受回调地址" type="VARCHAR(128)"/>
|
||||
<column name="additional_config" remarks="额外配置" type="JSON"/>
|
||||
<column name="create_by" remarks="创建者" type="VARCHAR(64)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column defaultValueComputed="CURRENT_TIMESTAMP" name="create_time" remarks="创建时间" type="datetime">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="update_by" remarks="修改者" type="VARCHAR(64)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column defaultValueComputed="CURRENT_TIMESTAMP" name="update_time" remarks="修改时间" type="datetime">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="remark_" remarks="备注" type="TEXT"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
|
@ -0,0 +1,18 @@
|
|||
package cn.topiam.employee.core.protocol;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/1/2 11:50
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class CasSsoModel implements Serializable {
|
||||
|
||||
private String ssoCallbackUrl;
|
||||
|
||||
}
|
|
@ -105,6 +105,7 @@ import cn.topiam.employee.portal.listener.PortalAuthenticationSuccessEventListen
|
|||
import cn.topiam.employee.portal.listener.PortalLogoutSuccessEventListener;
|
||||
import cn.topiam.employee.portal.listener.PortalSessionInformationExpiredStrategy;
|
||||
import cn.topiam.employee.portal.mfa.MfaAuthenticationConfigurer;
|
||||
import cn.topiam.employee.protocol.cas.idp.CasIdpConfigurer;
|
||||
import cn.topiam.employee.protocol.oidc.authentication.EiamOAuth2AuthorizationService;
|
||||
import cn.topiam.employee.protocol.oidc.repository.OidcConfigRegisteredClientRepository;
|
||||
import cn.topiam.employee.protocol.oidc.token.ApplicationOpaqueTokenIntrospector;
|
||||
|
@ -230,6 +231,36 @@ public class PortalSecurityConfiguration {
|
|||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* CAS 协议
|
||||
*
|
||||
* @param http {@link HttpSecurity}
|
||||
* @return {@link SecurityFilterChain}
|
||||
* @throws Exception Exception
|
||||
*/
|
||||
@Order(3)
|
||||
@Bean(value = CAS_PROTOCOL_SECURITY_FILTER_CHAIN)
|
||||
@RefreshScope
|
||||
public SecurityFilterChain casProtocolSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
CasIdpConfigurer<HttpSecurity> configurer = new CasIdpConfigurer<>();
|
||||
RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();
|
||||
http.requestMatcher(endpointsMatcher)
|
||||
.authorizeHttpRequests(
|
||||
authorizeRequests -> authorizeRequests.anyRequest().authenticated())
|
||||
//异常处理
|
||||
.exceptionHandling(withExceptionConfigurerDefaults())
|
||||
//CSRF
|
||||
.csrf(withCsrfConfigurerDefaults(endpointsMatcher))
|
||||
//headers
|
||||
.headers(withHeadersConfigurerDefaults())
|
||||
//cors
|
||||
.cors(withCorsConfigurerDefaults())
|
||||
//会话管理器
|
||||
.sessionManagement(withSessionManagementConfigurerDefaults(settingRepository))
|
||||
.apply(configurer);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* SAML 协议
|
||||
*
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* eiam-protocol-cas - Employee Identity and Access Management Program
|
||||
* Copyright © 2020-2022 TopIAM (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.protocol.cas.idp;
|
||||
|
||||
import cn.topiam.employee.application.ApplicationServiceLoader;
|
||||
import cn.topiam.employee.common.repository.app.AppCasConfigRepository;
|
||||
import cn.topiam.employee.protocol.cas.idp.auth.CentralAuthenticationService;
|
||||
import cn.topiam.employee.protocol.cas.idp.endpoint.CasIdpSingleSignOnEndpointFilter;
|
||||
import cn.topiam.employee.protocol.cas.idp.endpoint.CasIdpValidateEndpointFilter;
|
||||
import cn.topiam.employee.protocol.cas.idp.filter.CasAuthorizationServerContextFilter;
|
||||
import cn.topiam.employee.protocol.cas.idp.util.CasUtils;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.topiam.employee.protocol.cas.idp.util.CasUtils.*;
|
||||
|
||||
/**
|
||||
* 认证配置
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:23
|
||||
*/
|
||||
public class CasIdpConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
extends AbstractHttpConfigurer<CasIdpConfigurer<B>, B> {
|
||||
|
||||
@Override
|
||||
public void configure(B http) {
|
||||
AppCasConfigRepository appCasConfigRepository = CasUtils.getAppCasConfigRepository(http);
|
||||
ApplicationServiceLoader applicationServiceLoader = getApplicationServiceLoader(http);
|
||||
SessionRegistry sessionRegistry = getSessionRegistry(http);
|
||||
CentralAuthenticationService centralAuthenticationService = getCentralAuthenticationService(
|
||||
http);
|
||||
DocumentBuilder documentBuilder = getDocumentBuilder(http);
|
||||
//CAS 登陆过滤器
|
||||
http.addFilterAfter(new CasIdpSingleSignOnEndpointFilter(applicationServiceLoader,
|
||||
sessionRegistry, centralAuthenticationService),
|
||||
UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
//cas 验证过滤器
|
||||
http.addFilterBefore(
|
||||
new CasIdpValidateEndpointFilter(applicationServiceLoader, sessionRegistry,
|
||||
centralAuthenticationService, documentBuilder),
|
||||
CasIdpSingleSignOnEndpointFilter.class);
|
||||
|
||||
//CAS 授权服务器应用上下文过滤器
|
||||
http.addFilterBefore(
|
||||
new CasAuthorizationServerContextFilter(getEndpointsMatcher(), appCasConfigRepository),
|
||||
CasIdpValidateEndpointFilter.class);
|
||||
}
|
||||
|
||||
public RequestMatcher getEndpointsMatcher() {
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
requestMatchers.add(CasIdpSingleSignOnEndpointFilter.getRequestMatcher());
|
||||
requestMatchers.add(CasIdpValidateEndpointFilter.getRequestMatcher());
|
||||
return new OrRequestMatcher(requestMatchers);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.auth;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 23:47
|
||||
*/
|
||||
public class AuthenticationContext {
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.auth;
|
||||
|
||||
import cn.topiam.employee.core.security.userdetails.UserDetails;
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.ServiceTicket;
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.Ticket;
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.TicketGrantingTicket;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/1/2 23:43
|
||||
*/
|
||||
public interface CentralAuthenticationService {
|
||||
TicketGrantingTicket createTicketGrantingTicket(UserDetails userDetails, String sessionId);
|
||||
|
||||
ServiceTicket grantServiceTicket(String tgtId, String service);
|
||||
|
||||
<T extends Ticket> T getTicket(String id, Class<T> clazz);
|
||||
|
||||
ServiceTicket validateServiceTicket(String id, String service);
|
||||
|
||||
void destroyTicketGrantingTicket(String var1);
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.auth;
|
||||
|
||||
import cn.topiam.employee.core.security.userdetails.UserDetails;
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.*;
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/1/1 16:25
|
||||
*/
|
||||
@Service(value = "cas-authentication-service")
|
||||
public class CentralAuthenticationServiceImp implements CentralAuthenticationService {
|
||||
final TicketRegistry ticketRegistry;
|
||||
final TicketFactory ticketFactory;
|
||||
|
||||
public CentralAuthenticationServiceImp(TicketRegistry ticketRegistry,
|
||||
TicketFactory ticketFactory) {
|
||||
this.ticketRegistry = ticketRegistry;
|
||||
this.ticketFactory = ticketFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TicketGrantingTicket createTicketGrantingTicket(UserDetails userDetails,
|
||||
String sessionId) {
|
||||
TicketGrantingTicketFactory ticketGrantingTicketFactory = ticketFactory
|
||||
.get(TicketGrantingTicket.class);
|
||||
TicketGrantingTicket ticketGrantingTicket = ticketGrantingTicketFactory.create(userDetails,
|
||||
sessionId);
|
||||
ticketRegistry.addTicket(ticketGrantingTicket);
|
||||
return ticketGrantingTicket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceTicket grantServiceTicket(final String tgtId, final String service) {
|
||||
TicketGrantingTicket ticketGrantingTicket = this.getTicket(tgtId,
|
||||
TicketGrantingTicket.class);
|
||||
ServiceTicketFactory serviceTicketFactory = ticketFactory.get(ServiceTicket.class);
|
||||
ServiceTicket serviceTicket = serviceTicketFactory.create(ticketGrantingTicket, service);
|
||||
ticketRegistry.addTicket(serviceTicket);
|
||||
return serviceTicket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Ticket> T getTicket(String id, Class<T> clazz) {
|
||||
return ticketRegistry.getTicket(id, clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceTicket validateServiceTicket(String serviceTicketId, String service) {
|
||||
try {
|
||||
ServiceTicket serviceTicket = ticketRegistry
|
||||
.getTicket(Preconditions.checkNotNull(serviceTicketId), ServiceTicket.class);
|
||||
TicketGrantingTicket ticketGrantingTicket = Preconditions.checkNotNull(serviceTicket)
|
||||
.getTicketGrantingTicket();
|
||||
Preconditions.checkNotNull(ticketGrantingTicket);
|
||||
return serviceTicket;
|
||||
} catch (NullPointerException e) {
|
||||
return null;
|
||||
} finally {
|
||||
this.ticketRegistry.deleteTicket(serviceTicketId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyTicketGrantingTicket(final String ticketGrantingTicketId) {
|
||||
TicketGrantingTicket ticketGrantingTicket = this.getTicket(ticketGrantingTicketId,
|
||||
TicketGrantingTicket.class);
|
||||
// TODO: 通知客户端销毁ticket
|
||||
ticketRegistry.deleteTicket(ticketGrantingTicketId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.auth;
|
||||
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.ServiceTicket;
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.Ticket;
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.TicketGrantingTicket;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 21:42
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
@Service
|
||||
public class CentralCacheService {
|
||||
private static final int DEFAULT_ST_EXPIRED_TIME = 10;
|
||||
//将Service Ticket存放到redis,一次性使用,10秒内过期
|
||||
private static final int DEFAULT_TGT_EXPIRED_TIME = 7200;
|
||||
//TicketGrantingTicket 默认两个小时过期
|
||||
private final RedisTemplate<Object, Object> redisTemplate;
|
||||
|
||||
public CentralCacheService(RedisTemplate<Object, Object> redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
public void save(Ticket ticket) {
|
||||
long timeout = 0;
|
||||
if (ticket instanceof ServiceTicket) {
|
||||
timeout = DEFAULT_ST_EXPIRED_TIME;
|
||||
}
|
||||
if (ticket instanceof TicketGrantingTicket) {
|
||||
timeout = DEFAULT_TGT_EXPIRED_TIME;
|
||||
}
|
||||
redisTemplate.opsForValue().set(ticket.getId(), ticket, timeout, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public Ticket get(String id) {
|
||||
return (Ticket) redisTemplate.opsForValue().get(id);
|
||||
}
|
||||
|
||||
public void remove(String id) {
|
||||
redisTemplate.delete(id);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.configuration;
|
||||
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.DefaultTicketFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/30 01:03
|
||||
*/
|
||||
@Configuration
|
||||
public class CasConfiguration {
|
||||
|
||||
@Bean
|
||||
public DefaultTicketFactory factory() {
|
||||
final DefaultTicketFactory factory = new DefaultTicketFactory();
|
||||
factory.initialize();
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DocumentBuilder documentBuilder() throws ParserConfigurationException {
|
||||
return DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.constant;
|
||||
|
||||
/**
|
||||
* 变量
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 22:07
|
||||
*/
|
||||
public class ProtocolConstants {
|
||||
|
||||
public static final String PREFIX_ST = "ST";
|
||||
public static final String PREFIX_TGT = "TGT";
|
||||
|
||||
public static final String TICKET = "ticket";
|
||||
public static final String SERVICE = "service";
|
||||
|
||||
public static final String SERVICE_RESPONSE = "cas:serviceResponse";
|
||||
public static final String SERVICE_ATTRIBUTES = "http://www.yale.edu/tp/cas";
|
||||
|
||||
public static final String AUTHENTICATION_FAILED = "cas:authenticationFailure";
|
||||
public static final String AUTHENTICATION_SUCCESS = "cas:authenticationSuccess";
|
||||
|
||||
public static final String CAS_ATTRIBUTES = "cas:attributes";
|
||||
public static final String CAS_USER = "cas:user";
|
||||
|
||||
public static final String INVALID_TICKET = "INVALID_TICKET";
|
||||
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.endpoint;
|
||||
|
||||
import cn.topiam.employee.application.ApplicationService;
|
||||
import cn.topiam.employee.application.ApplicationServiceLoader;
|
||||
import cn.topiam.employee.application.CasApplicationService;
|
||||
import cn.topiam.employee.application.context.ApplicationContext;
|
||||
import cn.topiam.employee.application.context.ApplicationContextHolder;
|
||||
import cn.topiam.employee.common.constants.ProtocolConstants;
|
||||
import cn.topiam.employee.core.context.ServerContextHelp;
|
||||
import cn.topiam.employee.core.protocol.CasSsoModel;
|
||||
import cn.topiam.employee.core.security.savedredirect.HttpSessionRedirectCache;
|
||||
import cn.topiam.employee.core.security.savedredirect.RedirectCache;
|
||||
import cn.topiam.employee.core.security.userdetails.UserDetails;
|
||||
import cn.topiam.employee.core.security.util.SecurityUtils;
|
||||
import cn.topiam.employee.protocol.cas.idp.auth.CentralAuthenticationService;
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.ServiceTicket;
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.TicketGrantingTicket;
|
||||
import cn.topiam.employee.support.exception.TopIamException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.web.servlet.filter.OrderedFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.topiam.employee.common.constants.AuthorizeConstants.FE_LOGIN;
|
||||
import static cn.topiam.employee.core.security.util.SecurityUtils.isAuthenticated;
|
||||
import static cn.topiam.employee.protocol.cas.idp.constant.ProtocolConstants.SERVICE;
|
||||
import static cn.topiam.employee.protocol.cas.idp.constant.ProtocolConstants.TICKET;
|
||||
|
||||
/**
|
||||
* CAS 单点登录
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 17:38
|
||||
*/
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
public class CasIdpSingleSignOnEndpointFilter extends OncePerRequestFilter
|
||||
implements OrderedFilter {
|
||||
private static final Logger logger = LoggerFactory
|
||||
.getLogger(CasIdpSingleSignOnEndpointFilter.class);
|
||||
private static final RequestMatcher CAS_SSO_REQUEST_MATCHER = new AntPathRequestMatcher(
|
||||
ProtocolConstants.CasEndpointConstants.CAS_LOGIN_PATH, HttpMethod.GET.name());
|
||||
private final RedirectCache redirectCache = new HttpSessionRedirectCache();
|
||||
|
||||
/**
|
||||
* 应用配置
|
||||
*/
|
||||
private final ApplicationServiceLoader applicationServiceLoader;
|
||||
|
||||
private final SessionRegistry sessionRegistry;
|
||||
|
||||
private final CentralAuthenticationService centralAuthenticationService;
|
||||
|
||||
public CasIdpSingleSignOnEndpointFilter(ApplicationServiceLoader applicationServiceLoader,
|
||||
SessionRegistry sessionRegistry,
|
||||
CentralAuthenticationService centralAuthenticationService) {
|
||||
this.applicationServiceLoader = applicationServiceLoader;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.centralAuthenticationService = centralAuthenticationService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.LOWEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
if (CAS_SSO_REQUEST_MATCHER.matches(request)) {
|
||||
if (!isAuthenticated()) {
|
||||
//Saved Redirect
|
||||
if (!CollectionUtils.isEmpty(request.getParameterMap())) {
|
||||
redirectCache.saveRedirect(request, response,
|
||||
RedirectCache.RedirectType.REQUEST);
|
||||
}
|
||||
//跳转登录
|
||||
response.sendRedirect(ServerContextHelp.getPortalPublicBaseUrl() + FE_LOGIN);
|
||||
return;
|
||||
}
|
||||
UserDetails userDetails = SecurityUtils.getCurrentUser();
|
||||
List<SessionInformation> sessionInformations = sessionRegistry
|
||||
.getAllSessions(userDetails.getUsername(), false);
|
||||
if (sessionInformations.size() != 1) {
|
||||
throw new TopIamException("用户身份出现异常");
|
||||
}
|
||||
String sessionId = sessionInformations.get(0).getSessionId();
|
||||
//获取应用配置
|
||||
ApplicationContext applicationContext = ApplicationContextHolder
|
||||
.getApplicationContext();
|
||||
ApplicationService applicationService = applicationServiceLoader
|
||||
.getApplicationService(applicationContext.getAppTemplate());
|
||||
CasSsoModel ssoModel = ((CasApplicationService) applicationService)
|
||||
.getSsoModel(applicationContext.getAppId());
|
||||
|
||||
String service = request.getParameter(SERVICE);
|
||||
|
||||
TicketGrantingTicket ticketGrantingTicket = centralAuthenticationService
|
||||
.getTicket(sessionId, TicketGrantingTicket.class);
|
||||
|
||||
if (ticketGrantingTicket == null) {
|
||||
ticketGrantingTicket = centralAuthenticationService
|
||||
.createTicketGrantingTicket(userDetails, sessionId);
|
||||
}
|
||||
ServiceTicket serviceTicket = centralAuthenticationService
|
||||
.grantServiceTicket(ticketGrantingTicket.getId(), service);
|
||||
|
||||
response.sendRedirect(UriComponentsBuilder.fromHttpUrl(ssoModel.getSsoCallbackUrl())
|
||||
.queryParam(TICKET, serviceTicket.getId()).build().toString());
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
public static RequestMatcher getRequestMatcher() {
|
||||
return CAS_SSO_REQUEST_MATCHER;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.endpoint;
|
||||
|
||||
import cn.topiam.employee.application.ApplicationServiceLoader;
|
||||
import cn.topiam.employee.common.constants.ProtocolConstants;
|
||||
import cn.topiam.employee.core.security.userdetails.UserDetails;
|
||||
import cn.topiam.employee.protocol.cas.idp.auth.CentralAuthenticationService;
|
||||
import cn.topiam.employee.protocol.cas.idp.tickets.ServiceTicket;
|
||||
import cn.topiam.employee.protocol.cas.idp.xml.ResponseGenerator;
|
||||
import cn.topiam.employee.protocol.cas.idp.xml.ResponseGeneratorImpl;
|
||||
import org.springframework.boot.web.servlet.filter.OrderedFilter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.topiam.employee.protocol.cas.idp.constant.ProtocolConstants.SERVICE;
|
||||
import static cn.topiam.employee.protocol.cas.idp.constant.ProtocolConstants.TICKET;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/1/2 13:38
|
||||
*/
|
||||
public class CasIdpValidateEndpointFilter extends OncePerRequestFilter implements OrderedFilter {
|
||||
|
||||
private final ApplicationServiceLoader applicationServiceLoader;
|
||||
|
||||
private final SessionRegistry sessionRegistry;
|
||||
|
||||
private final CentralAuthenticationService centralAuthenticationService;
|
||||
|
||||
private static final OrRequestMatcher orRequestMatcher;
|
||||
|
||||
static {
|
||||
List<RequestMatcher> requestMatchers = new ArrayList<>();
|
||||
requestMatchers.add(new AntPathRequestMatcher(
|
||||
ProtocolConstants.CasEndpointConstants.CAS_VALIDATE_PATH, HttpMethod.GET.name()));
|
||||
requestMatchers.add(new AntPathRequestMatcher(
|
||||
ProtocolConstants.CasEndpointConstants.CAS_VALIDATE_V2_PATH, HttpMethod.GET.name()));
|
||||
requestMatchers.add(new AntPathRequestMatcher(
|
||||
ProtocolConstants.CasEndpointConstants.CAS_VALIDATE_V3_PATH, HttpMethod.GET.name()));
|
||||
orRequestMatcher = new OrRequestMatcher(requestMatchers);
|
||||
}
|
||||
|
||||
private final DocumentBuilder documentBuilder;
|
||||
|
||||
public CasIdpValidateEndpointFilter(ApplicationServiceLoader applicationServiceLoader,
|
||||
SessionRegistry sessionRegistry,
|
||||
CentralAuthenticationService centralAuthenticationService,
|
||||
DocumentBuilder documentBuilder) {
|
||||
this.applicationServiceLoader = applicationServiceLoader;
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
this.centralAuthenticationService = centralAuthenticationService;
|
||||
this.documentBuilder = documentBuilder;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.LOWEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
if (orRequestMatcher.matches(request)) {
|
||||
ResponseGenerator generator = new ResponseGeneratorImpl(documentBuilder, response);
|
||||
String ticketId = request.getParameter(TICKET);
|
||||
String service = request.getParameter(SERVICE);
|
||||
ServiceTicket serviceTicket = centralAuthenticationService
|
||||
.validateServiceTicket(ticketId, service);
|
||||
if (serviceTicket == null) {
|
||||
generator.genFailedMessage(ticketId);
|
||||
} else {
|
||||
UserDetails userDetails = serviceTicket.getTicketGrantingTicket().getUserDetails();
|
||||
// TODO: 2023/1/2 根据配置返回额外的属性配置
|
||||
generator.genSucceedMessage(userDetails.getUsername(), new HashMap<>());
|
||||
}
|
||||
generator.sendMessage();
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
}
|
||||
|
||||
public static RequestMatcher getRequestMatcher() {
|
||||
return orRequestMatcher;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.filter;
|
||||
|
||||
import cn.topiam.employee.application.context.ApplicationContext;
|
||||
import cn.topiam.employee.application.context.ApplicationContextHolder;
|
||||
import cn.topiam.employee.application.exception.AppNotExistException;
|
||||
import cn.topiam.employee.common.constants.ProtocolConstants;
|
||||
import cn.topiam.employee.common.entity.app.po.AppCasConfigPO;
|
||||
import cn.topiam.employee.common.repository.app.AppCasConfigRepository;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.topiam.employee.common.constants.ProtocolConstants.APP_CODE;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 22:37
|
||||
*/
|
||||
public class CasAuthorizationServerContextFilter extends OncePerRequestFilter {
|
||||
|
||||
private final RequestMatcher endpointsMatcher;
|
||||
private final AppCasConfigRepository appCasConfigRepository;
|
||||
|
||||
private final RequestMatcher appAuthorizePathRequestMatcher = new AntPathRequestMatcher(
|
||||
ProtocolConstants.CasEndpointConstants.CAS_AUTHORIZE_BASE_PATH + "/**");
|
||||
|
||||
public CasAuthorizationServerContextFilter(RequestMatcher endpointsMatcher,
|
||||
AppCasConfigRepository appCasConfigRepository) {
|
||||
Assert.notNull(endpointsMatcher, "endpointsMatcher cannot be null");
|
||||
Assert.notNull(appCasConfigRepository, "appCasConfigRepository cannot be null");
|
||||
this.endpointsMatcher = endpointsMatcher;
|
||||
this.appCasConfigRepository = appCasConfigRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
try {
|
||||
//匹配
|
||||
if (appAuthorizePathRequestMatcher.matches(request)
|
||||
&& endpointsMatcher.matches(request)) {
|
||||
//获取应用编码
|
||||
Map<String, String> variables = appAuthorizePathRequestMatcher.matcher(request)
|
||||
.getVariables();
|
||||
String appCode = variables.get(APP_CODE);
|
||||
AppCasConfigPO configPo = appCasConfigRepository.findByAppCode(appCode);
|
||||
if (Objects.isNull(configPo)) {
|
||||
throw new AppNotExistException();
|
||||
}
|
||||
|
||||
//封装上下文内容
|
||||
Map<String, Object> config = new HashMap<>(16);
|
||||
ApplicationContextHolder.setProviderContext(new ApplicationContext(
|
||||
configPo.getAppId(), configPo.getAppCode(), configPo.getAppTemplate(),
|
||||
configPo.getClientId(), configPo.getClientSecret(), config));
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
ApplicationContextHolder.resetProviderContext();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public class DefaultServiceTicketFactory implements ServiceTicketFactory {
|
||||
@Override
|
||||
public ServiceTicket create(final TicketGrantingTicket ticketGrantingTicket,
|
||||
final String service) {
|
||||
if (ticketGrantingTicket == null) {
|
||||
return null;
|
||||
}
|
||||
return ticketGrantingTicket.grantServiceTicket(service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends TicketFactory> T get(final Class<? extends Ticket> clazz) {
|
||||
return (T) this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public class DefaultTicketFactory implements TicketFactory {
|
||||
private Map<String, Object> factoryMap;
|
||||
private ServiceTicketFactory serviceTicketFactory = new DefaultServiceTicketFactory();
|
||||
private TicketGrantingTicketFactory ticketGrantingTicketFactory = new DefaultTicketGrantingTicketFactory();
|
||||
|
||||
public void initialize() {
|
||||
serviceTicketFactory = new DefaultServiceTicketFactory();
|
||||
ticketGrantingTicketFactory = new DefaultTicketGrantingTicketFactory();
|
||||
factoryMap = new HashMap<>();
|
||||
factoryMap.put(TicketGrantingTicket.class.getCanonicalName(),
|
||||
this.ticketGrantingTicketFactory);
|
||||
factoryMap.put(ServiceTicket.class.getCanonicalName(), this.serviceTicketFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends TicketFactory> T get(final Class<? extends Ticket> clazz) {
|
||||
return (T) this.factoryMap.get(clazz.getCanonicalName());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
import cn.topiam.employee.core.security.userdetails.UserDetails;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public class DefaultTicketGrantingTicketFactory implements TicketGrantingTicketFactory {
|
||||
|
||||
@Override
|
||||
public <T extends TicketFactory> T get(final Class<? extends Ticket> clazz) {
|
||||
return (T) this;
|
||||
}
|
||||
|
||||
@Override
|
||||
// TODO: 2023/1/2 TGT本来应该以TGT-xxx命名,此处为了兼容系统session,直接使用sessionId作为TGT的id
|
||||
public TicketGrantingTicket create(UserDetails userDetails, String sessionId) {
|
||||
return new TicketGrantingTicketImpl(sessionId, userDetails);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
import cn.topiam.employee.protocol.cas.idp.auth.CentralCacheService;
|
||||
import com.google.common.base.Preconditions;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DefaultTicketRegistry implements TicketRegistry {
|
||||
|
||||
CentralCacheService cacheService;
|
||||
|
||||
public DefaultTicketRegistry(CentralCacheService cacheService) {
|
||||
this.cacheService = cacheService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTicket(final Ticket ticket) {
|
||||
cacheService.save(ticket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Ticket getTicket(final String ticketId) {
|
||||
return cacheService.get(ticketId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Ticket> T getTicket(final String id, final Class<T> clazz) {
|
||||
Preconditions.checkNotNull(clazz, "clazz cannot be null");
|
||||
Ticket ticket = this.getTicket(id);
|
||||
if (ticket == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!clazz.isAssignableFrom(ticket.getClass())) {
|
||||
throw new ClassCastException("Ticket [" + ticket.getId() + " is of type "
|
||||
+ ticket.getClass() + " when we were expecting " + clazz);
|
||||
}
|
||||
return (T) ticket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTicket(final Ticket ticket) {
|
||||
addTicket(ticket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteTicket(final String id) {
|
||||
cacheService.remove(id);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public interface ServiceTicket extends Ticket {
|
||||
TicketGrantingTicket getTicketGrantingTicket();
|
||||
|
||||
String getService();
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public interface ServiceTicketFactory extends TicketFactory {
|
||||
ServiceTicket create(TicketGrantingTicket ticketGrantingTicket, String service);
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public class ServiceTicketImpl extends TicketGrantingTicketImpl implements ServiceTicket {
|
||||
private TicketGrantingTicket ticketGrantingTicket;
|
||||
private String service;
|
||||
|
||||
ServiceTicketImpl(final String id, final TicketGrantingTicket ticket, final String service) {
|
||||
super(id, ticket.getUserDetails());
|
||||
this.ticketGrantingTicket = ticket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getService() {
|
||||
return this.service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TicketGrantingTicket getTicketGrantingTicket() {
|
||||
return ticketGrantingTicket;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public interface Ticket extends Serializable {
|
||||
String getId();
|
||||
|
||||
boolean isExpired();
|
||||
|
||||
long getCreateTime();
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public interface TicketFactory {
|
||||
<T extends TicketFactory> T get(Class<? extends Ticket> clazz);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
import cn.topiam.employee.core.security.userdetails.UserDetails;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
* <p>
|
||||
* 其实TGT可以理解为用户登陆的session,包含了用户的信息
|
||||
*/
|
||||
public interface TicketGrantingTicket extends Ticket {
|
||||
ServiceTicket grantServiceTicket(String service);
|
||||
|
||||
UserDetails getUserDetails();
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
import cn.topiam.employee.core.security.userdetails.UserDetails;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public interface TicketGrantingTicketFactory extends TicketFactory {
|
||||
|
||||
TicketGrantingTicket create(UserDetails userDetails, String sessionId);
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
import cn.topiam.employee.core.security.userdetails.UserDetails;
|
||||
import cn.topiam.employee.protocol.cas.idp.util.TicketUtils;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import static cn.topiam.employee.protocol.cas.idp.constant.ProtocolConstants.PREFIX_ST;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public class TicketGrantingTicketImpl implements TicketGrantingTicket {
|
||||
private final String id;
|
||||
private final long createTime;
|
||||
|
||||
private final UserDetails userDetails;
|
||||
|
||||
TicketGrantingTicketImpl(final String id, final UserDetails userDetails) {
|
||||
this.id = id;
|
||||
this.userDetails = userDetails;
|
||||
this.createTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public UserDetails getUserDetails() {
|
||||
return this.userDetails;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized ServiceTicket grantServiceTicket(final String service) {
|
||||
return new ServiceTicketImpl(TicketUtils.generateTicket(PREFIX_ST), this, service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (obj == null || this.getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Ticket ticket = (Ticket) obj;
|
||||
return Objects.equals(this.id, ticket.getId())
|
||||
&& Objects.equals(this.createTime, ticket.getCreateTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode() + Long.hashCode(createTime);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.tickets;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public interface TicketRegistry {
|
||||
|
||||
void addTicket(Ticket ticket);
|
||||
|
||||
Ticket getTicket(String id);
|
||||
|
||||
<T extends Ticket> T getTicket(String id, Class<T> clazz);
|
||||
|
||||
void updateTicket(Ticket ticket);
|
||||
|
||||
void deleteTicket(String ticketId);
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* eiam-protocol-cas - Employee Identity and Access Management Program
|
||||
* Copyright © 2020-2022 TopIAM (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.protocol.cas.idp.util;
|
||||
|
||||
import cn.topiam.employee.application.ApplicationServiceLoader;
|
||||
import cn.topiam.employee.common.repository.app.AppCasConfigRepository;
|
||||
import cn.topiam.employee.protocol.cas.idp.auth.CentralAuthenticationService;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public class CasUtils {
|
||||
|
||||
public static <B extends HttpSecurityBuilder<B>> AppCasConfigRepository getAppCasConfigRepository(B builder) {
|
||||
AppCasConfigRepository appRepository = builder
|
||||
.getSharedObject(AppCasConfigRepository.class);
|
||||
if (appRepository == null) {
|
||||
appRepository = builder.getSharedObject(ApplicationContext.class)
|
||||
.getBean(AppCasConfigRepository.class);
|
||||
builder.setSharedObject(AppCasConfigRepository.class, appRepository);
|
||||
}
|
||||
return appRepository;
|
||||
}
|
||||
|
||||
public static <B extends HttpSecurityBuilder<B>> SessionRegistry getSessionRegistry(B builder) {
|
||||
SessionRegistry sessionRegistry = builder.getSharedObject(SessionRegistry.class);
|
||||
if (sessionRegistry == null) {
|
||||
sessionRegistry = getBean(builder, SessionRegistry.class);
|
||||
builder.setSharedObject(SessionRegistry.class, sessionRegistry);
|
||||
}
|
||||
return sessionRegistry;
|
||||
}
|
||||
|
||||
public static <B extends HttpSecurityBuilder<B>> CentralAuthenticationService getCentralAuthenticationService(B builder) {
|
||||
CentralAuthenticationService authenticationService = builder
|
||||
.getSharedObject(CentralAuthenticationService.class);
|
||||
if (authenticationService == null) {
|
||||
authenticationService = getBean(builder, CentralAuthenticationService.class);
|
||||
builder.setSharedObject(CentralAuthenticationService.class, authenticationService);
|
||||
}
|
||||
return authenticationService;
|
||||
}
|
||||
|
||||
public static <B extends HttpSecurityBuilder<B>> DocumentBuilder getDocumentBuilder(B builder) {
|
||||
DocumentBuilder documentBuilder = builder.getSharedObject(DocumentBuilder.class);
|
||||
if (documentBuilder == null) {
|
||||
documentBuilder = getBean(builder, DocumentBuilder.class);
|
||||
builder.setSharedObject(DocumentBuilder.class, documentBuilder);
|
||||
}
|
||||
return documentBuilder;
|
||||
}
|
||||
|
||||
public static <B extends HttpSecurityBuilder<B>> ApplicationServiceLoader getApplicationServiceLoader(B builder) {
|
||||
ApplicationServiceLoader applicationServiceLoader = builder
|
||||
.getSharedObject(ApplicationServiceLoader.class);
|
||||
if (applicationServiceLoader == null) {
|
||||
applicationServiceLoader = getBean(builder, ApplicationServiceLoader.class);
|
||||
builder.setSharedObject(ApplicationServiceLoader.class, applicationServiceLoader);
|
||||
}
|
||||
return applicationServiceLoader;
|
||||
}
|
||||
|
||||
public static <B extends HttpSecurityBuilder<B>, T> T getBean(B builder, Class<T> type) {
|
||||
return builder.getSharedObject(ApplicationContext.class).getBean(type);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.util;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2022/12/29 16:25
|
||||
*/
|
||||
public class TicketUtils {
|
||||
private static final char[] PRINTABLE_CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345679"
|
||||
.toCharArray();
|
||||
|
||||
private static String getNewString() {
|
||||
SecureRandom randomizer = new SecureRandom();
|
||||
byte[] random = new byte[20];
|
||||
randomizer.nextBytes(random);
|
||||
return convertBytesToString(random);
|
||||
}
|
||||
|
||||
private static String convertBytesToString(byte[] random) {
|
||||
char[] output = new char[random.length];
|
||||
IntStream.range(0, random.length).forEach((i) -> {
|
||||
int index = Math.abs(random[i] % PRINTABLE_CHARACTERS.length);
|
||||
output[i] = PRINTABLE_CHARACTERS[index];
|
||||
});
|
||||
return new String(output);
|
||||
}
|
||||
|
||||
private static AtomicLong count = new AtomicLong(0L);
|
||||
|
||||
private static String getNextNumberAsString() {
|
||||
return Long.toString(getNextValue());
|
||||
}
|
||||
|
||||
private static long getNextValue() {
|
||||
return count.compareAndSet(9223372036854775807L, 0L) ? 9223372036854775807L
|
||||
: count.getAndIncrement();
|
||||
}
|
||||
|
||||
private static String getSuffix(String suffix) {
|
||||
if (StringUtils.isNotBlank(suffix)) {
|
||||
return suffix;
|
||||
}
|
||||
return getCasServerHostName();
|
||||
}
|
||||
|
||||
private static String getCasServerHostName() {
|
||||
try {
|
||||
String hostName = InetAddress.getLocalHost().getCanonicalHostName();
|
||||
int index = hostName.indexOf(46);
|
||||
return index > 0 ? hostName.substring(0, index) : hostName;
|
||||
} catch (Exception var2) {
|
||||
throw new IllegalArgumentException("Host name could not be determined automatically.",
|
||||
var2);
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateTicket(String prefix, String suffix) {
|
||||
return prefix + "-" + getNextNumberAsString() + "-" + getNewString() + "-"
|
||||
+ getSuffix(suffix);
|
||||
}
|
||||
|
||||
public static String generateTicket(String prefix) {
|
||||
return generateTicket(prefix, null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.xml;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/1/2 21:22
|
||||
*/
|
||||
public interface ResponseGenerator {
|
||||
|
||||
void genFailedMessage(String serviceTicketId);
|
||||
|
||||
void genSucceedMessage(String casUser, Map<String, Object> attributes);
|
||||
|
||||
void sendMessage() throws IOException;
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package cn.topiam.employee.protocol.cas.idp.xml;
|
||||
|
||||
import org.dom4j.io.OutputFormat;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerException;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.topiam.employee.protocol.cas.idp.constant.ProtocolConstants.*;
|
||||
|
||||
/**
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/1/2 20:23
|
||||
*/
|
||||
public class ResponseGeneratorImpl implements ResponseGenerator {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(ResponseGeneratorImpl.class);
|
||||
|
||||
private final HttpServletResponse response;
|
||||
private final DocumentBuilder documentBuilder;
|
||||
private String message;
|
||||
|
||||
public ResponseGeneratorImpl(DocumentBuilder documentBuilder, HttpServletResponse response) {
|
||||
this.response = response;
|
||||
this.documentBuilder = documentBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void genFailedMessage(String serviceTicketId) {
|
||||
Document document = documentBuilder.newDocument();
|
||||
Element serviceResponse = document.createElement(SERVICE_RESPONSE);
|
||||
serviceResponse.setAttribute("xmlns:cas", SERVICE_ATTRIBUTES);
|
||||
|
||||
Element failElement = document.createElement(AUTHENTICATION_FAILED);
|
||||
failElement.setAttribute("code", INVALID_TICKET);
|
||||
failElement.setTextContent(String.format("未能够识别出目标 '%s'票根", serviceTicketId));
|
||||
serviceResponse.appendChild(failElement);
|
||||
document.appendChild(serviceResponse);
|
||||
this.message = parseDocumentToString(serviceResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void genSucceedMessage(String casUser, Map<String, Object> attributes) {
|
||||
Document document = documentBuilder.newDocument();
|
||||
OutputFormat outputFormat = OutputFormat.createCompactFormat();
|
||||
outputFormat.setExpandEmptyElements(true);
|
||||
Element serviceResponse = document.createElement(SERVICE_RESPONSE);
|
||||
serviceResponse.setAttribute("xmlns:cas", SERVICE_ATTRIBUTES);
|
||||
Element successElement = document.createElement(AUTHENTICATION_SUCCESS);
|
||||
serviceResponse.appendChild(successElement);
|
||||
Element userElement = document.createElement(CAS_USER);
|
||||
userElement.setTextContent(casUser);
|
||||
successElement.appendChild(userElement);
|
||||
if (attributes.size() > 0) {
|
||||
Element attributeElement = document.createElement(CAS_ATTRIBUTES);
|
||||
for (Map.Entry<String, Object> entry : attributes.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof List) {
|
||||
for (Object valueItem : (List) value) {
|
||||
Element entryElement = document.createElement("cas:" + entry.getKey());
|
||||
entryElement.setTextContent(String.valueOf(valueItem));
|
||||
attributeElement.appendChild(entryElement);
|
||||
}
|
||||
} else {
|
||||
Element entryElement = document.createElement("cas:" + entry.getKey());
|
||||
entryElement.setTextContent(String.valueOf(entry.getValue()));
|
||||
attributeElement.appendChild(entryElement);
|
||||
}
|
||||
}
|
||||
successElement.appendChild(attributeElement);
|
||||
}
|
||||
document.appendChild(serviceResponse);
|
||||
this.message = parseDocumentToString(serviceResponse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage() throws IOException {
|
||||
response.setContentType("text/xml;charset=UTF-8");
|
||||
PrintWriter out = response.getWriter();
|
||||
out.println(message);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private String parseDocumentToString(Element node) {
|
||||
try {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
TransformerFactory transFactory = TransformerFactory.newInstance();
|
||||
Transformer transformer = transFactory.newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");//序列化不保留标头
|
||||
DOMSource domSource = new DOMSource(node);
|
||||
transformer.transform(domSource, new StreamResult(bos));
|
||||
return bos.toString(StandardCharsets.UTF_8);
|
||||
} catch (TransformerException e) {
|
||||
logger.error("xmlUtils failed to parseDocumentToString:" + e.getMessage(), e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue