单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
很早期的公司,一家公司可能只有一个Server,慢慢的Server开始变多了。每个Server都要进行注册登录,退出的时候又要一个个退出。用户体验很不好!你可以想象一下,上豆瓣 要登录豆瓣FM、豆瓣读书、豆瓣电影、豆瓣日记…真的会让人崩溃的。我们想要另一种登录体验:一家企业下的服务只要一次注册,登录的时候只要一次登录,退出的时候只要一次退出。怎么做?
一次注册。 一次注册不难,想一下是不是只要Server之间同步用户信息就行了?可以,但这样描述不太完整,后续讲用户注册的时候详细说。实际上用户信息的管理才是SSO真正的难点,只是作为初学者,我们的难点在于实现SSO的技术!我们先讨论实现手段。
一次登录与一次退出。 回头看看普通商场的故事,什么东西才是保持登录状态关键的东西?记录器(session)?那种叫做cookie的纸张?写在纸张上的ID? 是session里面记录的信息跟那个ID,cookie不只是记录ID的工具而已。客户端持有ID,服务端持有session,两者一起用来保持登录状态。客户端需要用ID来作为凭证,而服务端需要用session来验证ID的有效性(ID可能过期、可能根本就是伪造的找不到对应的信息、ID下对应的客户端还没有进行登录验证等)。但是session这东西一开始是每个server自己独有的,豆瓣FM有自己的session、豆瓣读书有自己的session,而记录ID的cookie又是不能跨域的。所以,我们要实现一次登录一次退出,只需要想办法让各个server的共用一个session的信息,让客户端在各个域名下都能持有这个ID就好了。再进一步讲,只要各个server拿到同一个ID,都能有办法检验出ID的有效性、并且能得到ID对应的用户信息就行了,也就是能检验ID 。
以server群如何生成、验证ID的方式大致分为两种:
第一种:
共享Cookie”这个就是上面提到的共享session的方式,我倒觉得叫“共享session”来得好一点,本质上cookie只是存储session-id的介质,session-id也可以放在每一次请求的url里。据说这种方式不安全,我没去细究,哪位大神可以推荐下相关的资料,我后期补上。其实也是,毕竟session这项机制一开始就是一个server一个session的,把session拿出来让所有server共享确实有点奇怪。
第二种:
SSO-Token方式因为共享session的方式不安全,所以我们不再以session-id作为身份的标识。我们另外生成一种标识,把它取名SSO-Token(或Ticket),这种标识是整个server群唯一的,并且所有server群都能验证这个token,同时能拿到token背后代表的用户的信息。我们要讨论的也是这种方式,一会上具体流程图。
单点登录还有非常关键的一步,这一步跟server端验证token的方式无关,用最早的“共享session”的方式还是现在的“token”方式,身份标识到了浏览器端都要面临这样的一个问题:用户登录成功拿到token(或者是session-id)后怎么让浏览器存储和分享到其它域名下?同域名很简单,把token存在cookie里,把cookie的路径设置成顶级域名下,这样所有子域都能读取cookie中的token。这就是共享cookie的方式(这才叫共享Cookie嘛,上面那个应该叫共享session)。比如:谷歌公司,google.com是他的顶级域名,邮箱服务的mail.google.com和地图服务的map.google.com都是它的子域。但是,跨域的时候怎么办?谷歌公司还有一个域名,youtube.com,提供视频服务
1.用户第一次访问受保护的应用,将会重定向到cas登录页面
2.用户输入用户名和密码,cas server 认证用户创建sso session,并生成TGT和改客户端的服务票据(ST)
3.应用拿着ST到 CAS 服务验证ST,验证通过后设置session和cookie返回浏览器
4.用户浏览器携带cookie访问,应用验证后返回内容
5.访问第二个cas client 应用,此时cookie中有TGT但是没有ST,应用要求去CAS 服务获取ST,cas 服务验证TGT并生成一个ST
6.客户端携带ST访问应用,应用服务区Cas server 验证ticket,验证成功后设置session,cookie,重定向到应用服务地址
7.展示第二个应用的内容
jdk11
tomcat9
GRADLE5.6.3
cas-overlay-template 6.2
jdk8
tomcat7
cas-client3.6.1
好了,说了这么多正文开始了。最近公司要求我用最新的cas6.2服务端来集成公司的几个项目。由于6.2是最新的版本,现在网上基本上找不到关于最新版的资料。我自己在部署服务端和集成客户端的时候也遇到了非常多了的坑。这里我会吧我遇到的坑也会一一跟大家说出来,以及是如何解决的。
首先https://github.com/apereo/cas-overlay-template/tree/master冲github上 下载最新的cas-overlay-template 6.2。
执行 gradlew.bat clean build 第一次构建比较慢,耐心等待。可能会根据网速我有的时候七八分钟就好了,有的时候也要四五十个多分钟
gradlew.bat*explodeWar 解压此时将会在bulid目录下生成一个cas-resources 文件夹,我们把里面的文件全部拷贝到src/main/resources,将\etc\cas\thekeystore 也拷贝到该目录下
更改配置application.properties server.ssl.key-store=classpath:thekeystore
gradlew.bat run 将cas运行在内嵌的 Embedded Tomcat 中
启动完成后浏览器中打开 https://localhost:8443/cas/login 、
默认密码是casuser Mellon 如果出现以下画面就说明然后最简单的cas-overlay-template 6.2服务端已经部署好了简单吧
我这里是使用的mysql然后我注释掉的部分是Oracle的配置。和md5码的配置数据库连接要自己填哦
##Query Database Authentication 数据库查询校验用户名开始--------------------
##查询账号密码sql,必须包含密码字段(**重点要填自己的数据库的表名记得要改**)
cas.authn.jdbc.query[0].sql=select * from pri_user where LOGINNAME=?
##指定上面的sql查询字段名(必须!!!!**自己密码的字段记得要改**)
cas.authn.jdbc.query[0].fieldPassword=PASSWORD
##指定过期字段,1为过期,若过期不可用
#cas.authn.jdbc.query[0].fieldExpired=expired
##为不可用字段段,1为不可用,需要修改密码
#cas.authn.jdbc.query[0].fieldDisabled=disabled
##数据库方言hibernate的
##cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.Oracle10gDialect
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQL5InnoDBDialect
##数据库驱动
##cas.authn.jdbc.query[0].driverClass=oracle.jdbc.driver.OracleDriver
cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
###数据库连接
cas.authn.jdbc.query[0].url=jdbc:mysql://xxx.xxx.xx.xx:xxxx/xxxxxx?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=UTC& allowPublicKeyRetrieval=true
###数据库用户名
cas.authn.jdbc.query[0].user=jtsdb
###数据库密码
cas.authn.jdbc.query[0].password=jtsdb
###默认加密策略,通过encodingAlgorithm来指定算法,默认NONE不加密
cas.authn.jdbc.query[0].passwordEncoder.type=DEFAULT
cas.authn.jdbc.query[0].passwordEncoder.characterEncoding=UTF-8
cas.authn.jdbc.query[0].passwordEncoder.encodingAlgorithm=MD5
##Query Database Authentication 数据库查询校验用户名结束
好拉配置好了就可以使用自己数据库的用户名和密码进行登录了
cas-client-core-3.6.1.jar
commons-logging-1.1.jar
log4j-api-2.9.1.jar
log4j-slf4j-impl-2.9.1.jar
slf4j-api-1.7.6.jar
在部署cas-overlay-template 6.2的时候会有一个自带证书,但是我在使用的时候发现在集成客户端的时候自带的那个证书服务端并不信赖那个证书所以我自己在集成的时候遇到了很多很多问题是一个很大的坑
第一步:keytool -genkeypair -alias cas -keyalg RSA -keypass changeit -storepass changeit -keystore D:/key//thekeystore 生成之后要替换掉原来cas-overlay-template 6.2自带的证书
请记住这个第一段您的名字与姓名时什么这个很重要这就是域名待会在配置客户端的时候需要用到不要写错了哈
目录结构
第二步:生成jdk所需要的 证书 keytool -exportcert -alias cas -storepass changeit -keystore D:/cas/thekeystore -file D:/kes/key/cas.cer
第三步:把证书导入到jdk的认证库里面(记得是导入客户端的jdk里面 别弄错了)
keytool -import -keystore “D:/wanghuyue/Jdk/jdk-11.0.2/lib/security/cacerts” -file cas.cer -alias cas
好了这样就把证书的问题解决了
第四步:配置域名 进入C:\Windows\System32\drivers\etc\hosts(windows系统下默认是在这个位置)
这里是服务端的ip(记得改) sso.text.com
第五步:修改 src\main\resources\下HTTPSandIMAPS-10000001.json文件,做一下简单的修改(复制即可)
{
"@class": "org.apereo.cas.services.RegexRegisteredService",
"serviceId": "^(http|https|imaps)://.*",
"name": "HTTPS and IMAPS",
"id": 10000001,
"description": "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
"evaluationOrder": 10000
}
第六步:在原有的https和imaps基础上增加了http应用,我们还需要再application.properties中配置注册文件的位置
cas.serviceRegistry.initFromJson=false
cas.serviceRegistry.json.location=classpath:/services
并且需要在build.gradle中加入依赖包
dependencies {
// Other CAS dependencies/modules may be listed here…
compile “org.apereo.cas:cas-server-support-json-service-registry:${casServerVersion}”
}
第七步:修改web.xml文件(好像下面部分 有显示的bug都是需要的哈 这些filter)
<!--登出过滤器-->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>https://sso.text.com:8444/cas</param-value>
</init-param>
</filter>
<!-- 登出监听器 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 认证过滤器 -->
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<!-- 服务端的域名不要写错了 -->
<param-value>https://sso.text.com:8443/cas/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<!-- 客户端的ip和地址 -->
<param-value>http://localhost:8444</param-value>
</init-param>
<init-param>
<param-name>encodeServiceUrl</param-name>
<param-value>false</param-value>
</init-param>
<!-- 忽略验证的url,多个url使用"|"分割 -->
<init-param>
<param-name>ignorePattern</param-name>
<param-value>/js/*|/img/*|/css/*
CAS Validation Filter
org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter
casServerUrlPrefix
https://sso.text.com:8443/cas
serverName
http://localhost:8444/
redirectAfterValidation
true
useSession
true
CasLoginFilter
cn.jasgroup.jasframework.privilege.filter.CasLoginFilter
CasLoginFilter
*
CAS HttpServletRequest Wrapper Filter
org.jasig.cas.client.util.HttpServletRequestWrapperFilter
CAS Single Sign Out Filter
/*
CAS Validation Filter
/*
CAS Authentication Filter
/*
CAS HttpServletRequest Wrapper Filter
/*
CasLoginFilter
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.xml.ws.spi.http.HttpContext;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.validation.Assertion;
import cn.jasgroup.jasframework.privilege.business.bo.UserBo;
import cn.jasgroup.jasframework.privilege.business.service.UserService;
import cn.jasgroup.jasframework.privilege.manager.TrustAllTrustManager;
import cn.jasgroup.jasframework.utils.web.WebContextFactoryUtil;
@WebFilter({
"/CasLoginFilter"})
public class CasLoginFilter
implements Filter
{
public void destroy()
{
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
//下面的几种方法是我找到可以从服务端获取的方法
HttpServletRequest p = (HttpServletRequest)request;
HttpSession session = p.getSession();
String strUser=p.getRemoteUser();
Principal tmpPrincipal=p.getUserPrincipal();
Assertion assertion1 = (Assertion)p.getSession().getAttribute("_const_cas_assertion_");
UsernamePasswordCredentials user = (UsernamePasswordCredentials)request.getAttribute("credentials");
Assertion assertion = (Assertion) p.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
Assertion assertion2 = (Assertion) p.getSession().getAttribute("edu.yale.its.tp.cas.client.filter.user");
String userName = null;
if (assertion != null) {
AttributePrincipal principal = assertion.getPrincipal();
userName = principal.getName();
}
if (assertion1 != null) {
AttributePrincipal attributePrincipal = assertion1.getPrincipal();
UserService userService = (UserService)WebContextFactoryUtil.getBean("cn.jasgroup.jasframework.privilege.business.service.impl.UserServiceImpl", UserService.class);
UserBo userBo = userService.queryByLoginName(attributePrincipal.getName());
String userid = attributePrincipal.getName();
session.removeAttribute("user_in_session");
session.removeAttribute("i18n");
session.setAttribute("user_in_session", userBo);
session.getServletContext().setAttribute(userid, session.getId());
p.setAttribute("userName", userid);
request.setAttribute("userName", userid);
}
chain.doFilter(request, response);
}
public void init(FilterConfig fConfig)
throws ServletException
{
}
好了这里就集成好了一个客户端了!如果发现有问题请大佬指出来,一起交流学习谢谢。
这里我参考两篇很好的文章大家可以好好看看
https://blog.csdn.net/u012705000/article/details/104739996?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522158978057819195162513870%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=158978057819195162513870&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogfirst_rank_v2~rank_v25-1-104739996.nonecase&utm_term=cas-server+6.1
https://baike.baidu.com/item/%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95/4940767?fr=aladdin