cas 是一套单点登录的具体实现,耶鲁大学开发, 目前最高版本为6.x (6.x使用gradle编译) 此处使用5.x版本(maven编译)
分为两部分,服务端和客户端
服务端就是统一的认证中心,客户端就是所有需要接入单点登录的系统
[cas github地址]: https://github.com/apereo/cas “源码地址”
cas不建议直接在源码上修改,提供了一个模板项目,在模板项目上做扩展
项目拉下来之后,导入idea进行编译打包
或者直接使用
mvn clean package
打成war包 部署到tomcat下
启动Tomcat cas服务端就部署好了
1. xml
1. java package com.zhangyao.cas.config; import org.apache.tomcat.util.net.openssl.ciphers.Authentication; import org.jasig.cas.client.authentication.AuthenticationFilter; import org.jasig.cas.client.session.SingleSignOutFilter; import org.jasig.cas.client.session.SingleSignOutHttpSessionListener; import org.jasig.cas.client.util.AssertionThreadLocalFilter; import org.jasig.cas.client.util.HttpServletRequestWrapperFilter; import org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * @author: zhangyao * @create:2020-05-08 22:52 **/ @Configuration public class CasConfig { @Bean public SingleSignOutHttpSessionListener singleSignOutHttpSessionListener(){ return new SingleSignOutHttpSessionListener(); } /** * 监听退出 * @return */ @Bean public ServletListenerRegistrationBean
java @SpringBootApplication @EnableCasClient public class TestApplication { public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } }
实现的效果是: 每一个客户端登出的时候,cas服务端也需要登出,从而达到一端登出,多端登出的效果
实现方法: 客户端发送登出请求时,后台跳转cas服务端登出路径
package com.zhangyao.cas.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.servlet.http.HttpSession;
/**
* @author: zhangyao
* @create:2020-05-08 22:58
**/
@Controller
@RequestMapping("")
public class LoginController {
@Autowired
RestTemplate restTemplate;
@GetMapping("/loginOut")
public String test(HttpSession session){
session.invalidate();
// String forObject = restTemplate.getForObject("http://127.0.0.1:8085/cas_overlay_template_war/logout", String.class);
return "redirect:http://127.0.0.1:8085/cas_overlay_template_war/logout?service=http://127.0.0.1:8086/index";
}
}
上文中说到下载后直接mvn clean package即可打包部署,但是当我们想要对服务端进行一些扩展,比如修改默认登录页,修改登录的验证方式等
有两种方法:
具体分析overlays
pom.xml中的配置
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-war-pluginartifactId>
<version>2.6version>
<configuration>
<warName>caswarName>
<failOnMissingWebXml>falsefailOnMissingWebXml>
<recompressZippedFiles>falserecompressZippedFiles>
<archive>
<compress>falsecompress>
<manifestFile>${manifestFileToUse}manifestFile>
archive>
<overlays>
<overlay>
<groupId>org.apereo.casgroupId>
<artifactId>cas-server-webapp${app.server}artifactId>
overlay>
overlays>
configuration>
plugin>
这里的overlay配置的意思是使用我们主项目中的同名同路径文件覆盖cas-server-webapp${app.server}下的文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d2Ny4w8t-1591259677620)(.\images\image-20200512093750382.png)]
具体操作方式如上图
在根项目下新建 src/main/resources作为资源目录
src/main/java作为根目录
官网中也有介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oZeUANHh-1591259677622)(.\images\image-20200512093919720.png)]
修改配置文件时从war包中沾出来再修改 比如修改cas服务端支持http访问
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mf15Se9N-1591259677623)(.images/image-20200512095229110.png)]
复制出这两个文件
并修改HTTPSandIMAPS-10000001.json
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(http|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
}
修改application.properties
在最后加上
#支持http访问
cas.tgc.secure=false
cas.serviceRegistry.initFromJson=true
#支持退出自定义跳转页面
cas.logout.followServiceRedirects=true
cas.logout.redirectParameter=service
cas.logout.confirmLogout=false
cas官网流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LJ6KoS7S-1591259677624)(.images/cas_flow_diagram.png)]
ST:service-ticket cas服务端签发的票据,用于浏览器访问不同系统时携带,cas服务端使用此票据验证是否有效用户
TGC:全局session的key,存放于浏览器的cookie中,访问cas服务端时使用
TGT:全局session 存放于cas服务端中
验证是否授权的过滤器,也即上文中配置的授权过滤器
org.jasig.cas.client.authentication.AuthenticationFilter
进入doFilter
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 {
//如果既不是不拦截请求,也没有session,就需要重定向至cas服务端登录
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);
}
}
}
}
org.jasig.cas.client.validation.AbstractTicketValidationFilter
进入doFilter
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//preFilter 执行过滤器前可以进行一些操作 源码中直接返回true了
if (this.preFilter(servletRequest, servletResponse, filterChain)) {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
//获取ticket
String ticket = this.retrieveTicketFromRequest(request);
if (CommonUtils.isNotBlank(ticket)) {
this.logger.debug("Attempting to validate ticket: {}", ticket);
try {
//发送请求到cas服务端验证ticket是否有效
Assertion assertion = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response));
this.logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
//验证通过后设置属性
request.setAttribute("_const_cas_assertion_", assertion);
//如果是已经登录过有会话,更新session中的属性
if (this.useSession) {
request.getSession().setAttribute("_const_cas_assertion_", assertion);
}
//验证成功进行一些操作,源码中为空
this.onSuccessfulValidation(request, response, assertion);
//redirectAfterValidation 源码中定义为true
if (this.redirectAfterValidation) {
this.logger.debug("Redirecting after successful ticket validation.");
//重定向至之前的访问请求
response.sendRedirect(this.constructServiceUrl(request, response));
return;
}
} catch (TicketValidationException var8) {//如果有异常 则返回错误403
this.logger.debug(var8.getMessage(), var8);
this.onFailedValidation(request, response);
if (this.exceptionOnValidationFailure) {
throw new ServletException(var8);
}
response.sendError(403, var8.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
}
org.jasig.cas.client.util.HttpServletRequestWrapperFilter
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//从session/request中获取Principal用户信息
AttributePrincipal principal = this.retrievePrincipalFromSessionOrRequest(servletRequest);
//CasHttpServletRequestWrapper是此类中定义的内部类,继承了HttpServletRequestWrapperFilter 增加了获取用户的方法 之后获取用户信息可以直接通过 request.getUserPrincipal()获取用户信息
filterChain.doFilter(new HttpServletRequestWrapperFilter.CasHttpServletRequestWrapper((HttpServletRequest)servletRequest, principal), servletResponse);
}
properties #查询用户密码的sql cas.authn.jdbc.query[0].sql=SELECT * FROM cof_user WHERE name=? #数据库连接 cas.authn.jdbc.query[0].url=jdbc:mysql://122.51.97.53:3306/coframe?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false #数据库用户 cas.authn.jdbc.query[0].user=root #数据库密码 cas.authn.jdbc.query[0].password=tlqaz1234 #数据库驱动 cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver #标识sql查出来的哪个字段是password cas.authn.jdbc.query[0].fieldPassword=PASSWORD
xml
##### 7.2 自定义加密类
properties #自定义加密类 cas.authn.jdbc.query[0].passwordEncoder.type=com.zhangyao.cas.CustomPasswordEncoder
定义加密类 并实现import org.springframework.security.crypto.password.PasswordEncoder
此处我采用的是 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder spring security默认的加密类