之前搭建了个springboot+cas的项目,一直想做个总结,今天刚好有时间就总结下吧。只描述自己遇到过的问题,其他的没有涉及的请见谅。cas服务端搭建在这里不做过多讲解,直接从网上下载,然后更改部分配置,放在tomcat里跑起来就可以用(不懂得百度cas服务端搭建,很多)。这里只针对cas客户端。
springboot版本:2.2.2 cas版本:1.5.0
在springboot项目的pom文件里导入cas依赖包
<!-- CAS依赖包 -->
<dependency>
<groupId>net.unicon.cas</groupId>
<artifactId>cas-client-autoconfig-support</artifactId>
<version>1.5.0-GA</version>
</dependency>
修改application里的配置,增加cas相关配置
#CAS 配置
#cas服务器地址
cas.server-url-prefix=http://192.168.20.69:8010/gisquest-sso
#客户端服务器地址
cas.client-host-url=http://devkpi.gisquest.com:8010/GisqPerfAssess-WebApp/a/cas-login
#默认cas登陆地址(oa)
cas.server-login-url=http://192.168.20.69:8010/gisquest-sso/login
#cas登出地址
perform.logoutUrl=http://192.168.20.69:8010/gisquest-sso/logout
cas.validation-type=CAS
#cas不拦截的url
ignore-host-url=/perf-assess/updateAssessStatus
根据自己实际情况去配,不要照搬
配置关于cas的过滤器
@Configuration
public class CasFilterConfig {
@Value("${cas.server-url-prefix}")
private String CAS_URL;
@Value("${cas.client-host-url}")
private String APP_URL;
@Value("${ignore-host-url}")
private String IGNORE_URL;
/**
* 单点登录退出(必须放在最前面)
* @return
*/
@Bean
public FilterRegistrationBean singleSignOutFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new SingleSignOutFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.addInitParameter("casServerUrlPrefix", CAS_URL );
registrationBean.setName("CAS Single Sign Out Filter");
registrationBean.setOrder(2);
return registrationBean;
}
/**
* @Author chenb2
* @Description 设置监听器
* @Date 14:24 2019/12/5
* @Param []
* @return org.springframework.boot.web.servlet.ServletListenerRegistrationBean
**/
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean(){
ServletListenerRegistrationBean listenerRegistrationBean = new ServletListenerRegistrationBean();
listenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());
listenerRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return listenerRegistrationBean;
}
/**
* @Description 自定义的过滤器(session失效后跳转到登录页)
* @Date 14:58 2020/1/6
* @Param []
* @return org.springframework.boot.web.servlet.FilterRegistrationBean
**/
@Bean
public FilterRegistrationBean filterAuthenticationRegistration(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
// 设定匹配的路径
registrationBean.setFilter(new MyAuthenticationFilter());
registrationBean.addUrlPatterns("/*");
//registrationBean.setName("CAS Filter");//加这个后无法自定义cas登录地址
Map<String,String> initParameters = new HashMap<>();
initParameters.put("casServerLoginUrl", CAS_URL);
initParameters.put("serverName", APP_URL);
//忽略的url,"|"分隔多个url
initParameters.put("ignorePattern", IGNORE_URL);
initParameters.put("ignoreUrlPatternType","CONTAINS");
registrationBean.setInitParameters(initParameters);
// 设定加载的顺序
registrationBean.setOrder(0);
return registrationBean;
}
/**
* @Description cas的授权过滤器配置
* @Date 15:02 2020/1/6
* @Param []
* @return org.springframework.boot.web.servlet.FilterRegistrationBean
**/
@Bean
public FilterRegistrationBean myFilterAuthenticationRegistration(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
// 设定匹配的路径
registrationBean.setFilter(new AuthenticationFilter());
registrationBean.addUrlPatterns("/*");
//registrationBean.setName("CAS Filter");//加这个后无法自定义cas登录地址
Map<String,String> initParameters = new HashMap<>();
initParameters.put("casServerLoginUrl", CAS_URL);
initParameters.put("serverName", APP_URL);
//忽略的url,"|"分隔多个url
initParameters.put("ignorePattern", IGNORE_URL);
initParameters.put("ignoreUrlPatternType","CONTAINS");
registrationBean.setInitParameters(initParameters);
// 设定加载的顺序
registrationBean.setOrder(1);
return registrationBean;
}
/**
* 单点登录校验
* @return
*/
@Bean
public FilterRegistrationBean cas20ProxyReceivingTicketValidationFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new Cas20ProxyReceivingTicketValidationFilter());
registrationBean.addUrlPatterns("/*");
Map<String,String> initParameters = new HashMap<String, String>();
initParameters.put("casServerUrlPrefix", CAS_URL);
initParameters.put("serverName", APP_URL);
initParameters.put("useSession", "true");
registrationBean.setName("CAS Validation Filter");
registrationBean.setInitParameters(initParameters);
registrationBean.setOrder(3);
return registrationBean;
}
/**
* 单点登录请求包装
* @return
*/
@Bean
public FilterRegistrationBean httpServletRequestWrapperFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new HttpServletRequestWrapperFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setName("CAS HttpServletRequest Wrapper Filter");
registrationBean.setOrder(4);
return registrationBean;
}
/**
* 单点登录本地用户信息
* @return
*/
@Bean
public FilterRegistrationBean localUserInfoFilter(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new LocalUserInfoFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setName("localUserInfoFilter");
registrationBean.setOrder(5);
return registrationBean;
}
}
注意: CAS_URL是配置文件中cas服务端的地址
APP_URL是配置文件中cas客户端地址
IGNORE_URL是配置文件中配置的关于忽略拦截请求的地址(不需要登陆认证就可以访问该接口)
MyAuthenticationFilter和LocalUserInfoFilter是我自己定义的过滤器,这里暂时会报错,后面做讲解,其他的过滤器都是默认的,导入相关的包后不会报错。
过滤器的执行顺序:除了自己定义的过滤器之外其他的顺序建议按照上面代码中的来,避免出错。
过滤器配置完成之后,cas客户端其实已经已经搭建完成了,至于网上说的配置cas客户端的重定向策略,我并没有配置,也没发现什么问题。怎么样,是不是很简单。好了我们接着往下看。
AuthenticationFilter和MyAuthenticationFilter是我今天着重想提一嘴的。
AuthenticationFilter里面定义了关于请求的处理,忽略请求的判断,session的是否存在的判断等等,有兴趣的朋友可以去看看源码。主要看doFileter方法和isRequestUrlExcluded方法
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
if (this.isRequestUrlExcluded(request)) {
this.logger.debug("Request is ignored.");
filterChain.doFilter(request, response);
} else {
HttpSession session = request.getSession(false);
Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
if (assertion != null) {
filterChain.doFilter(request, response);
} else {
String serviceUrl = this.constructServiceUrl(request, response);
String ticket = this.retrieveTicketFromRequest(request);
boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
this.logger.debug("no ticket and no assertion found");
String modifiedServiceUrl;
if (this.gateway) {
this.logger.debug("setting gateway attribute in session");
modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
} else {
modifiedServiceUrl = serviceUrl;
}
this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
} else {
filterChain.doFilter(request, response);
}
}
}
}
private boolean isRequestUrlExcluded(HttpServletRequest request) {
if (this.ignoreUrlPatternMatcherStrategyClass == null) {
return false;
} else {
StringBuffer urlBuffer = request.getRequestURL();
if (request.getQueryString() != null) {
urlBuffer.append("?").append(request.getQueryString());
}
String requestUri = urlBuffer.toString();
return this.ignoreUrlPatternMatcherStrategyClass.matches(requestUri);
}
}
MyAuthenticationFilter是自定义的过滤器,目的是为了解决session过期或失效点击页面上的接口报接口异常的问题(如果是location.href的页面跳转则没有问题,如果是ajax请求则会报接口异常,百度了一通说是会存在跨域的问题,也没找到很好的办法,只能自己写过滤器处理请求)
MyAuthenticationFilter的代码如下
public class MyAuthenticationFilter extends AbstractCasFilter {
private static final String loginUrl = "http://deliver.gisquest.com/GisqPerfAssess-WebApp/a/cas-login";
private String casServerLoginUrl;
private boolean renew;
private boolean gateway;
private GatewayResolver gatewayStorage;
private AuthenticationRedirectStrategy authenticationRedirectStrategy;
private UrlPatternMatcherStrategy ignoreUrlPatternMatcherStrategyClass;
private static final Map<String, Class<? extends UrlPatternMatcherStrategy>> PATTERN_MATCHER_TYPES = new HashMap();
/**
* 构造方法
*/
public MyAuthenticationFilter() {
this(Protocol.CAS2);
}
protected MyAuthenticationFilter(Protocol protocol) {
super(protocol);
this.renew = false;
this.gateway = false;
this.gatewayStorage = new DefaultGatewayResolverImpl();
this.authenticationRedirectStrategy = new DefaultAuthenticationRedirectStrategy();
this.ignoreUrlPatternMatcherStrategyClass = null;
}
protected void initInternal(FilterConfig filterConfig) throws ServletException {
if (!this.isIgnoreInitConfiguration()) {
super.initInternal(filterConfig);
this.setCasServerLoginUrl(this.getString(ConfigurationKeys.CAS_SERVER_LOGIN_URL));
this.setRenew(this.getBoolean(ConfigurationKeys.RENEW));
this.setGateway(this.getBoolean(ConfigurationKeys.GATEWAY));
String ignorePattern = this.getString(ConfigurationKeys.IGNORE_PATTERN);
String ignoreUrlPatternType = this.getString(ConfigurationKeys.IGNORE_URL_PATTERN_TYPE);
Class gatewayStorageClass;
if (ignorePattern != null) {
gatewayStorageClass = (Class)PATTERN_MATCHER_TYPES.get(ignoreUrlPatternType);
if (gatewayStorageClass != null) {
this.ignoreUrlPatternMatcherStrategyClass = (UrlPatternMatcherStrategy) ReflectUtils.newInstance(gatewayStorageClass.getName(), new Object[0]);
} else {
try {
this.logger.trace("Assuming {} is a qualified class name...", ignoreUrlPatternType);
this.ignoreUrlPatternMatcherStrategyClass = (UrlPatternMatcherStrategy)ReflectUtils.newInstance(ignoreUrlPatternType, new Object[0]);
} catch (IllegalArgumentException var6) {
this.logger.error("Could not instantiate class [{}]", ignoreUrlPatternType, var6);
}
}
if (this.ignoreUrlPatternMatcherStrategyClass != null) {
this.ignoreUrlPatternMatcherStrategyClass.setPattern(ignorePattern);
}
}
gatewayStorageClass = this.getClass(ConfigurationKeys.GATEWAY_STORAGE_CLASS);
if (gatewayStorageClass != null) {
this.setGatewayStorage((GatewayResolver)ReflectUtils.newInstance(gatewayStorageClass, new Object[0]));
}
Class<? extends AuthenticationRedirectStrategy> authenticationRedirectStrategyClass = this.getClass(ConfigurationKeys.AUTHENTICATION_REDIRECT_STRATEGY_CLASS);
if (authenticationRedirectStrategyClass != null) {
this.authenticationRedirectStrategy = (AuthenticationRedirectStrategy)ReflectUtils.newInstance(authenticationRedirectStrategyClass, new Object[0]);
}
}
}
/**
* 初始化
*/
public void init() {
super.init();
CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
}
/**
* 核心方法
* @param servletRequest
* @param servletResponse
* @param filterChain
* @throws IOException
* @throws ServletException
*/
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//判断是否是不必要拦截的请求地址
if (this.isRequestUrlExcluded(request)) {
this.logger.debug("Request is ignored.");
filterChain.doFilter(request, response);
} else {
//获取session,判断session是否失效
HttpSession session = request.getSession(false);
Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
if (assertion != null) {
filterChain.doFilter(request, response);
} else {
//session失效判断票据和断言
String serviceUrl = this.constructServiceUrl(request, response);
String ticket = this.retrieveTicketFromRequest(request);
boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
this.logger.debug("no ticket and no assertion found");
String modifiedServiceUrl;
if (this.gateway) {
this.logger.debug("setting gateway attribute in session");
modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
} else {
modifiedServiceUrl = serviceUrl;
}
//获取请求路径
this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
//判断请求方式是否为ajax请求
String header = request.getHeader("X-Requested-With");
if (header != null && "XMLHttpRequest".equals(header)){
//给这个请求打上标记(登录已经超时或者认证未通过)
ajaxHttpToLogin(request,response,loginUrl);
return;
}else{
this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
}
} else {
filterChain.doFilter(request, response);
}
}
}
}
/**
* ajax请求标记
* @param request
* @param response
* @param loginUrl
*/
private void ajaxHttpToLogin(HttpServletRequest request, HttpServletResponse response, String loginUrl) {
try {
response.setHeader("SESSIONSTATUS", "TIMEOUT");
response.setHeader("CONTEXTPATH", loginUrl);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);//403 禁止
}catch (Exception e){
e.printStackTrace();
}
}
public final void setRenew(boolean renew) {
this.renew = renew;
}
public final void setGateway(boolean gateway) {
this.gateway = gateway;
}
public final void setCasServerLoginUrl(String casServerLoginUrl) {
this.casServerLoginUrl = casServerLoginUrl;
}
public final void setGatewayStorage(GatewayResolver gatewayStorage) {
this.gatewayStorage = gatewayStorage;
}
private boolean isRequestUrlExcluded(HttpServletRequest request) {
if (this.ignoreUrlPatternMatcherStrategyClass == null) {
return false;
} else {
StringBuffer urlBuffer = request.getRequestURL();
if (request.getQueryString() != null) {
urlBuffer.append("?").append(request.getQueryString());
}
String requestUri = urlBuffer.toString();
return this.ignoreUrlPatternMatcherStrategyClass.matches(requestUri);
}
}
static {
PATTERN_MATCHER_TYPES.put("CONTAINS", ContainsPatternUrlPatternMatcherStrategy.class);
PATTERN_MATCHER_TYPES.put("REGEX", RegexUrlPatternMatcherStrategy.class);
PATTERN_MATCHER_TYPES.put("EXACT", ExactUrlPatternMatcherStrategy.class);
}
}
可以看到这个类是对AuthenticationFilter的部分源码进行了加工处理,对请求进行处理:session失效后判断是否是ajax请求,是的话对请求进行标记,返回状态为403,返回登陆页的地址,在前端进行处理。
在前端的页面里引入一个common.js,js里统一对403状态的请求进行处理(跳转到登陆页)
<script src="${pageContext.request.contextPath}/static/js/common.js"></script>
common.js的内容
```java
//超时异常处理
$.ajaxSetup({
complete : function(xhr) {
//拦截器实现超时跳转到登录页面
// 通过xhr取得响应头
var SESSIONSTATUS = xhr.getResponseHeader("SESSIONSTATUS");
//如果响应头中包含 TIMEOUT 则说明是登录过期
if (SESSIONSTATUS == "TIMEOUT"){
var win = window;
while (win != win.top){
win = win.top;
}
//重新跳转到首页
win.location.href = xhr.getResponseHeader("CONTEXTPATH");
}
}
});
LocalUserInfoFilter是一个简单的输出登陆人信息的,没啥用,我测试用的
public class LocalUserInfoFilter implements Filter {
private static Logger logger = LoggerFactory.getLogger(LocalUserInfoFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request_ = (HttpServletRequest)request;
String username = CASUtil.getAccountNameFromCas(request_);
if(StringUtils.isNotEmpty(username)){
logger.info("访问者:{}",username);
request_.getSession().setAttribute("username", username);
}
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
``
退出功能直接重定向到cas服务端的退出地址(cas自己会清除票据的),想要跳转到登陆页,需要更改cas服务端的一个配置(跳转地址的service后面的参数,不会就百度吧,很好找的),直接看代码吧
@RequestMapping(value = "/logout")
public void logout(HttpSession session, HttpServletResponse response){
session.invalidate();//清除session
try {
response.sendRedirect(casProperties.getLogoutUrl());
} catch (IOException e) {
logger.error("登出系统失败");
}
}
至此,关于cas客户端搭建以及忽略请求配置和session失效处理就已经讲完了,好多东西也是在慢慢摸索中解决的,如果大家有更好的方案可以在下方留言互相交流。