CAS(Central Authentication Service)中心认证服务
下面这张图来自官网,清晰简单的介绍了CAS的继续交互过程
首先需要分别搭建CAS-server和CAS-client服务,
这两个服务分别在2台机器上,官方地址如下:
https://github.com/apereo/java-cas-client
这一步就不详细阐述了,许多公司内部都已经搭建好了CAS server,我们只需要把我们的域名注册到CAS server即可。
在我们自己的项目中,我们首先需要导入依赖
<dependency>
<groupId>org.jasig.cas.clientgroupId>
<artifactId>cas-client-coreartifactId>
<version>3.6.4version>
dependency>
根据这个库的实现,我们还需要写2个Filter,分别为Filter1_CasAuthenticationFilter
和Filter2_CasTicketValidationFilter
具体实现如下:
Filter1_CasAuthenticationFilter
package com.vip.data.unific.server.config;
import lombok.extern.slf4j.Slf4j;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;
@WebFilter(urlPatterns = "/*")
@Slf4j
public class Filter1_CasAuthenticationFilter implements Filter {
private AuthenticationFilter authentication;
@Autowired
private CasProperties casProperties;
public Filter1_CasAuthenticationFilter() {
super();
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.authentication = new AuthenticationFilter();
this.authentication.setIgnoreInitConfiguration(true);
this.authentication.setServerName(casProperties.getServerName());
this.authentication.setCasServerLoginUrl(casProperties.getCasUrlPrefix() + "/login");
authentication.init(filterConfig);
}
@Override
public void doFilter(ServletRequest req, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if (Boolean.TRUE.equals(casProperties.getSkipFilter())) {
chain.doFilter(request, response);
return ;
}
// 不需要 CAS 单点登录的页面直接跳过
if (Arrays.stream(casProperties.getIgnorePaths()).filter(p -> request.getRequestURI().matches(p)).count() > 0) {
// log.info(LogMsgKit.of("doFilter").p("uri", request.getRequestURI()).end("跳过cas认证"));
chain.doFilter(request, response);
return;
}
this.authentication.doFilter(request, response, chain);
}
@Override
public void destroy() {
this.authentication.destroy();
}
}
Filter2_CasTicketValidationFilter
如下
package com.vip.data.unific.server.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
@WebFilter(urlPatterns = "/*")
@Slf4j
public class Filter2_CasTicketValidationFilter implements Filter {
private static String casServerUrlPrefix = "casServerUrlPrefix";
private static String serverName = "serverName";
private static String encoding = "encoding";
private final TicketValidationWrapper ticketValidation;
@Autowired
private CasProperties casProperties;
public Filter2_CasTicketValidationFilter() {
super();
this.ticketValidation = new TicketValidationWrapper();
}
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
ticketValidation.init(new FilterConfig() {
@Override
public String getFilterName() {
return filterConfig.getFilterName();
}
@Override
public ServletContext getServletContext() {
return filterConfig.getServletContext();
}
@Override
public String getInitParameter(String name) {
String value = null;
if (casServerUrlPrefix.equals(name)) {
value = casProperties.getCasUrlPrefix();
} else if (serverName.equals(name)) {
value = casProperties.getServerName();
} else if(encoding.equals(name)){
value = casProperties.getEncoding();
}
if (value == null) {
value = filterConfig.getInitParameter(name);
}
return value;
}
@Override
public Enumeration<String> getInitParameterNames() {
Enumeration<String> name = filterConfig.getInitParameterNames();
Set<String> set = new HashSet<>();
while (name.hasMoreElements()) {
set.add(name.nextElement());
}
set.add(casServerUrlPrefix);
set.add(serverName);
set.add(encoding);
final Iterator<String> iterator = set.iterator();
set = null;
return new Enumeration<String>() {
@Override
public boolean hasMoreElements() {
return iterator.hasNext();
}
@Override
public String nextElement() {
return iterator.next();
}
};
}
});
ticketValidation.setRedirectAfterValidation(false);//取消重定向,自定义重定向
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
if (Boolean.TRUE.equals(casProperties.getSkipFilter())) {
chain.doFilter(request, response);
return ;
}
// 不需要 CAS 单点登录的页面直接跳过
if (Arrays.stream(casProperties.getIgnorePaths()).filter(p -> request.getRequestURI().matches(p)).count() > 0) {
// log.info(LogMsgKit.of("doFilter").p("uri", request.getRequestURI()).end("跳过cas认证"));
chain.doFilter(request, response);
return;
}
ticketValidation.doFilter(request, response, chain);
}
@Override
public void destroy() {
ticketValidation.destroy();
}
}
这2个Filter基本就是固定写法,顺序最好不要交换(虽然交换了也能执行成功),
常见问题
单点登录其本质就是分布式session共享的一种解决方案,也就是集中管理session,所以单点登录已经解决了session共享问题
修改重定向有2个方法,一个是修改response如下,但是cas-client中是直接response.sendRedirect();所以这种方法不管用
response.setHeader(“Location”,“/index”);
response.setStatus(302);
第二种方法,继承Cas20ProxyReceivingTicketValidationFilter,然后重写onSuccessfulValidation方法,自己定义重定向地址
public class TicketValidationWrapper extends Cas20ProxyReceivingTicketValidationFilter {
// @Value("${cas.redirectURL}") //filter启动先于spring bean初始化
public static final String redirectURL="/";
@Override
protected void onSuccessfulValidation(final HttpServletRequest request, final HttpServletResponse response,
final Assertion assertion) {
try {
response.sendRedirect(redirectURL);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
2个Filter负责的职责不同,理论上可以写在一起,但是分开写逻辑更加清楚
通过实验测试,发现2个Filter交换顺序并不会影响登录,用户体验结果是一样,但是F2放在F1前面的话会多走1此Filter,建议Filter1 auth,Filter2 ticket
Filter1_CasAuthenticationFilter
这个主要用来判断用户是否登录,没有登录则重定向到登录界面进行登录,登录完成继续重定向到原始路径
Filter2_CasTicketValidationFilter
这个主要用来验证是否有ST-ticket(ticket是一次性使用的)
登录成功后,CAS-server有一个session,当用户请求的cookie中携带TGT-xxx访问CAS-server时,可以得到一个ST-ticket
自己的app中也会存储一个session,当用户请求的cookie中携带jsessionid时,则判断为登录成功
也就是CAS-server和自己的app都存储一份session,这两个session是不一样的
注册Filter时,使用2个注解即可,否则会多次注册Filer,导致一个请求执行多次Filter
@ServletComponentScan 加在springboot启动main函数上
@WebFilter(urlPatterns = “/*”) 加在Filter上
Filter上不用加@Component,加了可能会报错或者filter多次注册
Filer执行多次原因还有可能是浏览器默认请求了favicon.ico这个文件,可能检查网络请求中是否有
参考链接:https://blog.csdn.net/chaijunkun/article/details/7646338
在Filter进行注入时要注意,无法使用@Value注入,因为Filter启动时,springbean还没初始化?(可能)
@order注解不管用,默认是按照类名,所以建议以Filer1xxx,Filter2xxx命名
参考链接:https://www.cnblogs.com/tfgzs/p/4571137.html
分布式session解决方案
方案1:Tomcat集群Session全局复制(集群内每个tomcat的session完全同步)【会影响集群的性能】
方案2:根据请求的IP进行Hash映射到对应的机器上【如果服务器宕机了,会丢失Session的数据,实现最简单】
方案3:引入中间件Redis,把Session数据放在Redis中,已经实现的框架有Spring session 使用Spring Session和Redis解决分布式Session共享【有一定的侵入性,实现难度中等,】
方案4:JWT方式,user信息保存在token中,每次请求都携带token【可能存在安全问题,网络开销大一点】
(单点登录其实就属于方案1和3的结合,CAS-server保存登录状态,每个app中也保存的单独的session)
共同点(单点登录的核心)
问题:单点登录的问题是session是各个系统所独自拥有的,各个系统不知道用户是否登录,无法共享用户的登录状态,
目标/切入点:目标/切入点是 “一定要让所有的系统就都可以知道现在用户登录没有”,只要能够实现这个目标/切入点,就可以作为方案,所以 Tomcat集群Session全局复制、请求的IP一直会访问同一个服务器、引入中间件Redis 都是方案。