# 概述
- CAS是Central Authentication Service的缩写,中央认证服务,一种独立开放指令协议。
- CAS是耶鲁大学发起的一个开源项目,旨在为Web应用系统提供一种可靠的单点登录的方法.名称为 Yale CAS
- 2004年12月,CAS正式成为JS-SIG的一个项目,项目更名为JASIG CAS
- 2012年,JASIG与Sakai基金会合并,更名为Apereo基金会,全部的CAS也跟着更名为Apereo CAS
# CAS服务端源码概要
# 源码槽点
- 项目基于传统的tomcat容器展开,灵活性差
- 版本管理混乱,最新版本6.x源码不全
- 源码兼容性差,新版本跟旧版本API不一致
- 庞大,臃肿
# SpringWebFlow与Cas Server
- Cas Server依赖于SpringWebFlow展开相应的流程
- SpringWebFlow将在以后的时间里面专门解析,本次不涉及SpringWebFlow的内容
# CAS服务端登录设计
# 认证过程
略
# TGT的结构
public interface Ticket extends Serializable, Comparable<Ticket> {
String getId();
boolean isExpired();
String getPrefix();
/*xxx: CAS中,所有的Ticket,都与tgt有关系*/
TicketGrantingTicket getTicketGrantingTicket();
}
public interface TicketGrantingTicket extends Ticket {
String PREFIX = "TGT";
Authentication getAuthentication();
/*xxx: 签发st*/
ServiceTicket grantServiceTicket(String id, Service service,
ExpirationPolicy expirationPolicy,
boolean credentialProvided,
boolean onlyTrackMostRecentSession);
/*xxx: 获取所有签发了st的服务*/
Map<String, Service> getServices();
}
@Entity
@Table(name = "TICKETGRANTINGTICKET")
public class TicketGrantingTicketImpl extends AbstractTicket implements TicketGrantingTicket {
}
# TGT的颁发
/*xxx: 生成tgt的action */
public class CreateTicketGrantingTicketAction extends AbstractAction {
@Override
public Event doExecute(final RequestContext context) {
/*xxx: 获取已有tgt*/
final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
/*xxx: 生成tgt的关键方法 */
/*xxx: tgt与认证结果是有一定的关系的*/
final TicketGrantingTicket tgt = createOrUpdateTicketGrantingTicket(authenticationResult, authentication, ticketGrantingTicket);
/*xxx: 将生成的 tgt,放在 flow-scope中,后续的 sendtgt,会将之,取出来,并以 tgc的名称,写入 cookie中 */
WebUtils.putTicketGrantingTicketInScopes(context, tgt);
/*xxx: 省略其它抽象...*/
}
protected TicketGrantingTicket createOrUpdateTicketGrantingTicket(final AuthenticationResult authenticationResult,
final Authentication authentication, final String ticketGrantingTicket) {
final TicketGrantingTicket tgt;
/*xxx: 省略其它抽象...*/
tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationResult);
return tgt;
}
}
public interface CentralAuthenticationService {
/*xxx: 生成tgt的关键接口 */
TicketGrantingTicket createTicketGrantingTicket(AuthenticationResult authenticationResult)
throws AuthenticationException, AbstractTicketException;
}
public class DefaultCentralAuthenticationService extends AbstractCentralAuthenticationService {
/*xxx: 生成tgt*/
public TicketGrantingTicket createTicketGrantingTicket(final AuthenticationResult authenticationResult)
throws AuthenticationException, AbstractTicketException {
/*xxx: 获取认证信息*/
final Authentication authentication = authenticationResult.getAuthentication();
/*xxx: 获取服务信息*/
final Service service = authenticationResult.getService();
/*xxx: 进行服务授权验证,略*/
final TicketGrantingTicketFactory factory = (TicketGrantingTicketFactory) this.ticketFactory.get(TicketGrantingTicket.class);
/*xxx: 通过tgt工厂生成的tgt */
final TicketGrantingTicket ticketGrantingTicket = factory.create(authentication, TicketGrantingTicket.class);
/*xxx: 生成tgt后,将之保存在 ticket注册表中 */
this.ticketRegistry.addTicket(ticketGrantingTicket);
return ticketGrantingTicket;
}
}
public interface TicketGrantingTicketFactory extends TicketFactory {
<T extends TicketGrantingTicket> T create(Authentication authentication, Class<T> clazz);
}
public class DefaultTicketGrantingTicketFactory implements TicketGrantingTicketFactory {
/*xxx: 唯一键生成器*/
protected UniqueTicketIdGenerator ticketGrantingTicketUniqueTicketIdGenerator;
@Override
public <T extends TicketGrantingTicket> T create(final Authentication authentication, final Class<T> clazz) {
final String tgtId = produceTicketIdentifier(authentication);
return produceTicket(authentication, tgtId, clazz);
}
protected String produceTicketIdentifier(final Authentication authentication) {
/*xxx: TicketGrantingTicket.PREFIX="TGT"*/
String tgtId = this.ticketGrantingTicketUniqueTicketIdGenerator.getNewTicketId(TicketGrantingTicket.PREFIX);
return tgtId;
}
protected <T extends TicketGrantingTicket> T produceTicket(final Authentication authentication,
final String tgtId, final Class<T> clazz) {
final TicketGrantingTicket result = new TicketGrantingTicketImpl(
tgtId, authentication, this.ticketGrantingTicketExpirationPolicy);
return (T) result;
}
}
# TGT的存储(默认只提供了jpa的存储实现)
/*xxx: ticket注册表 */
public interface TicketRegistry {
void addTicket(Ticket ticket);
}
@Component("jpaTicketRegistry")
/*xxx: 涉及到两张表的操作;TICKETGRANTINGTICKET, SERVICETICKET */
public class JpaTicketRegistry extends AbstractDistributedTicketRegistry {
@PersistenceContext(unitName = "ticketEntityManagerFactory")
private EntityManager entityManager;
@Override
public void addTicket(final Ticket ticket) {
entityManager.persist(ticket);
}
}
# TGT的发送
/*xxx: 发送tgt动作*/
public class SendTicketGrantingTicketAction extends AbstractAction {
@Override
protected Event doExecute(final RequestContext context) {
/*xxx: 从请求上下文中,获取tgtId信息 */
final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
/*xxx: 从cookie中,获取tgtId信息 */
final String ticketGrantingTicketValueFromCookie = WebUtils.getTicketGrantingTicketIdFrom(context.getFlowScope());
/*xxx: 发送tgt*/
this.ticketGrantingTicketCookieGenerator.addCookie(context, ticketGrantingTicketId);
/*xxx: 如果cookie中的tgc,与当前上下文中的 tgtId不匹配,则需要进行销毁 tgt*/
if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
}
}
}
public class CookieRetrievingCookieGenerator extends CookieGenerator {
private final CookieValueManager casCookieValueManager;
/*xxx: 添加tgc*/
public void addCookie(final RequestContext requestContext, final String cookieValue) {
/*xxx: 根据cookie中的tgt,生成客户端tgt*/
final String theCookieValue = this.casCookieValueManager.buildCookieValue(cookieValue, request);
super.addCookie(response, theCookieValue);
}
/*xxx: 实例化时,指定了cookie的名称*/
public CookieRetrievingCookieGenerator(final String name, final String path, final int maxAge, final boolean secure,
final String domain, final CookieValueManager casCookieValueManager,
final int rememberMeMaxAge, final boolean httpOnly) {
}
}
@Configuration("casCookieConfiguration")
public class CasCookieConfiguration {
@ConditionalOnMissingBean(name = "ticketGrantingTicketCookieGenerator")
@Bean
@RefreshScope
/*xxx: tgc生成器 */
public CookieRetrievingCookieGenerator ticketGrantingTicketCookieGenerator() {
final TicketGrantingCookieProperties tgc = casProperties.getTgc();
final int rememberMeMaxAge = (int) Beans.newDuration(tgc.getRememberMeMaxAge()).getSeconds();
return new TGCCookieRetrievingCookieGenerator(cookieValueManager(),
tgc.getName(),
tgc.getPath(),
tgc.getDomain(),
rememberMeMaxAge,
tgc.isSecure(),
tgc.getMaxAge(),
tgc.isHttpOnly());
}
}
public class TicketGrantingCookieProperties extends CookieProperties {
public TicketGrantingCookieProperties() {
super.setName("TGC");
}
/*xxx: 省略其它抽象...*/
}
public interface CookieValueManager {
String buildCookieValue(String givenCookieValue, HttpServletRequest request);
}
/*xxx: 对cookie中的值进行加密的处理*/
public class EncryptedCookieValueManager implements CookieValueManager {
private final CipherExecutor<Serializable, Serializable> cipherExecutor;
@Override
/*xxx: 将tgt拼接上客户端以及客户端代理信息,并进行加密*/
public final String buildCookieValue(final String givenCookieValue, final HttpServletRequest request) {
final String res = buildCompoundCookieValue(givenCookieValue, request);
return cipherExecutor.encode(res, new Object[]{}).toString();
}
}
public class DefaultCasCookieValueManager extends EncryptedCookieValueManager {
@Override
protected String buildCompoundCookieValue(final String givenCookieValue, final HttpServletRequest request) {
final ClientInfo clientInfo = ClientInfoHolder.getClientInfo();
final StringBuilder builder = new StringBuilder(givenCookieValue);
builder.append('@').append(clientInfo.getClientIpAddress());
final String userAgent = HttpRequestUtils.getHttpServletRequestUserAgent(request);
builder.append('@').append(userAgent);
return builder.toString();
}
}
# 客户端服务--逻辑结构设计
# 客户端服务
public interface Principal extends Serializable {
String getId();
default Map<String, Object> getAttributes() {
return new LinkedHashMap<>(0);
}
}
public interface Service extends Principal {
default boolean matches(Service service) {
final String thisUrl = URLDecoder.decode(getId(), StandardCharsets.UTF_8.name());
final String serviceUrl = URLDecoder.decode(service.getId(), StandardCharsets.UTF_8.name());
return thisUrl.equalsIgnoreCase(serviceUrl);
}
}
# 客户端服务存储器
public interface ServicesManager {
RegisteredService save(RegisteredService registeredService);
Collection<RegisteredService> getAllServices();
RegisteredService delete(RegisteredService svc);
}
public abstract class AbstractServicesManager implements ServicesManager {
/*xxx: 托管*/
private final ServiceRegistry serviceRegistry;
/*xxx: 本地缓存*/
private Map<Long, RegisteredService> services = new ConcurrentHashMap<>();
}
public class DomainServicesManager extends AbstractServicesManager {
private final Map<String, TreeSet<RegisteredService>> domains = new ConcurrentHashMap<>();
@Override
protected void deleteInternal(final RegisteredService service) {
final String domain = extractDomain(service.getServiceId());
this.domains.get(domain).remove(service);
}
/*xxx: 省略其它抽象...*/
}
public interface ServiceRegistry {
}
public class InMemoryServiceRegistry extends AbstractServiceRegistry {
private List<RegisteredService> registeredServices = new ArrayList<>();
}
# 客户端服务授权检查
public class SetServiceUnauthorizedRedirectUrlAction extends AbstractAction {
@Override
protected Event doExecute(final RequestContext requestContext) throws Exception {
final RegisteredService registeredService = WebUtils.getRegisteredService(requestContext);
if (registeredService != null && registeredService.getAccessStrategy() != null) {
/*xxx: 本质上: requestContext.getFlowScope().put("unauthorizedRedirectUrl", url);*/
WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(requestContext, registeredService.getAccessStrategy().getUnauthorizedRedirectUrl());
}
return null;
}
}
# service参数检查
public class DefaultLoginWebflowConfigurer extends AbstractCasWebflowConfigurer {
/*xxx: 执行 flowScope.service != null,并获取结果*/
protected void createHasServiceCheckDecisionState(final Flow flow) {
createDecisionState(flow, CasWebflowConstants.STATE_ID_HAS_SERVICE_CHECK,
"flowScope.service != null",
CasWebflowConstants.STATE_ID_RENEW_REQUEST_CHECK,
CasWebflowConstants.STATE_ID_VIEW_GENERIC_LOGIN_SUCCESS);
}
}
# ST的结构
public interface ServiceTicket extends Ticket {
String PREFIX = "ST";
Service getService();
}
@Entity
@Table(name = "SERVICETICKET")
public class ServiceTicketImpl extends AbstractTicket implements ServiceTicket {
@ManyToOne(targetEntity = TicketGrantingTicketImpl.class)
private TicketGrantingTicket ticketGrantingTicket;
@Lob
@Column(name = "SERVICE", nullable = false, length = Integer.MAX_VALUE)
private Service service;
@Override
public Authentication getAuthentication() {
return getTicketGrantingTicket().getAuthentication();
}
}
# ST的颁发
/*xxx: 生成st的action */
public class GenerateServiceTicketAction extends AbstractAction {
@Override
protected Event doExecute(final RequestContext context) {
/*xxx: 获取到客户端服务信息*/
final Service service = WebUtils.getService(context);
/*xxx :获取上下文中的 tgt*/
final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);
/*xxx: 省略其它抽象...*/
/*xxx: 通过tgt,为客户端服务颁发 st*/
final ServiceTicket serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicket, service, authenticationResult);
/*xxx: 将st保存绑定到 request中*/
/*xxx: 本质上: map.put("serviceTicketId", serviceTicketId.getId());*/
WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
}
}
public interface CentralAuthenticationService {
/*xxx: 签发ServiceTicket的关键接口*/
ServiceTicket grantServiceTicket(String ticketGrantingTicketId, Service service, AuthenticationResult authenticationResult)
throws AuthenticationException, AbstractTicketException;
}
public class DefaultCentralAuthenticationService extends AbstractCentralAuthenticationService {
public ServiceTicket grantServiceTicket(final String ticketGrantingTicketId, final Service service, final AuthenticationResult authenticationResult)
throws AuthenticationException, AbstractTicketException {
/*xxx: 获取tgt实体 */
final TicketGrantingTicket ticketGrantingTicket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
/*xxx: 解析客户端服务*/
final Service selectedService = resolveServiceFromAuthenticationRequest(service);
final RegisteredService registeredService = this.servicesManager.findServiceBy(selectedService);
/*xxx: 通过 serviceTicket工厂创建的 serviceTicket */
final ServiceTicket serviceTicket = factory.create(ticketGrantingTicket, selectedService, credentialProvided, ServiceTicket.class);
/*xxx: 更新tgt,内部存储了签发的service信息*/
this.ticketRegistry.updateTicket(ticketGrantingTicket);
/*xxx: 将st进行存储*/
this.ticketRegistry.addTicket(serviceTicket);
}
}
public interface ServiceTicketFactory extends TicketFactory {
<T extends Ticket> T create(TicketGrantingTicket ticketGrantingTicket,
Service service,
boolean credentialProvided, Class<T> clazz);
}
public class DefaultServiceTicketFactory implements ServiceTicketFactory {
private final Map<String, UniqueTicketIdGenerator> uniqueTicketIdGeneratorsForService;
@Override
public <T extends Ticket> T create(final TicketGrantingTicket ticketGrantingTicket, final Service service,
final boolean credentialProvided, final Class<T> clazz) {
String ticketId = produceTicketIdentifier(service, ticketGrantingTicket, credentialProvided);
return produceTicket(ticketGrantingTicket, service, credentialProvided, ticketId, clazz);
}
protected String produceTicketIdentifier(final Service service, final TicketGrantingTicket ticketGrantingTicket,
final boolean credentialProvided) {
UniqueTicketIdGenerator serviceTicketUniqueTicketIdGenerator = this.uniqueTicketIdGeneratorsForService.get(service.getClass().getName());
return serviceTicketUniqueTicketIdGenerator.getNewTicketId("ST");
}
protected <T extends Ticket> T produceTicket(final TicketGrantingTicket ticketGrantingTicket, final Service service,
final boolean credentialProvided, final String ticketId, final Class<T> clazz) {
final ServiceTicket result = ticketGrantingTicket.grantServiceTicket(
ticketId,
service,
this.serviceTicketExpirationPolicy,
credentialProvided,
trackMostRecentSession);
return (T) result;
}
}
# ST的存储
同tgt,略;
# ST的发送
/*xxx: 生成st的action */
public class GenerateServiceTicketAction extends AbstractAction {
@Override
protected Event doExecute(final RequestContext context) {
/*xxx: 将st保存绑定到 request中*/
WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
}
}
/*xxx: 登录成功后,重定向到客户端的action */
public class RedirectToServiceAction extends AbstractAction {
@Override
protected Event doExecute(final RequestContext requestContext) {
final String serviceTicketId = WebUtils.getServiceTicketFromRequestScope(requestContext);
final ResponseBuilder builder = responseBuilderLocator.locate(service);
final Response response = builder.build(service, serviceTicketId, auth);
}
}
public class WebApplicationServiceResponseBuilder extends AbstractWebApplicationServiceResponseBuilder {
@Override
public Response build(final WebApplicationService service, final String serviceTicketId, final Authentication authentication) {
final Map<String, String> parameters = new HashMap<>();
parameters.put("ticket", serviceTicketId);
if (responseType == ResponseType.POST) {
return buildPost(finalService, parameters);
}
if (responseType == ResponseType.REDIRECT) {
return buildRedirect(finalService, parameters);
}
if (responseType == ResponseType.HEADER) {
return buildHeader(finalService, parameters);
}
/*xxx: 省略其它抽象...*/
}
protected Response buildPost(final WebApplicationService service, final Map<String, String> parameters) {
return DefaultResponse.getPostResponse(service.getOriginalUrl(), parameters);
}
protected Response buildRedirect(final WebApplicationService service, final Map<String, String> parameters) {
return DefaultResponse.getRedirectResponse(service.getOriginalUrl(), parameters);
}
protected Response buildHeader(final WebApplicationService service, final Map<String, String> parameters) {
return DefaultResponse.getHeaderResponse(service.getOriginalUrl(), parameters);
}
}
public class DefaultResponse implements Response {
public static Response getPostResponse(final String url, final Map<String, String> attributes) {
return new DefaultResponse(ResponseType.POST, url, attributes);
}
public static Response getHeaderResponse(final String url, final Map<String, String> attributes) {
return new DefaultResponse(ResponseType.HEADER, url, attributes);
}
public static Response getRedirectResponse(final String url, final Map<String, String> parameters) {
final StringBuilder builder = new StringBuilder(parameters.size() * 40 + 100);
/*xxx: 将parameters的参数,以 query的方式,拼接在 请求路径上,略*/
final String urlRedirect = builder.toString();
return new DefaultResponse(ResponseType.REDIRECT, urlRedirect, parameters);
}
}
# 服务票据的核验
# 票据凭证验证接口设计
@Configuration("casValidationConfiguration")
public class CasValidationConfiguration {
@Bean
@ConditionalOnMissingBean(name = "v3ServiceValidateController")
public V3ServiceValidateController v3ServiceValidateController() {
return new V3ServiceValidateController(
cas20WithoutProxyProtocolValidationSpecification,
authenticationSystemSupport.getIfAvailable(),
servicesManager,
centralAuthenticationService,
proxy20Handler.getIfAvailable(),
argumentExtractor.getIfAvailable(),
multifactorTriggerSelectionStrategy,
authenticationContextValidator,
cas3ServiceJsonView(),
cas3ServiceSuccessView(),
cas3ServiceFailureView,
casProperties.getAuthn().getMfa().getAuthenticationContextAttribute(),
serviceValidationAuthorizers,
casProperties.getSso().isRenewAuthnEnabled()
);
}
}
public class V3ServiceValidateController extends AbstractServiceValidateController {
@GetMapping(path = "/p3/serviceValidate")
protected ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
return super.handleRequestInternal(request, response);
}
}
# 验证流程设计
public abstract class AbstractServiceValidateController extends AbstractDelegateController {
@Override
public ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
/*xxx: 提取服务信息 */
final WebApplicationService service = this.argumentExtractor.extractService(request);
/*xxx: 获取 stId */
final String serviceTicketId = service != null ? service.getArtifactId() : null;
prepareForTicketValidation(request, service, serviceTicketId);
return handleTicketValidation(request, service, serviceTicketId);
}
protected void prepareForTicketValidation(final HttpServletRequest request, final WebApplicationService service, final String serviceTicketId) {
}
protected ModelAndView handleTicketValidation(final HttpServletRequest request, final WebApplicationService service, final String serviceTicketId) {
/*xxx: 验证st合法性 */
final Assertion assertion = validateServiceTicket(service, serviceTicketId);
/*xxx: 省略其它抽象...*/
}
/*xxx: 验证st的合法性*/
protected Assertion validateServiceTicket(final WebApplicationService service, final String serviceTicketId) {
return this.centralAuthenticationService.validateServiceTicket(serviceTicketId, service);
}
}
public interface CentralAuthenticationService {
/*xxx: 验证st合法性*/
Assertion validateServiceTicket(String serviceTicketId, Service service) throws AbstractTicketException;
}
public class DefaultCentralAuthenticationService extends AbstractCentralAuthenticationService {
public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws AbstractTicketException {
/*xxx: 通过stId获取 service实例*/
final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);
/*xxx: 获取认证信息*/
final TicketGrantingTicket root = serviceTicket.getTicketGrantingTicket().getRoot();
final Authentication authentication = getAuthenticationSatisfiedByPolicy(root.getAuthentication(),
new ServiceContext(selectedService, registeredService));
/*xxx: 组装认证构造器 */
final AuthenticationBuilder builder = DefaultAuthenticationBuilder.newInstance(authentication);
/*xxx: 获取认证信息 */
final Authentication finalAuthentication = builder.build();
/*xxx: 组装 Assertion实体 */
final Assertion assertion = new DefaultAssertionBuilder(finalAuthentication)
.with(selectedService)
.with(serviceTicket.getTicketGrantingTicket().getChainedAuthentications())
.with(serviceTicket.isFromNewLogin())
.build();
/*xxx: 省略其它抽象...*/
return assertion;
}
}
# cas客户端服务专题
# 已注册服务--RegisteredService
# 已注册服务-逻辑结构
public interface RegisteredService extends Serializable, Comparable<RegisteredService> {
/*xxx: 服务唯一标志*/
String getServiceId();
/*xxx: 服务名称*/
String getName();
/*xxx: 主题名称*/
String getTheme();
/*xxx: 服务响应策略,通常是带上 ticketId并重定向*/
String getResponseType();
/*xxx: 决定服务是否匹配时,相对其它已注册服务的评估顺序*/
int getEvaluationOrder();
/*xxx: 决定请求的服务与已注册服务是否匹配*/
boolean matches(Service service);
/*xxx: 决定serviceId 与已注册服务是否匹配*/
boolean matches(String serviceId);
/*xxx: 已注册服务的登出类型*/
LogoutType getLogoutType();
}
/*xxx: 通过正则匹配的已注册服务*/
public class RegexRegisteredService extends AbstractRegisteredService {
/*xxx: 服务 正则匹配器 */
private transient Pattern servicePattern;
@Override
public boolean matches(final String serviceId) {
if (this.servicePattern == null) {
/*xxx: 通过serviceId去创建正则表达式,并决定是否匹配*/
this.servicePattern = RegexUtils.createPattern(this.serviceId);
}
return StringUtils.isBlank(serviceId) ? false : this.servicePattern.matcher(serviceId).matches();
}
}
/*xxx: 已注册服务,还可以通过jdbc进行配置*/
@Entity
@Table(name = "RegexRegisteredService")
public abstract class AbstractRegisteredService implements RegisteredService {
@Column(nullable = false)
protected String serviceId;
@Column(nullable = false)
private String name;
@Column
private String theme;
/*xxx: 省略其它抽象....*/
}
# 默认的已注册服务配置
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^https://www.apereo.org",
"name" : "Apereo",
"theme" : "apereo",
"id" : 10000002,
"description" : "Apereo foundation sample service",
"evaluationOrder" : 1
}
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(https|imaps|http)://.*",
"name" : "HTTPS and IMAPS",
"id" : 10000001,
"description" : "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
"evaluationOrder" : 10000
}
# 服务注册表
# 服务注册表--逻辑设计
/*xxx: 服务注册表*/
public interface ServiceRegistry {
/*xxx: 保存已注册服务*/
RegisteredService save(RegisteredService registeredService);
/*xxx: 移除已注册服务*/
boolean delete(RegisteredService registeredService);
/*xxx: 加载已注册服务*/
List<RegisteredService> load();
/*xxx: 通过serviceId查找已注册服务*/
RegisteredService findServiceById(String id);
/*xxx: 获取已注册服务注册表大小*/
default long size() {
return load().size();
}
}
# 基于文件资源的已注册服务注册表
/*xxx: 基于抽象资源的 服务注册表*/
public abstract class AbstractResourceBasedServiceRegistry extends AbstractServiceRegistry implements ResourceBasedServiceRegistry {
/*xxx: 服务注册表配置文件的路径地址*/
protected Path serviceRegistryDirectory;
/*xxx: 已注册服务列表,由 id 号构成的主键 */
private Map<Long, RegisteredService> serviceMap = new ConcurrentHashMap<>();
@Override
public synchronized List<RegisteredService> load() {
/*xxx: 获取到所有的文件列表 */
final Collection<File> files = FileUtils.listFiles(this.serviceRegistryDirectory.toFile(), new String[]{getExtension()}, true);
/*xxx: 从文件中进行加载,并收集配置*/
this.serviceMap = files
.stream()
.map(this::load)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.sorted()
.collect(Collectors.toMap(RegisteredService::getId, Function.identity(),
LOG_DUPLICATE_AND_RETURN_FIRST_ONE, LinkedHashMap::new));
}
@Override
/*xxx: 从文件中读取配置*/
public Collection<RegisteredService> load(final File file) {
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
/*xxx: 通过已配置服务序列号器,对流进行读取解析 */
return this.registeredServiceSerializers
.stream()
.filter(s -> s.supports(file))
.map(s -> s.load(in))
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.collect(Collectors.toList());
} catch (final Exception e) {
/*xxx: 打印错误日志...*/
}
}
/*xxx: 资源文件的后缀*/
protected abstract String getExtension();
}
/*xxx: 内部资源服务注册表*/
public static class EmbeddedResourceBasedServiceRegistry extends AbstractResourceBasedServiceRegistry {
private static List getRegisteredServiceSerializers() {
/*xxx: 提供了两类实现,默认json序列化器,cas插件json序列化器*/
return CollectionUtils.wrapList(
new CasAddonsRegisteredServicesJsonSerializer(),
new DefaultRegisteredServiceJsonSerializer());
}
@Override
protected String getExtension() {
return "json";
}
}
/*xxx: cas已注册服务插件json序列化器*/
public class CasAddonsRegisteredServicesJsonSerializer extends DefaultRegisteredServiceJsonSerializer {
@Override
public Collection<RegisteredService> load(final InputStream stream) {
final List<RegisteredService> results = new ArrayList<>();
final Map<String, List> servicesMap = this.objectMapper.readValue(stream, Map.class);
final Iterator<Map> it = servicesMap.get("services").iterator();
/*xxx: 将文件内容的json内容,转换为实体*/
final RegisteredService svc = convertServiceProperties(record);
results.add(svc);
return svc;
}
}
# 基于内存的已注册服务注册表
public class InMemoryServiceRegistry extends AbstractServiceRegistry {
@Override
public List<RegisteredService> load() {
final List<RegisteredService> services = new ArrayList<>();
return services;
}
}
public class ImmutableInMemoryServiceRegistry extends InMemoryServiceRegistry implements ImmutableServiceRegistry {
@Override
public RegisteredService save(final RegisteredService registeredService) {
return registeredService;
}
}
# 服务注册表的配置
@ConditionalOnProperty(prefix = "cas.serviceRegistry", name = "initFromJson", havingValue = "true")
public class CasServiceRegistryInitializationConfiguration {
@RefreshScope
@Bean
public ServiceRegistry embeddedJsonServiceRegistry() {
final Resource location = getServiceRegistryInitializerServicesDirectoryResource();
return new EmbeddedResourceBasedServiceRegistry(eventPublisher, location);
}
/*xxx: 如果没有配置,则用默认的 services作为配置项*/
private Resource getServiceRegistryInitializerServicesDirectoryResource() {
final JsonServiceRegistryProperties registry = casProperties.getServiceRegistry().getJson();
return ObjectUtils.defaultIfNull(registry.getLocation(), new ClassPathResource("services"));
}
}
# 普通服务
# 普通服务与已注册服务的区别
- 普通服务,是认证服务器,对外来请求信息的简单封装
- 已注册服务,代表了外来的服务应该怎么处理,是否需要认证,是否需要认证,颁发授权的方式,采用的登录主题等等
# 普通服务的抽象设计
/*xxx: 认证凭证*/
public interface Principal extends Serializable {
/*xxx: 凭证id*/
String getId();
/*xxx: 凭证属性集合*/
default Map<String, Object> getAttributes() {
return new LinkedHashMap<>(0);
}
}
/*xxx: 普通服务 */
public interface Service extends Principal {
/*xxx: 普通服务 */
public interface Service extends Principal {
/*xxx: 设置凭据*/
default void setPrincipal(String principal) {}
}
}
/*xxx: web应用程序服务 */
public interface WebApplicationService extends Service {
/*xxx: 获取 web服务制品 id*/
String getArtifactId();
/*xxx: 获取原始请求的url */
String getOriginalUrl();
/*xxx: 省略其它抽象...*/
}
public abstract class AbstractWebApplicationService implements WebApplicationService {
private String id;
private String artifactId;
private String originalUrl;
protected AbstractWebApplicationService(final String id, final String originalUrl, final String artifactId) {
this.id = id;
this.originalUrl = originalUrl;
this.artifactId = artifactId;
}
}
# 普通服务的生效机制
public class WebApplicationServiceFactory extends AbstractServiceFactory<WebApplicationService> {
public class WebApplicationServiceFactory extends AbstractServiceFactory<WebApplicationService> {
@Override
/*xxx: id代表 重定向地址,重定向地址通过重定向参数获取而来 */
public WebApplicationService createService(final String id) {
return newWebApplicationService(HttpRequestUtils.getHttpServletRequestFromRequestAttributes(), id);
}
}
/*xxx: 通过 http请求,创建服务 */
protected static AbstractWebApplicationService newWebApplicationService(final HttpServletRequest request,
final String serviceToUse) {
final String artifactId = request != null ? request.getParameter("ticket") : null;
/*xxx: serviceToUse 代表的是 客户端后端服务器的地址 */
/*xxx: 将请求中的 查询参数,以及 参数 移除*/
final String id = cleanupUrl(serviceToUse);
/*xxx: 没有参数的 url ->id*/
/*xxx: 完整参数的 url -> originalUrl*/
/*xxx: ticket -> artifactId*/
final AbstractWebApplicationService newService = new SimpleWebApplicationServiceImpl(id, serviceToUse, artifactId);
}
}
# cas服务主题配置
# 主题解析器运行原理
- Spring MVC中通过ThemeSource接口来提供对动态更换样式的支持,并提供了ResourceBundleThemeSource这个具体实现类来提供通 过properties配置文件对theme中的样式的配置
- 例如配置文件中的内容为 helloworld=theme/default/css/helloworld.css 而jsp文件中使用 来引用对helloworld这个样式文件的引入
# 已注册服务主题解析器逻辑设计
/*xxx: 已注册服务主题解析器 */
public class RegisteredServiceThemeResolver extends AbstractThemeResolver {
/*xxx: 服务管理器*/
private final ServicesManager servicesManager;
@Override
public String resolveThemeName(final HttpServletRequest request) {
final Service service = this.authenticationRequestServiceSelectionStrategies.resolveService(serviceContext);
final RegisteredService rService = this.servicesManager.findServiceBy(service);
/*xxx: 简化之后的方法...,省略其它抽象... */
return rService.getTheme();
}
}
# CAS服务端登出设计
# 登出流程集成设计
<end-state id="redirectView" view="externalRedirect:#{flowScope.logoutRedirectUrl}"/>
//package org.springframework.web.servlet.view;
public class RedirectView extends AbstractUrlBasedView implements SmartView {
}
# 登出说明
- externalRedirect,重定向流程的外部页面
- flowRedirect,重定向到另一个流程中
# CAS客户端
# 集成设计
@Component
public class CasClientConfiguration {
@Bean
/*xxx: 认证过滤器*/
public FilterRegistrationBean<AuthenticationFilter> casAuthenticationFilterRegistrationBean(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
AuthenticationFilter authenticationFilter = new AuthenticationFilter();
registrationBean.setName("casAuthenticationFilter");
registrationBean.setFilter(authenticationFilter);
registrationBean.addInitParameter("casServerLoginUrl",clientProperties.getCasServerLoginUrl());
registrationBean.addInitParameter("serverName",clientProperties.getServerName());
return registrationBean;
}
@Bean
/*xxx: ticket校验过滤器*/
public FilterRegistrationBean<Cas30ProxyReceivingTicketValidationFilter> ticketValidationFilterFilterRegistrationBean(){
FilterRegistrationBean<Cas30ProxyReceivingTicketValidationFilter> registrationBean = new FilterRegistrationBean<Cas30ProxyReceivingTicketValidationFilter>();
Cas30ProxyReceivingTicketValidationFilter filter = new Cas30ProxyReceivingTicketValidationFilter();
registrationBean.setName("ticketValidationFilter");
registrationBean.addInitParameter("casServerUrlPrefix",clientProperties.getCasServerUrlPrefix());
registrationBean.addInitParameter("serverName",clientProperties.getServerName());
registrationBean.addInitParameter("redirectAfterValidation","true");
registrationBean.addInitParameter("useSession","true");
registrationBean.setFilter(filter);
return registrationBean;
}
@Bean
/*xxx: 请求包装过滤器*/
public FilterRegistrationBean<HttpServletRequestWrapperFilter> httpServletRequestWrapperFilterFilterRegistrationBean(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
HttpServletRequestWrapperFilter filter = new HttpServletRequestWrapperFilter();
registrationBean.setFilter(filter);
return registrationBean;
}
}
# 请求包装过滤器
public final class HttpServletRequestWrapperFilter extends AbstractConfigurationFilter {
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
final AttributePrincipal principal = retrievePrincipalFromSessionOrRequest(servletRequest);
filterChain.doFilter(new CasHttpServletRequestWrapper((HttpServletRequest) servletRequest, principal),
servletResponse);
}
protected AttributePrincipal retrievePrincipalFromSessionOrRequest(final ServletRequest servletRequest) {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpSession session = request.getSession(false);
/*xxx: 票据是否验证通过的标志*/
final Assertion assertion = (Assertion) (session == null ? request
.getAttribute("_const_cas_assertion_") : session
.getAttribute("_const_cas_assertion_"));
return assertion == null ? null : assertion.getPrincipal();
}
}
final class CasHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final AttributePrincipal principal;
/*xxx: 获取认证信息*/
public Principal getUserPrincipal() {
return this.principal;
}
/*xxx: 获取认证的用户名*/
public String getRemoteUser() {
return principal != null ? this.principal.getName() : null;
}
}
# 认证过滤器
public abstract class AbstractCasFilter extends AbstractConfigurationFilter {
private String serverName;
private String service;
private Protocol protocol;
}
public class AuthenticationFilter extends AbstractCasFilter {
/*xxx: 认证重定向策略*/
private AuthenticationRedirectStrategy authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy();
/*xxx: url忽略匹配策略*/
private UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass = null;
public AuthenticationFilter() {
this(Protocol.CAS2);
}
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
/*xxx: 跳过认证*/
if (isRequestUrlExcluded(request)) {
filterChain.doFilter(request, response);
return;
}
final HttpSession session = request.getSession(false);
final Assertion assertion = session != null ? (Assertion) session.getAttribute("_const_cas_assertion_") : null;
/*xxx: 已经认证过,则跳过*/
if (assertion != null) {
filterChain.doFilter(request, response);
return;
}
final String serviceUrl = constructServiceUrl(request, response);
/*xxx: 从请求提取 ticket信息*/
final String ticket = retrieveTicketFromRequest(request);
/*xxx: 请求是否已经被网关记录过*/
final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
/*xxx: 请求已经被网关记录过,或者存在 ticket,则放行*/
if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {
filterChain.doFilter(request, response);
return;
}
final String modifiedServiceUrl;
if (this.gateway) {
/*xxx: 通过网关记录*/
modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
} else {
modifiedServiceUrl = serviceUrl;
}
final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl,
getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
/*xxx: 重定向到 认证服务器进行 认证 */
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
}
/*xxx: 是否跳过认证*/
private boolean isRequestUrlExcluded(final HttpServletRequest request) {
if (this.ignoreUrlPatternMatcherStrategyClass == null) {
return false;
}
final StringBuffer urlBuffer = request.getRequestURL();
if (request.getQueryString() != null) {
urlBuffer.append("?").append(request.getQueryString());
}
final String requestUri = urlBuffer.toString();
return this.ignoreUrlPatternMatcherStrategyClass.matches(requestUri);
}
}
# ticket校验过滤器
public abstract class AbstractTicketValidationFilter extends AbstractCasFilter {
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
/*xxx: 前置过滤*/
if (!preFilter(servletRequest, servletResponse, filterChain)) {
return;
}
/*xxx: 从请求中,提取ticket*/
final String ticket = retrieveTicketFromRequest(request);
/*xxx: 通过票据验证器进行验证*/
final Assertion assertion = this.ticketValidator.validate(ticket,
constructServiceUrl(request, response));
/*xxx: 验证成功后,将之存储在 request域,以及session域*/
request.setAttribute("_const_cas_assertion_", assertion);
if (this.useSession) {
request.getSession().setAttribute("_const_cas_assertion_", assertion);
}
if (this.redirectAfterValidation) {
/*xxx: 验证成功后进行重定向*/
response.sendRedirect(constructServiceUrl(request, response));
return;
}
/*xxx: 否则继续往下执行过滤链*/
filterChain.doFilter(request, response);
}
protected abstract TicketValidator getTicketValidator(final FilterConfig filterConfig);
}
public class Cas20ProxyReceivingTicketValidationFilter extends AbstractTicketValidationFilter {
/*xxx: 票据验证器默认实现类*/
protected Class<? extends Cas20ServiceTicketValidator> defaultServiceTicketValidatorClass;
protected final boolean preFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
/*xxx: 跟代理有关的啥东东... 懒得看了*/
final String requestUri = request.getRequestURI();
if (CommonUtils.isEmpty(this.proxyReceptorUrl) || !requestUri.endsWith(this.proxyReceptorUrl)) {
return true;
}
try {
CommonUtils.readAndRespondToProxyReceptorRequest(request, response, this.proxyGrantingTicketStorage);
} catch (final RuntimeException e) {
throw e;
}
return false;
}
protected final TicketValidator getTicketValidator(final FilterConfig filterConfig) {
final Cas20ServiceTicketValidator validator = createNewTicketValidator(ticketValidatorClass, casServerUrlPrefix,
this.defaultServiceTicketValidatorClass);
/*xxx: 省略其它抽象...*/
return validator;
}
}
public class Cas30ProxyReceivingTicketValidationFilter extends Cas20ProxyReceivingTicketValidationFilter {
public Cas30ProxyReceivingTicketValidationFilter() {
super(Protocol.CAS3);
this.defaultServiceTicketValidatorClass = Cas30ServiceTicketValidator.class;
}
}
# 票据验证器
public interface TicketValidator {
Assertion validate(String ticket, String service) throws TicketValidationException;
}
public abstract class AbstractUrlBasedTicketValidator implements TicketValidator {
private HttpURLConnectionFactory urlConnectionFactory = new HttpsURLConnectionFactory();
protected abstract String retrieveResponseFromServer(URL validationUrl, String ticket);
public final Assertion validate(final String ticket, final String service) throws TicketValidationException {
final String validationUrl = constructValidationUrl(ticket, service);
final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);
return parseResponseFromServer(serverResponse);
}
}
public abstract class AbstractCasProtocolUrlBasedTicketValidator extends AbstractUrlBasedTicketValidator {
protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
return CommonUtils.getResponseFromServer(validationUrl, getURLConnectionFactory(), getEncoding());
}
}
public class Cas20ServiceTicketValidator extends AbstractCasProtocolUrlBasedTicketValidator {
protected String getUrlSuffix() {
return "serviceValidate";
}
/*xxx: 从服务端响应中,获取认证信息*/
protected final Assertion parseResponseFromServer(final String response) throws TicketValidationException {
final String principal = XmlUtils.getTextForElement(response, "user");
final Map<String, Object> attributes = extractCustomAttributes(response);
/*xxx: 省略其它抽象...*/
final Assertion assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes));
return assertion;
}
}
public class Cas30ServiceTicketValidator extends Cas20ServiceTicketValidator {
@Override
protected String getUrlSuffix() {
return "p3/serviceValidate";
}
}
# 集群登出过滤器
# 单点登出关键要素
- 拦截登出请求的过滤器
- 登出请求后,销毁对应的session的监听器
# 单点登出过滤器逻辑设计
public final class SingleSignOutFilter extends AbstractConfigurationFilter {
/*xxx: 登出处理器*/
private static final SingleSignOutHandler HANDLER = new SingleSignOutHandler();
public void init(final FilterConfig filterConfig) throws ServletException {
/*xxx: 省略其它抽象...*/
HANDLER.init();
}
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
if (!this.handlerInitialized.getAndSet(true)) {
HANDLER.init();
}
if (HANDLER.process(request, response)) {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
# 单点登出处理器
public final class SingleSignOutHandler {
public boolean process(final HttpServletRequest request, final HttpServletResponse response) {
/*xxx: 如果是认证请求*/
if (isTokenRequest(request)) {
/*xxx: 将 令牌与 session 绑定起来 */
recordSession(request);
return true;
/*xxx: 判断是否是后端登出请求(从认证服务器发起的)*/
} else if (isBackChannelLogoutRequest(request)) {
/*xxx: 本质上是调用 session.invalidate()*/
destroySession(request);
return false;
/*xxx: 判断是否是 前端登出请求 */
} else if (isFrontChannelLogoutRequest(request)) {
/*xxx: 本质上是调用 session.invalidate()*/
destroySession(request);
/*xxx: 重定向到cas服务器的地址*/
final String redirectionUrl = computeRedirectionToServer(request);
if (redirectionUrl != null) {
CommonUtils.sendRedirect(response, redirectionUrl);
}
return false;
} else {
return true;
}
}
}
# 单点登出处理器的拦截条件
- cas客户端认证(验证token)时
- 后端登出请求
- 前端登出请求
# 单点登出session监听器
public final class SingleSignOutHttpSessionListener implements HttpSessionListener {
public void sessionDestroyed(final HttpSessionEvent event) {
if (sessionMappingStorage == null) {
sessionMappingStorage = getSessionMappingStorage();
}
final HttpSession session = event.getSession();
sessionMappingStorage.removeBySessionById(session.getId());
}
}
public interface SessionMappingStorage {
/*xxx: 通过token移除数据*/
HttpSession removeSessionByMappingId(String mappingId);
/*xxx: 通过sessionId移除数据*/
void removeBySessionById(String sessionId);
/*xxx: 添加token与session的绑定*/
void addSessionById(String mappingId, HttpSession session);
}