增加CAS协议支持

pull/5/head
yunzhang 2023-01-02 23:20:45 +08:00
parent 72b36178dd
commit 16bed0aa80
48 changed files with 2029 additions and 67 deletions

View File

@ -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 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));
}
}

View File

@ -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,6 +54,23 @@ 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;
}
/**
*
@ -44,6 +80,46 @@ public class CasStandardApplicationServiceImpl extends AbstractCasApplicationSer
*/
@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);
}
}

View File

@ -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 {
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
@ -47,6 +50,7 @@ import static cn.topiam.employee.support.util.RsaUtils.getKeys;
*/
public abstract class AbstractApplicationService implements ApplicationService {
private final Logger logger = LoggerFactory.getLogger(AbstractApplicationService.class);
protected final ObjectMapper mapper = new ObjectMapper();
/**
*
@ -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();
}
}

View File

@ -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);
}

View File

@ -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")

View File

@ -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);

View File

@ -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";
/**
*

View File

@ -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
*/
@ -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";
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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
*

View File

@ -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);
}
}

View File

@ -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 {
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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>
* TGTsession
*/
public interface TicketGrantingTicket extends Ticket {
ServiceTicket grantServiceTicket(String service);
UserDetails getUserDetails();
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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 "";
}
}