feat: 优化,解耦
This commit is contained in:
parent
8cdc212d78
commit
a9323e4f9b
5
pom.xml
5
pom.xml
@ -45,6 +45,11 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>dev.flyfish.boot</groupId>
|
||||
<artifactId>spring-webflux-cas-client-starter</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
@ -1,99 +0,0 @@
|
||||
package dev.flyfish.boot.cas.config;
|
||||
|
||||
import dev.flyfish.boot.cas.config.resolver.CASUserArgumentResolver;
|
||||
import dev.flyfish.boot.cas.config.session.WebSessionDecorator;
|
||||
import dev.flyfish.boot.cas.config.session.WebSessionListener;
|
||||
import dev.flyfish.boot.cas.filter.CASFilter;
|
||||
import dev.flyfish.boot.cas.filter.CASParameter;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.session.DefaultWebSessionManager;
|
||||
import org.springframework.web.server.session.InMemoryWebSessionStore;
|
||||
import org.springframework.web.server.session.WebSessionManager;
|
||||
import org.springframework.web.server.session.WebSessionStore;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* cas核心配置
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
@Configuration
|
||||
public class CASConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
|
||||
configurer.addCustomResolver(new CASUserArgumentResolver());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties("cas.filter")
|
||||
public CASParameter casParameter() {
|
||||
return new CASParameter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CASFilter casFilter(CASParameter casParameter) {
|
||||
return new CASFilter(casParameter);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSessionStore webSessionStore(WebSessionManager webSessionManager, ServerProperties serverProperties,
|
||||
ObjectProvider<WebSessionListener> listeners) {
|
||||
if (webSessionManager instanceof DefaultWebSessionManager defaultWebSessionManager) {
|
||||
Duration timeout = serverProperties.getReactive().getSession().getTimeout();
|
||||
int maxSessions = serverProperties.getReactive().getSession().getMaxSessions();
|
||||
ListenableWebSessionStore sessionStore = new ListenableWebSessionStore(timeout, listeners);
|
||||
sessionStore.setMaxSessions(maxSessions);
|
||||
defaultWebSessionManager.setSessionStore(sessionStore);
|
||||
return sessionStore;
|
||||
}
|
||||
throw new IllegalStateException("Cannot find web session manager");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理session销毁,保证正确退出
|
||||
*
|
||||
* @param casFilter 过滤器
|
||||
* @return 结果
|
||||
*/
|
||||
@Bean
|
||||
public WebSessionListener singleSignOutSessionListener(CASFilter casFilter) {
|
||||
return new WebSessionListener() {
|
||||
@Override
|
||||
public Mono<Void> onSessionInvalidated(WebSession session) {
|
||||
return casFilter.getSessionMappingStorage().removeBySessionById(session.getId());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static final class ListenableWebSessionStore extends InMemoryWebSessionStore {
|
||||
private final Duration timeout;
|
||||
private final List<WebSessionListener> listeners;
|
||||
|
||||
private ListenableWebSessionStore(Duration timeout, ObjectProvider<WebSessionListener> listeners) {
|
||||
this.timeout = timeout;
|
||||
this.listeners = listeners.stream().toList();
|
||||
}
|
||||
|
||||
public Mono<WebSession> createWebSession() {
|
||||
return super.createWebSession()
|
||||
.map(session -> (WebSession) new WebSessionDecorator(session, listeners))
|
||||
.doOnSuccess(this::setMaxIdleTime);
|
||||
}
|
||||
|
||||
private void setMaxIdleTime(WebSession session) {
|
||||
session.setMaxIdleTime(this.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package dev.flyfish.boot.cas.config.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.PARAMETER)
|
||||
public @interface CASUser {
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package dev.flyfish.boot.cas.config.resolver;
|
||||
|
||||
import dev.flyfish.boot.cas.config.annotation.CASUser;
|
||||
import dev.flyfish.boot.cas.filter.CASLoginFilter;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.web.reactive.BindingContext;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolverSupport;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public class CASUserArgumentResolver extends HandlerMethodArgumentResolverSupport {
|
||||
|
||||
public CASUserArgumentResolver() {
|
||||
super(ReactiveAdapterRegistry.getSharedInstance());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return checkAnnotatedParamNoReactiveWrapper(parameter, CASUser.class, (anno, type) -> true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
|
||||
return exchange.getSession().mapNotNull(session -> session.getAttribute(CASLoginFilter.CONST_CAS_USERNAME));
|
||||
}
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
package dev.flyfish.boot.cas.config.session;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class WebSessionDecorator implements WebSession {
|
||||
|
||||
private final WebSession decorated;
|
||||
|
||||
private final List<WebSessionListener> listeners;
|
||||
|
||||
/**
|
||||
* Return a unique session identifier.
|
||||
*/
|
||||
@Override
|
||||
public String getId() {
|
||||
return decorated.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a map that holds session attributes.
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return decorated.getAttributes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the session attribute value if present.
|
||||
*
|
||||
* @param name the attribute name
|
||||
* @return the attribute value
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public <T> T getAttribute(String name) {
|
||||
return decorated.getAttribute(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the session attribute value or if not present raise an
|
||||
* {@link IllegalArgumentException}.
|
||||
*
|
||||
* @param name the attribute name
|
||||
* @return the attribute value
|
||||
*/
|
||||
@Override
|
||||
public <T> T getRequiredAttribute(String name) {
|
||||
return decorated.getRequiredAttribute(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the session attribute value, or a default, fallback value.
|
||||
*
|
||||
* @param name the attribute name
|
||||
* @param defaultValue a default value to return instead
|
||||
* @return the attribute value
|
||||
*/
|
||||
@Override
|
||||
public <T> T getAttributeOrDefault(String name, T defaultValue) {
|
||||
return decorated.getAttributeOrDefault(name, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the creation of a session causing the session id to be sent when
|
||||
* {@link #save()} is called.
|
||||
*/
|
||||
@Override
|
||||
public void start() {
|
||||
decorated.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a session with the client has been started explicitly via
|
||||
* {@link #start()} or implicitly by adding session attributes.
|
||||
* If "false" then the session id is not sent to the client and the
|
||||
* {@link #save()} method is essentially a no-op.
|
||||
*/
|
||||
@Override
|
||||
public boolean isStarted() {
|
||||
return decorated.isStarted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new id for the session and update the underlying session
|
||||
* storage to reflect the new id. After a successful call {@link #getId()}
|
||||
* reflects the new session id.
|
||||
*
|
||||
* @return completion notification (success or error)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> changeSessionId() {
|
||||
return decorated.changeSessionId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the current session and clear session storage.
|
||||
*
|
||||
* @return completion notification (success or error)
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> invalidate() {
|
||||
// 后续处理
|
||||
Mono<Void> consumer = Mono.defer(() -> listeners.stream()
|
||||
.map(listener -> listener.onSessionInvalidated(this.decorated))
|
||||
.reduce(Mono::then)
|
||||
.orElse(Mono.empty()));
|
||||
|
||||
return decorated.invalidate().then(consumer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the session through the {@code WebSessionStore} as follows:
|
||||
* <ul>
|
||||
* <li>If the session is new (i.e. created but never persisted), it must have
|
||||
* been started explicitly via {@link #start()} or implicitly by adding
|
||||
* attributes, or otherwise this method should have no effect.
|
||||
* <li>If the session was retrieved through the {@code WebSessionStore},
|
||||
* the implementation for this method must check whether the session was
|
||||
* {@link #invalidate() invalidated} and if so return an error.
|
||||
* </ul>
|
||||
* <p>Note that this method is not intended for direct use by applications.
|
||||
* Instead it is automatically invoked just before the response is
|
||||
* committed.
|
||||
*
|
||||
* @return {@code Mono} to indicate completion with success or error
|
||||
*/
|
||||
@Override
|
||||
public Mono<Void> save() {
|
||||
return decorated.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return {@code true} if the session expired after {@link #getMaxIdleTime()
|
||||
* maxIdleTime} elapsed.
|
||||
* <p>Typically expiration checks should be automatically made when a session
|
||||
* is accessed, a new {@code WebSession} instance created if necessary, at
|
||||
* the start of request processing so that applications don't have to worry
|
||||
* about expired session by default.
|
||||
*/
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return decorated.isExpired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the time when the session was created.
|
||||
*/
|
||||
@Override
|
||||
public Instant getCreationTime() {
|
||||
return decorated.getCreationTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last time of session access as a result of user activity such
|
||||
* as an HTTP request. Together with {@link #getMaxIdleTime()
|
||||
* maxIdleTimeInSeconds} this helps to determine when a session is
|
||||
* {@link #isExpired() expired}.
|
||||
*/
|
||||
@Override
|
||||
public Instant getLastAccessTime() {
|
||||
return decorated.getLastAccessTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the max amount of time that may elapse after the
|
||||
* {@link #getLastAccessTime() lastAccessTime} before a session is considered
|
||||
* expired. A negative value indicates the session should not expire.
|
||||
*
|
||||
* @param maxIdleTime
|
||||
*/
|
||||
@Override
|
||||
public void setMaxIdleTime(Duration maxIdleTime) {
|
||||
decorated.setMaxIdleTime(maxIdleTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the maximum time after the {@link #getLastAccessTime()
|
||||
* lastAccessTime} before a session expires. A negative time indicates the
|
||||
* session doesn't expire.
|
||||
*/
|
||||
@Override
|
||||
public Duration getMaxIdleTime() {
|
||||
return decorated.getMaxIdleTime();
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package dev.flyfish.boot.cas.config.session;
|
||||
|
||||
import org.springframework.web.server.WebSession;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* web session监听器
|
||||
*
|
||||
* @author wangyu
|
||||
* 基于装饰器增强实现,可灵活处理
|
||||
*/
|
||||
public interface WebSessionListener {
|
||||
|
||||
default Mono<Void> onSessionCreated(WebSession session) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
default Mono<Void> onSessionInvalidated(WebSession session) {
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
package dev.flyfish.boot.cas.context;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* cas 上下文
|
||||
*/
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class CASContext {
|
||||
|
||||
@Getter
|
||||
private String ticket;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private WebSession session;
|
||||
|
||||
private final ServerWebExchange exchange;
|
||||
|
||||
private final WebFilterChain chain;
|
||||
|
||||
private final Map<String, Mono<String>> parameters = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
private String username;
|
||||
|
||||
public static Mono<CASContext> create(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
return new CASContext(exchange, chain).init();
|
||||
}
|
||||
|
||||
private Mono<CASContext> init() {
|
||||
Mono<String> ticketMono = getParameter("ticket")
|
||||
.filter(StringUtils::hasText)
|
||||
.doOnNext(ticket -> this.ticket = ticket);
|
||||
// 此处必须保证session不为空
|
||||
Mono<WebSession> sessionMono = exchange.getSession()
|
||||
.doOnNext(session -> this.session = session);
|
||||
return Mono.zipDelayError(ticketMono, sessionMono)
|
||||
.onErrorContinue((e, v) -> e.printStackTrace())
|
||||
.thenReturn(this);
|
||||
}
|
||||
|
||||
public boolean isTokenRequest() {
|
||||
return StringUtils.hasText(ticket);
|
||||
}
|
||||
|
||||
public Mono<Void> filter() {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
public Mono<Void> redirect(String url) {
|
||||
ServerHttpResponse response = exchange.getResponse();
|
||||
response.setRawStatusCode(HttpStatus.FOUND.value());
|
||||
response.getHeaders().setLocation(URI.create(url));
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
public ServerHttpRequest getRequest() {
|
||||
return exchange.getRequest();
|
||||
}
|
||||
|
||||
public ServerHttpResponse getResponse() {
|
||||
return exchange.getResponse();
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return exchange.getRequest().getPath().value();
|
||||
}
|
||||
|
||||
public HttpMethod getMethod() {
|
||||
return exchange.getRequest().getMethod();
|
||||
}
|
||||
|
||||
public String getQuery(String key) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
return request.getQueryParams().getFirst(key);
|
||||
}
|
||||
|
||||
public Mono<String> getFormData(String key) {
|
||||
return exchange.getFormData()
|
||||
.mapNotNull(formData -> formData.getFirst(key));
|
||||
}
|
||||
|
||||
public void setSessionAttribute(String key, Object value) {
|
||||
session.getAttributes().put(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数
|
||||
*
|
||||
* @param key 键
|
||||
* @return 异步结果
|
||||
*/
|
||||
private Mono<String> getParameter(String key) {
|
||||
return parameters.computeIfAbsent(key, this::computeParameter);
|
||||
}
|
||||
|
||||
private Mono<String> computeParameter(String key) {
|
||||
return this.readParameter(key).cache();
|
||||
}
|
||||
|
||||
private Mono<String> readParameter(String key) {
|
||||
String query = getQuery(key);
|
||||
if (StringUtils.hasText(query)) {
|
||||
return Mono.just(query);
|
||||
}
|
||||
MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
|
||||
if (null != mediaType && mediaType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) {
|
||||
return exchange.getFormData().mapNotNull(formData -> formData.getFirst(key));
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package dev.flyfish.boot.cas.context;
|
||||
|
||||
/**
|
||||
* 上下文初始化逻辑
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
public interface CASContextInit {
|
||||
|
||||
String getTranslatorUser(String username);
|
||||
|
||||
void initContext(CASContext casContext, String username);
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package dev.flyfish.boot.cas.context;
|
||||
|
||||
import dev.flyfish.boot.cas.exception.CASAuthenticationException;
|
||||
import dev.flyfish.boot.cas.validator.ProxyTicketValidator;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Setter
|
||||
@ToString
|
||||
public class CASReceipt implements Serializable {
|
||||
|
||||
private static final Log log = LogFactory.getLog(CASReceipt.class);
|
||||
@Getter
|
||||
private String casValidateUrl;
|
||||
@Getter
|
||||
private String pgtIou;
|
||||
@Getter
|
||||
private boolean primaryAuthentication = false;
|
||||
@Getter
|
||||
private String proxyCallbackUrl;
|
||||
|
||||
private List<?> proxyList = new ArrayList<>();
|
||||
@Getter
|
||||
private String userName;
|
||||
|
||||
public static CASReceipt getReceipt(ProxyTicketValidator ptv) throws CASAuthenticationException {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("entering getReceipt(ProxyTicketValidator=[" + ptv + "])");
|
||||
}
|
||||
|
||||
if (!ptv.isAuthenticationSuccessful()) {
|
||||
try {
|
||||
ptv.validate();
|
||||
} catch (Exception e) {
|
||||
CASAuthenticationException casException = new CASAuthenticationException("Unable to validate ProxyTicketValidator [" + ptv + "]", e);
|
||||
log.error(casException);
|
||||
throw casException;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ptv.isAuthenticationSuccessful()) {
|
||||
log.error("validation of [" + ptv + "] was not successful.");
|
||||
throw new CASAuthenticationException("Unable to validate ProxyTicketValidator [" + ptv + "]");
|
||||
} else {
|
||||
CASReceipt receipt = new CASReceipt();
|
||||
receipt.casValidateUrl = ptv.getCasValidateUrl();
|
||||
receipt.pgtIou = ptv.getPgtIou();
|
||||
receipt.userName = ptv.getUser();
|
||||
receipt.proxyCallbackUrl = ptv.getProxyCallbackUrl();
|
||||
receipt.proxyList = ptv.getProxyList();
|
||||
receipt.primaryAuthentication = ptv.isRenew();
|
||||
if (!receipt.validate()) {
|
||||
throw new CASAuthenticationException("Validation of [" + ptv + "] did not result in an internally consistent CASReceipt.");
|
||||
} else {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("returning from getReceipt() with return value [" + receipt + "]");
|
||||
}
|
||||
|
||||
return receipt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CASReceipt() {
|
||||
}
|
||||
|
||||
public List<?> getProxyList() {
|
||||
return Collections.unmodifiableList(this.proxyList);
|
||||
}
|
||||
|
||||
public boolean isProxied() {
|
||||
return !this.proxyList.isEmpty();
|
||||
}
|
||||
|
||||
public String getProxyingService() {
|
||||
return this.proxyList.isEmpty() ? null : (String) this.proxyList.getFirst();
|
||||
}
|
||||
|
||||
private boolean validate() {
|
||||
boolean valid = true;
|
||||
if (this.userName == null) {
|
||||
log.error("Receipt was invalid because userName was null. Receipt:[" + this + "]");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (this.casValidateUrl == null) {
|
||||
log.error("Receipt was invalid because casValidateUrl was null. Receipt:[" + this + "]");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (this.proxyList == null) {
|
||||
log.error("receipt was invalid because proxyList was null. Receipt:[" + this + "]");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (this.primaryAuthentication && !this.proxyList.isEmpty()) {
|
||||
log.error("If authentication was by primary credentials then it could not have been proxied. Yet, primaryAuthentication is true where proxyList is not empty. Receipt:[" + this + "]");
|
||||
valid = false;
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
package dev.flyfish.boot.cas.context;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* webflux的session mapping存储
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
public interface SessionMappingStorage {
|
||||
|
||||
Mono<WebSession> removeSessionByMappingId(String mappingId);
|
||||
|
||||
Mono<Void> removeBySessionById(String mappingId);
|
||||
|
||||
Mono<Void> addSessionById(String mappingId, WebSession session);
|
||||
|
||||
@Slf4j
|
||||
class HashMapBackedSessionStorage implements SessionMappingStorage {
|
||||
|
||||
private final Map<String, WebSession> MANAGED_SESSIONS = new HashMap<>();
|
||||
private final Map<String, String> ID_TO_SESSION_KEY_MAPPING = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Mono<WebSession> removeSessionByMappingId(String mappingId) {
|
||||
WebSession session = this.MANAGED_SESSIONS.get(mappingId);
|
||||
if (session != null) {
|
||||
return this.removeBySessionById(session.getId()).thenReturn(session);
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeBySessionById(String sessionId) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Attempting to remove Session=[" + sessionId + "]");
|
||||
}
|
||||
|
||||
String key = this.ID_TO_SESSION_KEY_MAPPING.get(sessionId);
|
||||
if (log.isDebugEnabled()) {
|
||||
if (key != null) {
|
||||
log.debug("Found mapping for session. Session Removed.");
|
||||
} else {
|
||||
log.debug("No mapping for session found. Ignoring.");
|
||||
}
|
||||
}
|
||||
|
||||
this.MANAGED_SESSIONS.remove(key);
|
||||
this.ID_TO_SESSION_KEY_MAPPING.remove(sessionId);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> addSessionById(String mappingId, WebSession session) {
|
||||
this.ID_TO_SESSION_KEY_MAPPING.put(session.getId(), mappingId);
|
||||
this.MANAGED_SESSIONS.put(mappingId, session);
|
||||
return Mono.empty();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
package dev.flyfish.boot.cas.exception;
|
||||
|
||||
public class CASAuthenticationException extends Exception {
|
||||
public CASAuthenticationException(String string) {
|
||||
super(string);
|
||||
}
|
||||
|
||||
public CASAuthenticationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -1,420 +0,0 @@
|
||||
package dev.flyfish.boot.cas.filter;
|
||||
|
||||
import dev.flyfish.boot.cas.context.CASContext;
|
||||
import dev.flyfish.boot.cas.context.CASContextInit;
|
||||
import dev.flyfish.boot.cas.context.CASReceipt;
|
||||
import dev.flyfish.boot.cas.context.SessionMappingStorage;
|
||||
import dev.flyfish.boot.cas.exception.CASAuthenticationException;
|
||||
import dev.flyfish.boot.cas.validator.ProxyTicketValidator;
|
||||
import dev.flyfish.boot.cas.validator.XmlUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* cas filter的webflux实现
|
||||
*
|
||||
* @author wangyu
|
||||
* 实现相关核心逻辑,完成鉴权信息抽取
|
||||
*/
|
||||
@Slf4j
|
||||
public class CASFilter implements WebFilter {
|
||||
|
||||
public static final String LOGIN_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.loginUrl";
|
||||
public static final String VALIDATE_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.validateUrl";
|
||||
public static final String SERVICE_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.serviceUrl";
|
||||
public static final String SERVERNAME_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.serverName";
|
||||
public static final String RENEW_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.renew";
|
||||
public static final String AUTHORIZED_PROXY_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.authorizedProxy";
|
||||
public static final String PROXY_CALLBACK_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.proxyCallbackUrl";
|
||||
public static final String WRAP_REQUESTS_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.wrapRequest";
|
||||
public static final String GATEWAY_INIT_PARAM = "edu.yale.its.tp.cas.client.filter.gateway";
|
||||
public static final String CAS_FILTER_USER = "edu.yale.its.tp.cas.client.filter.user";
|
||||
public static final String CAS_FILTER_RECEIPT = "edu.yale.its.tp.cas.client.filter.receipt";
|
||||
|
||||
static final String CAS_FILTER_GATEWAYED = "edu.yale.its.tp.cas.client.filter.didGateway";
|
||||
static final String CAS_FILTER_INITCONTEXTCLASS = "edu.yale.its.tp.cas.client.filter.initContextClass";
|
||||
static final String CAS_FILTER_USERLOGINMARK = "edu.yale.its.tp.cas.client.filter.userLoginMark";
|
||||
static final String CAS_FILTER_EXCLUSION = "edu.yale.its.tp.cas.client.filter.filterExclusion";
|
||||
|
||||
private final CASParameter parameter;
|
||||
private final CASContextInit initializer;
|
||||
@Getter
|
||||
private final SessionMappingStorage sessionMappingStorage = new SessionMappingStorage.HashMapBackedSessionStorage();
|
||||
|
||||
public CASFilter(CASParameter parameter) {
|
||||
this.parameter = parameter.checked();
|
||||
this.initializer = createInitializer();
|
||||
}
|
||||
|
||||
private CASContextInit createInitializer() {
|
||||
if (null != parameter.casInitContextClass) {
|
||||
try {
|
||||
// 未正确配置类型,抛弃
|
||||
Class<? extends CASContextInit> cls = parameter.casInitContextClass.asSubclass(CASContextInit.class);
|
||||
// 实例化对象并返回
|
||||
return cls.getConstructor().newInstance();
|
||||
} catch (ClassCastException e) {
|
||||
log.warn("cas context init class not implements CASContextInit", e);
|
||||
} catch (IllegalArgumentException | IllegalAccessException | InstantiationException
|
||||
| SecurityException | NoSuchMethodException e) {
|
||||
log.error("error when initialize the context init class", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
log.error("error when create the cas context initializer's instance!", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isReceiptAcceptable(CASReceipt receipt) {
|
||||
if (receipt == null) {
|
||||
throw new IllegalArgumentException("Cannot evaluate a null receipt.");
|
||||
} else if (parameter.casRenew && !receipt.isPrimaryAuthentication()) {
|
||||
return false;
|
||||
} else {
|
||||
return !receipt.isProxied() || parameter.authorizedProxies.contains(receipt.getProxyingService());
|
||||
}
|
||||
}
|
||||
|
||||
private CASReceipt getAuthenticatedUser(CASContext context) throws CASAuthenticationException {
|
||||
log.trace("entering getAuthenticatedUser()");
|
||||
ProxyTicketValidator pv = new ProxyTicketValidator();
|
||||
pv.setCasValidateUrl(parameter.casValidate);
|
||||
pv.setServiceTicket(context.getTicket());
|
||||
pv.setService(this.getService(context));
|
||||
pv.setRenew(parameter.casRenew);
|
||||
if (parameter.casProxyCallbackUrl != null) {
|
||||
pv.setProxyCallbackUrl(parameter.casProxyCallbackUrl);
|
||||
}
|
||||
|
||||
log.debug("about to validate ProxyTicketValidator: [{}]", pv);
|
||||
|
||||
return CASReceipt.getReceipt(pv);
|
||||
}
|
||||
|
||||
private String getService(CASContext context) {
|
||||
log.trace("entering getService()");
|
||||
|
||||
if (parameter.casServerName == null && parameter.casServiceUrl == null) {
|
||||
throw new IllegalArgumentException("need one of the following configuration parameters: edu.yale.its.tp.cas.client.filter.serviceUrl or edu.yale.its.tp.cas.client.filter.serverName");
|
||||
}
|
||||
|
||||
String serviceString;
|
||||
if (parameter.casServiceUrl != null) {
|
||||
serviceString = URLEncoder.encode(parameter.casServiceUrl, StandardCharsets.UTF_8);
|
||||
} else {
|
||||
serviceString = computeService(context, parameter.getFullServerUrl());
|
||||
}
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("returning from getService() with service [{}]", serviceString);
|
||||
}
|
||||
return serviceString;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算服务地址,主要是替换url中server的部分,并去除ticket
|
||||
*
|
||||
* @param context 上下文
|
||||
* @param server 服务
|
||||
* @return 结果
|
||||
*/
|
||||
public String computeService(CASContext context, String server) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("entering getService({}, {})", context, server);
|
||||
}
|
||||
|
||||
if (server == null) {
|
||||
log.error("getService() argument \"server\" was illegally null.");
|
||||
throw new IllegalArgumentException("name of server is required");
|
||||
}
|
||||
|
||||
URI uri = context.getRequest().getURI();
|
||||
|
||||
StringBuilder sb = new StringBuilder(server).append(uri.getPath());
|
||||
|
||||
if (uri.getQuery() != null) {
|
||||
String query = uri.getQuery();
|
||||
|
||||
int ticketLoc = query.indexOf("ticket=");
|
||||
if (ticketLoc == -1) {
|
||||
sb.append("?").append(query);
|
||||
} else if (ticketLoc > 0) {
|
||||
ticketLoc = query.indexOf("&ticket=");
|
||||
if (ticketLoc == -1) {
|
||||
sb.append("?").append(query);
|
||||
} else if (ticketLoc > 0) {
|
||||
sb.append("?").append(query, 0, ticketLoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String encodedService = URLEncoder.encode(sb.toString(), StandardCharsets.UTF_8);
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("returning from getService() with encoded service [{}]", encodedService);
|
||||
}
|
||||
|
||||
return encodedService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心,跳转cas服务器鉴权
|
||||
*
|
||||
* @param context 上下文
|
||||
* @return 结果
|
||||
*/
|
||||
private Mono<Void> redirectToCAS(CASContext context) {
|
||||
ServerHttpRequest request = context.getRequest();
|
||||
String sessionId = context.getSession().getId();
|
||||
|
||||
log.trace("entering redirectToCAS()");
|
||||
|
||||
StringBuilder casLoginString = new StringBuilder()
|
||||
.append(parameter.casLogin)
|
||||
.append("?service=").append(this.getService(context))
|
||||
.append(parameter.casRenew ? "&renew=true" : "")
|
||||
.append(parameter.casGateway ? "&gateway=true" : "");
|
||||
|
||||
if (StringUtils.hasText(sessionId)) {
|
||||
String appId = parameter.casServerName + request.getPath().contextPath().value();
|
||||
casLoginString.append("&appId=").append(URLEncoder.encode(appId, StandardCharsets.UTF_8))
|
||||
.append("&sessionId=").append(sessionId);
|
||||
}
|
||||
|
||||
List<HttpCookie> cookies = request.getCookies().get("JSESSIONID");
|
||||
if (!CollectionUtils.isEmpty(cookies)) {
|
||||
cookies.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(HttpCookie::getValue)
|
||||
.filter(cookie -> !cookie.equals("null") && !cookie.equals(sessionId))
|
||||
.peek(cookie -> log.debug("Session is timeout. The timeout session is {}", cookie))
|
||||
.findFirst()
|
||||
.ifPresent(cookie -> casLoginString.append("&timeOut=").append(cookie));
|
||||
}
|
||||
|
||||
log.debug("Redirecting browser to [{})", casLoginString);
|
||||
log.trace("returning from redirectToCAS()");
|
||||
|
||||
return context.redirect(casLoginString.toString());
|
||||
}
|
||||
|
||||
private Mono<Void> redirectToInitFailure(CASContext context, String cause) {
|
||||
log.trace("entering redirectToInitFailure()");
|
||||
|
||||
String casLoginString = parameter.casLogin + "?action=initFailure";
|
||||
if (cause != null && cause.equals("Illegal user")) {
|
||||
casLoginString += "&userIllegal=true";
|
||||
}
|
||||
|
||||
String locale = context.getQuery("locale");
|
||||
if (locale != null) {
|
||||
casLoginString += "&locale=" + locale;
|
||||
}
|
||||
|
||||
log.debug("Redirecting browser to [{})", casLoginString);
|
||||
log.trace("returning from redirectToInitFailure()");
|
||||
return context.redirect(casLoginString);
|
||||
}
|
||||
|
||||
private boolean isExclusion(String url) {
|
||||
if (parameter.exclusions == null) {
|
||||
return false;
|
||||
} else {
|
||||
return parameter.exclusions.contains(url);
|
||||
}
|
||||
}
|
||||
|
||||
private Mono<Void> translate(CASContext context) {
|
||||
// 是代理回调地址,通过
|
||||
if (parameter.casProxyCallbackUrl != null && parameter.casProxyCallbackUrl.endsWith(context.getPath())
|
||||
&& context.getQuery("pgtId") != null && context.getQuery("pgtIou") != null) {
|
||||
log.trace("passing through what we hope is CAS's request for proxy ticket receptor.");
|
||||
return context.filter();
|
||||
}
|
||||
|
||||
// 请求包装,增强请求并完成自定义功能
|
||||
if (parameter.wrapRequest) {
|
||||
log.trace("Wrapping request with CASFilterRequestWrapper.");
|
||||
// todo 暂时啥也不干,看看有无问题
|
||||
// request = new CASFilterRequestWrapper((HttpServletRequest) request);
|
||||
}
|
||||
|
||||
WebSession session = context.getSession();
|
||||
Map<String, Object> sessionAttributes = session.getAttributes();
|
||||
|
||||
// 使用了用户标记,快速跳过
|
||||
if (parameter.userLoginMark != null && session.getAttribute(parameter.userLoginMark) != null) {
|
||||
return context.filter();
|
||||
}
|
||||
|
||||
// 获取receipt,若存在,则通过
|
||||
CASReceipt receipt = session.getAttribute(CAS_FILTER_RECEIPT);
|
||||
if (receipt != null && this.isReceiptAcceptable(receipt)) {
|
||||
log.trace("CAS_FILTER_RECEIPT attribute was present and acceptable - passing request through filter..");
|
||||
return context.filter();
|
||||
}
|
||||
|
||||
// 命中排除地址,跳过请求
|
||||
if (this.isExclusion(context.getPath())) {
|
||||
return context.filter();
|
||||
}
|
||||
|
||||
// 判断票据
|
||||
String ticket = context.getTicket();
|
||||
// 存在票据时,验证票据
|
||||
if (StringUtils.hasText(ticket)) {
|
||||
try {
|
||||
receipt = this.getAuthenticatedUser(context);
|
||||
} catch (CASAuthenticationException e) {
|
||||
return this.redirectToCAS(context);
|
||||
}
|
||||
|
||||
if (!this.isReceiptAcceptable(receipt)) {
|
||||
throw new IllegalStateException("Authentication was technically successful but rejected as a matter of policy. [" + receipt + "]");
|
||||
}
|
||||
|
||||
// 记录receipt
|
||||
String pt = context.getQuery("pt");
|
||||
if (StringUtils.hasText(pt)) {
|
||||
context.setSessionAttribute(pt, receipt);
|
||||
}
|
||||
|
||||
// 获取到用户名
|
||||
String userName = receipt.getUserName();
|
||||
// 尝试初始化
|
||||
if (null != initializer) {
|
||||
try {
|
||||
String translated = initializer.getTranslatorUser(userName);
|
||||
log.debug("translated username: {} to {}", userName, translated);
|
||||
initializer.initContext(context, translated);
|
||||
} catch (Exception e) {
|
||||
String cause = e.getCause().getMessage();
|
||||
context.setSessionAttribute("initFailure", cause);
|
||||
return this.redirectToInitFailure(context, cause);
|
||||
}
|
||||
}
|
||||
|
||||
sessionAttributes.put(CAS_FILTER_USER, userName);
|
||||
sessionAttributes.put(CAS_FILTER_RECEIPT, receipt);
|
||||
sessionAttributes.remove(CAS_FILTER_GATEWAYED);
|
||||
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("validated ticket to get authenticated receipt [{}], now passing request along filter chain.", receipt);
|
||||
log.trace("returning from doFilter()");
|
||||
}
|
||||
|
||||
return context.filter();
|
||||
}
|
||||
|
||||
// 不存在票据,跳转验证
|
||||
log.trace("CAS ticket was not present on request.");
|
||||
boolean didGateway = Boolean.parseBoolean(session.getAttribute(CAS_FILTER_GATEWAYED));
|
||||
if (parameter.casLogin == null) {
|
||||
log.error("casLogin was not set, so filter cannot redirect request for authentication.");
|
||||
throw new IllegalArgumentException("When CASFilter protects pages that do not receive a 'ticket' parameter, it needs a edu.yale.its.tp.cas.client.filter.loginUrl filter parameter");
|
||||
}
|
||||
|
||||
if (!didGateway) {
|
||||
log.trace("Did not previously gateway. Setting session attribute to true.");
|
||||
sessionAttributes.put(CAS_FILTER_GATEWAYED, "true");
|
||||
return this.redirectToCAS(context);
|
||||
}
|
||||
|
||||
log.trace("Previously gatewayed.");
|
||||
if (!parameter.casGateway && session.getAttribute(CAS_FILTER_USER) == null) {
|
||||
if (session.getAttribute("initFailure") != null) {
|
||||
String cause = session.getAttribute("initFailure");
|
||||
return this.redirectToInitFailure(context, cause);
|
||||
}
|
||||
|
||||
sessionAttributes.put(CAS_FILTER_GATEWAYED, "true");
|
||||
return this.redirectToCAS(context);
|
||||
}
|
||||
|
||||
log.trace("casGateway was true and CAS_FILTER_USER set: passing request along filter chain.");
|
||||
return context.filter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 二阶段处理,预处理特殊情况,提前中断请求
|
||||
*
|
||||
* @param context 上下文工具
|
||||
* @return 结果
|
||||
*/
|
||||
private Mono<Void> handle(CASContext context) {
|
||||
// 优先处理token请求
|
||||
if (context.isTokenRequest()) {
|
||||
String sessionId = context.getSession().getId();
|
||||
log.debug("Storing session identifier for {}", sessionId);
|
||||
|
||||
// 包括ticket,尝试重新替换session
|
||||
return sessionMappingStorage.removeBySessionById(sessionId)
|
||||
.onErrorContinue((e, v) -> log.debug("error when remove session"))
|
||||
.then(Mono.defer(() -> sessionMappingStorage.addSessionById(context.getTicket(), context.getSession())
|
||||
.then(translate(context))));
|
||||
}
|
||||
// post请求需要特殊处理
|
||||
if (context.getMethod() == HttpMethod.POST) {
|
||||
// 通过form表单获取注销请求,处理注销逻辑
|
||||
return context.getFormData("logoutRequest")
|
||||
.doOnNext(payload -> log.trace("Logout request=[{}]", payload))
|
||||
.defaultIfEmpty("")
|
||||
.flatMap(payload -> {
|
||||
if (StringUtils.hasText(payload)) {
|
||||
String token = XmlUtils.getTextForElement(payload, "SessionIndex");
|
||||
if (StringUtils.hasText(token)) {
|
||||
// 满足条件时断路
|
||||
return sessionMappingStorage.removeSessionByMappingId(token)
|
||||
.doOnNext(session -> log.debug("Invalidating session [{}] for ST [{}]", session.getId(), token))
|
||||
.flatMap(WebSession::invalidate)
|
||||
.doOnError(IllegalStateException.class, e -> log.debug(e.getMessage(), e))
|
||||
.onErrorComplete();
|
||||
}
|
||||
}
|
||||
// 继续执行
|
||||
return translate(context);
|
||||
});
|
||||
}
|
||||
return translate(context);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
|
||||
// 拦截器需要基于session判定,故提前使用
|
||||
return CASContext.create(exchange, chain)
|
||||
.flatMap(context -> {
|
||||
WebSession session = context.getSession();
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("entering doFilter()");
|
||||
}
|
||||
// 执行跳过策略
|
||||
String pt = context.getQuery("pt");
|
||||
if (StringUtils.hasText(pt)) {
|
||||
if (session.getAttribute(pt) != null) {
|
||||
return context.filter();
|
||||
}
|
||||
}
|
||||
return handle(context);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package dev.flyfish.boot.cas.filter;
|
||||
|
||||
import dev.flyfish.boot.cas.context.CASContext;
|
||||
import dev.flyfish.boot.cas.context.CASContextInit;
|
||||
|
||||
/**
|
||||
* 登录过滤器,旨在缓存用户名
|
||||
*
|
||||
* @author wangyu
|
||||
*/
|
||||
public class CASLoginFilter implements CASContextInit {
|
||||
public static String CONST_CAS_USERNAME = "const_cas_username";
|
||||
|
||||
@Override
|
||||
public String getTranslatorUser(String username) {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initContext(CASContext casContext, String username) {
|
||||
casContext.setSessionAttribute(CONST_CAS_USERNAME, username);
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package dev.flyfish.boot.cas.filter;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import lombok.Data;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
@Data
|
||||
public class CASParameter {
|
||||
|
||||
// 排除的过滤地址
|
||||
@JsonAlias(CASFilter.CAS_FILTER_EXCLUSION)
|
||||
Set<String> exclusions;
|
||||
|
||||
@JsonAlias(CASFilter.LOGIN_INIT_PARAM)
|
||||
String casLogin;
|
||||
|
||||
@JsonAlias(CASFilter.VALIDATE_INIT_PARAM)
|
||||
String casValidate;
|
||||
|
||||
@JsonAlias(CASFilter.SERVICE_INIT_PARAM)
|
||||
String casServiceUrl;
|
||||
|
||||
@JsonAlias(CASFilter.SERVERNAME_INIT_PARAM)
|
||||
String casServerName;
|
||||
|
||||
String casServerProtocol = "http";
|
||||
|
||||
@JsonAlias(CASFilter.PROXY_CALLBACK_INIT_PARAM)
|
||||
String casProxyCallbackUrl;
|
||||
|
||||
@JsonAlias(CASFilter.CAS_FILTER_INITCONTEXTCLASS)
|
||||
Class<?> casInitContextClass;
|
||||
|
||||
@JsonAlias(CASFilter.RENEW_INIT_PARAM)
|
||||
boolean casRenew;
|
||||
|
||||
@JsonAlias(CASFilter.WRAP_REQUESTS_INIT_PARAM)
|
||||
boolean wrapRequest;
|
||||
|
||||
@JsonAlias(CASFilter.GATEWAY_INIT_PARAM)
|
||||
boolean casGateway = false;
|
||||
|
||||
@JsonAlias(CASFilter.CAS_FILTER_USERLOGINMARK)
|
||||
String userLoginMark = null;
|
||||
|
||||
// 已授权的代理地址列表
|
||||
@JsonAlias(CASFilter.AUTHORIZED_PROXY_INIT_PARAM)
|
||||
List<String> authorizedProxies = new ArrayList<>();
|
||||
|
||||
public void setAuthorizedProxies(String casAuthorizedProxy) {
|
||||
if (casAuthorizedProxy != null) {
|
||||
StringTokenizer casProxies = new StringTokenizer(casAuthorizedProxy);
|
||||
|
||||
while (casProxies.hasMoreTokens()) {
|
||||
String anAuthorizedProxy = casProxies.nextToken();
|
||||
this.authorizedProxies.add(anAuthorizedProxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getFullServerUrl() {
|
||||
if (StringUtils.hasText(casServerName)) {
|
||||
return casServerProtocol + "://" + casServerName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查配置参数是否有误
|
||||
*/
|
||||
public CASParameter checked() {
|
||||
if (this.casGateway && this.casRenew) {
|
||||
throw new IllegalArgumentException("gateway and renew cannot both be true in filter configuration");
|
||||
} else if (this.casServerName != null && this.casServiceUrl != null) {
|
||||
throw new IllegalArgumentException("serverName and serviceUrl cannot both be set: choose one.");
|
||||
} else if (this.casServerName == null && this.casServiceUrl == null) {
|
||||
throw new IllegalArgumentException("one of serverName or serviceUrl must be set.");
|
||||
} else if (this.casValidate == null) {
|
||||
throw new IllegalArgumentException("validateUrl parameter must be set.");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
package dev.flyfish.boot.cas.validator;
|
||||
|
||||
import lombok.ToString;
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ToString
|
||||
public class ProxyTicketValidator extends ServiceTicketValidator {
|
||||
protected List proxyList;
|
||||
|
||||
public ProxyTicketValidator() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.setProperty("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol");
|
||||
ProxyTicketValidator pv = new ProxyTicketValidator();
|
||||
pv.setCasValidateUrl("https://portal.yale.edu/cas/proxyValidate");
|
||||
pv.setService(args[0]);
|
||||
pv.setServiceTicket(args[1]);
|
||||
pv.validate();
|
||||
System.out.println(pv.getResponse());
|
||||
System.out.println();
|
||||
if (pv.isAuthenticationSuccessful()) {
|
||||
System.out.println("user: " + pv.getUser());
|
||||
System.out.println("proxies:\n " + pv.getProxyList());
|
||||
} else {
|
||||
System.out.println("error code: " + pv.getErrorCode());
|
||||
System.out.println("error message: " + pv.getErrorMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public List getProxyList() {
|
||||
return this.proxyList;
|
||||
}
|
||||
|
||||
protected DefaultHandler newHandler() {
|
||||
return new ProxyHandler();
|
||||
}
|
||||
|
||||
protected void clear() {
|
||||
super.clear();
|
||||
this.proxyList = null;
|
||||
}
|
||||
|
||||
protected class ProxyHandler extends ServiceTicketValidator.Handler {
|
||||
protected static final String PROXIES = "cas:proxies";
|
||||
protected static final String PROXY = "cas:proxy";
|
||||
protected List proxyList = new ArrayList();
|
||||
protected boolean proxyFragment = false;
|
||||
|
||||
protected ProxyHandler() {
|
||||
super();
|
||||
}
|
||||
|
||||
public void startElement(String ns, String ln, String qn, Attributes a) {
|
||||
super.startElement(ns, ln, qn, a);
|
||||
if (this.authenticationSuccess && qn.equals("cas:proxies")) {
|
||||
this.proxyFragment = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void endElement(String ns, String ln, String qn) throws SAXException {
|
||||
super.endElement(ns, ln, qn);
|
||||
if (qn.equals("cas:proxies")) {
|
||||
this.proxyFragment = false;
|
||||
} else if (this.proxyFragment && qn.equals("cas:proxy")) {
|
||||
this.proxyList.add(this.currentText.toString().trim());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void endDocument() throws SAXException {
|
||||
super.endDocument();
|
||||
if (this.authenticationSuccess) {
|
||||
ProxyTicketValidator.this.proxyList = this.proxyList;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package dev.flyfish.boot.cas.validator;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class SecureURL {
|
||||
private static final Log log = LogFactory.getLog(SecureURL.class);
|
||||
|
||||
public SecureURL() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
System.setProperty("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol");
|
||||
System.out.println(retrieve(args[0]));
|
||||
}
|
||||
|
||||
public static String retrieve(String url) throws IOException {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("entering retrieve(" + url + ")");
|
||||
}
|
||||
|
||||
URL u = URI.create(url).toURL();
|
||||
URLConnection uc = u.openConnection();
|
||||
uc.setRequestProperty("Connection", "close");
|
||||
InputStream in = uc.getInputStream();
|
||||
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
|
||||
for (int chByte = in.read(); chByte != -1; chByte = in.read()) {
|
||||
output.write(chByte);
|
||||
}
|
||||
|
||||
return output.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
@ -1,192 +0,0 @@
|
||||
package dev.flyfish.boot.cas.validator;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.XMLReader;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
|
||||
@ToString
|
||||
public class ServiceTicketValidator {
|
||||
@Getter
|
||||
@Setter
|
||||
private String casValidateUrl;
|
||||
@Getter
|
||||
@Setter
|
||||
private String proxyCallbackUrl;
|
||||
private String st;
|
||||
@Setter
|
||||
private String service;
|
||||
@Getter
|
||||
private String pgtIou;
|
||||
@Getter
|
||||
private String user;
|
||||
@Getter
|
||||
private String errorCode;
|
||||
@Getter
|
||||
private String errorMessage;
|
||||
private String entireResponse;
|
||||
private String ss;
|
||||
@Setter
|
||||
@Getter
|
||||
private boolean renew = false;
|
||||
private boolean attemptedAuthentication;
|
||||
private boolean successfulAuthentication;
|
||||
|
||||
public ServiceTicketValidator() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.setProperty("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol");
|
||||
ServiceTicketValidator sv = new ServiceTicketValidator();
|
||||
sv.setCasValidateUrl("https://portal1.wss.yale.edu/cas/serviceValidate");
|
||||
sv.setProxyCallbackUrl("https://portal1.wss.yale.edu/casProxy/receptor");
|
||||
sv.setService(args[0]);
|
||||
sv.setServiceTicket(args[1]);
|
||||
sv.validate();
|
||||
System.out.println(sv.getResponse());
|
||||
System.out.println();
|
||||
if (sv.isAuthenticationSuccessful()) {
|
||||
System.out.println("user: " + sv.getUser());
|
||||
System.out.println("pgtIou: " + sv.getPgtIou());
|
||||
} else {
|
||||
System.out.println("error code: " + sv.getErrorCode());
|
||||
System.out.println("error message: " + sv.getErrorMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setServiceTicket(String x) {
|
||||
this.st = x;
|
||||
}
|
||||
|
||||
public boolean isAuthenticationSuccessful() {
|
||||
return this.successfulAuthentication;
|
||||
}
|
||||
|
||||
public String getResponse() {
|
||||
return this.entireResponse;
|
||||
}
|
||||
|
||||
public void validate() throws IOException, SAXException, ParserConfigurationException {
|
||||
if (this.casValidateUrl != null && this.st != null) {
|
||||
this.clear();
|
||||
this.attemptedAuthentication = true;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(this.casValidateUrl);
|
||||
if (this.casValidateUrl.indexOf(63) == -1) {
|
||||
sb.append('?');
|
||||
} else {
|
||||
sb.append('&');
|
||||
}
|
||||
|
||||
sb.append("service=").append(this.service).append("&ticket=").append(this.st);
|
||||
if (this.proxyCallbackUrl != null) {
|
||||
sb.append("&pgtUrl=").append(this.proxyCallbackUrl);
|
||||
}
|
||||
|
||||
if (this.renew) {
|
||||
sb.append("&renew=true");
|
||||
}
|
||||
|
||||
String url = sb.toString();
|
||||
this.ss = url;
|
||||
String response = SecureURL.retrieve(url);
|
||||
this.entireResponse = response;
|
||||
if (response != null) {
|
||||
XMLReader r = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
|
||||
r.setFeature("http://xml.org/sax/features/namespaces", false);
|
||||
r.setContentHandler(this.newHandler());
|
||||
r.parse(new InputSource(new StringReader(response)));
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new IllegalStateException("must set validation URL and ticket");
|
||||
}
|
||||
}
|
||||
|
||||
protected DefaultHandler newHandler() {
|
||||
return new Handler();
|
||||
}
|
||||
|
||||
protected void clear() {
|
||||
this.user = this.pgtIou = this.errorMessage = null;
|
||||
this.attemptedAuthentication = false;
|
||||
this.successfulAuthentication = false;
|
||||
}
|
||||
|
||||
protected class Handler extends DefaultHandler {
|
||||
protected static final String AUTHENTICATION_SUCCESS = "cas:authenticationSuccess";
|
||||
protected static final String AUTHENTICATION_FAILURE = "cas:authenticationFailure";
|
||||
protected static final String PROXY_GRANTING_TICKET = "cas:proxyGrantingTicket";
|
||||
protected static final String USER = "cas:user";
|
||||
protected StringBuffer currentText = new StringBuffer();
|
||||
protected boolean authenticationSuccess = false;
|
||||
protected boolean authenticationFailure = false;
|
||||
protected String netid;
|
||||
protected String pgtIou;
|
||||
protected String errorCode;
|
||||
protected String errorMessage;
|
||||
|
||||
protected Handler() {
|
||||
}
|
||||
|
||||
public void startElement(String ns, String ln, String qn, Attributes a) {
|
||||
this.currentText = new StringBuffer();
|
||||
if (qn.equals(AUTHENTICATION_SUCCESS)) {
|
||||
this.authenticationSuccess = true;
|
||||
} else if (qn.equals(AUTHENTICATION_FAILURE)) {
|
||||
this.authenticationFailure = true;
|
||||
this.errorCode = a.getValue("code");
|
||||
if (this.errorCode != null) {
|
||||
this.errorCode = this.errorCode.trim();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void characters(char[] ch, int start, int length) {
|
||||
this.currentText.append(ch, start, length);
|
||||
}
|
||||
|
||||
public void endElement(String ns, String ln, String qn) throws SAXException {
|
||||
if (this.authenticationSuccess) {
|
||||
if (qn.equals(USER)) {
|
||||
ServiceTicketValidator.this.user = this.currentText.toString().trim();
|
||||
}
|
||||
|
||||
if (qn.equals(PROXY_GRANTING_TICKET)) {
|
||||
this.pgtIou = this.currentText.toString().trim();
|
||||
}
|
||||
} else if (this.authenticationFailure && qn.equals(AUTHENTICATION_FAILURE)) {
|
||||
this.errorMessage = this.currentText.toString().trim();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void endDocument() throws SAXException {
|
||||
if (this.authenticationSuccess) {
|
||||
ServiceTicketValidator.this.user = ServiceTicketValidator.this.user;
|
||||
ServiceTicketValidator.this.pgtIou = this.pgtIou;
|
||||
ServiceTicketValidator.this.successfulAuthentication = true;
|
||||
} else {
|
||||
if (!this.authenticationFailure) {
|
||||
throw new SAXException("no indication of success or failure from CAS");
|
||||
}
|
||||
|
||||
ServiceTicketValidator.this.errorMessage = this.errorMessage;
|
||||
ServiceTicketValidator.this.errorCode = this.errorCode;
|
||||
ServiceTicketValidator.this.successfulAuthentication = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
package dev.flyfish.boot.cas.validator;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.XMLReader;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
import org.xml.sax.helpers.XMLReaderFactory;
|
||||
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class XmlUtils {
|
||||
private static final Log LOG = LogFactory.getLog(XmlUtils.class);
|
||||
|
||||
public static XMLReader getXmlReader() {
|
||||
try {
|
||||
return XMLReaderFactory.createXMLReader();
|
||||
} catch (SAXException var1) {
|
||||
SAXException e = var1;
|
||||
throw new RuntimeException("Unable to create XMLReader", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static List getTextForElements(String xmlAsString, final String element) {
|
||||
final List elements = new ArrayList(2);
|
||||
XMLReader reader = getXmlReader();
|
||||
DefaultHandler handler = new DefaultHandler() {
|
||||
private boolean foundElement = false;
|
||||
private StringBuffer buffer = new StringBuffer();
|
||||
|
||||
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
|
||||
if (localName.equals(element)) {
|
||||
this.foundElement = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void endElement(String uri, String localName, String qName) throws SAXException {
|
||||
if (localName.equals(element)) {
|
||||
this.foundElement = false;
|
||||
elements.add(this.buffer.toString());
|
||||
this.buffer = new StringBuffer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void characters(char[] ch, int start, int length) throws SAXException {
|
||||
if (this.foundElement) {
|
||||
this.buffer.append(ch, start, length);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
reader.setContentHandler(handler);
|
||||
reader.setErrorHandler(handler);
|
||||
|
||||
try {
|
||||
reader.parse(new InputSource(new StringReader(xmlAsString)));
|
||||
return elements;
|
||||
} catch (Exception var6) {
|
||||
Exception e = var6;
|
||||
LOG.error(e, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getTextForElement(String xmlAsString, final String element) {
|
||||
XMLReader reader = getXmlReader();
|
||||
final StringBuffer buffer = new StringBuffer();
|
||||
DefaultHandler handler = new DefaultHandler() {
|
||||
private boolean foundElement = false;
|
||||
|
||||
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
|
||||
if (localName.equals(element)) {
|
||||
this.foundElement = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void endElement(String uri, String localName, String qName) throws SAXException {
|
||||
if (localName.equals(element)) {
|
||||
this.foundElement = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void characters(char[] ch, int start, int length) throws SAXException {
|
||||
if (this.foundElement) {
|
||||
buffer.append(ch, start, length);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
reader.setContentHandler(handler);
|
||||
reader.setErrorHandler(handler);
|
||||
|
||||
try {
|
||||
reader.parse(new InputSource(new StringReader(xmlAsString)));
|
||||
} catch (Exception var6) {
|
||||
Exception e = var6;
|
||||
LOG.error(e, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user