目录:
一、session和cookie
二、CAS名词
三、代码逻辑步骤
四、CAS大致工作流程
五、业务流程
六、图解
一、了解session和cookie
session文章: https://mp.weixin.qq.com/s/kjiAznd2MKD0Y-shXtzVQg
cookie文章: https://mp.weixin.qq.com/s/JW7mxXEqrV1rZ_pQOteXGQ
1.1、session:
session是服务端(后端)的存在内存中,一次请求后,后端tomcat就会判断是否有这个session来判断要不要创建,创建完成后就会返回给前端一个jsessionId,在set-cookie属性上,之后的每次请求都会自动携带这个jsessionId返回给后端(注:一个域名下同一个浏览器sessionid是一样的)在不关闭浏览器的情况下这个jessionid都存在。
销毁方式:
1、如果一直没有新的请求这个session在后端会自动销毁,超时时间默认是30分钟。
2、关闭重启服务器
3、关闭浏览器sessionid
笔记:
1、Session session=requerst.getSession(false)//false:没有就是null。true:没有就创建。 session.invalidate()//销毁内存中的session
2、reponse.sendRedirect(location) //该方法用于生成 302 响应码和 Location 响应头。会自动重定向。重定向地址就是Location中的地址。
3、把 session ID 加到一个连接可以使用一对方法来简化:response.encodeURL() 使 URL 包含 session ID,如果你需要使用重定向,可以使用 response.encodeRedirectURL () 来对 URL 进行编码。
4、request.getQueryString()得到接口地址?后面拼接的内容
5、HttpURLConnection.openConnection()后端向指定地址请求并返回内容。
6、InetAddress获取本地ip用户名等等。
7、负载均衡时如果 前后几次都在一台上面sessionid不会变,如果切换了机器sessionId就会变.这里对于单点登录来说,作用就是,你需要在session中存放数据好比token信息。这样每次请求登录你就需要验证token信息是否真实,sessionid是否还存在。
1.2、Cookie
cookie是客户端(前端)的(注:一次会话技术,浏览器关闭的就没了)如果服务器需要记录该用户的状态,就使用response向浏览器发送一个Cookie,浏览器会把Cookie保存起来。当浏览器再次访问服务器的时候,浏览器会把请求的网址连同Cookie一同交给服务器。
浏览器同源策略:
1、cookie跨域携带不了。跨子域一二级域名可以。
2、cookie可以给本接口路径设置cookie但是设置到不到跨域的域名下(如果跨域情况下是,否则不是)
3、只有访问这个网站才会携带这个cookie过去。(ip/域名相同时会自动携带) 4、cookie
的expirs字段设置的值就会存储在本地cookies中。
HTTP请求自动带上Cookie的前提:
1、浏览器端某个Cookie的domain字段等于aaa.www.com或者www.com
2、都是http或者https,或者不同的情况下Secure属性为false
3、要发送请求的路径,即上面的xxxxx跟浏览器端Cookie的path属性必须一致,或者是浏览器端Cookie的path的子目录,比如浏览器端Cookie的path为/test,那么xxxxxxx必须为/test或者/test/xxxx等子目录才可以。
笔记:
Cookie cookie=new Cookie(“token”,token);//后端给返回请求头添加cookie
cookie.setPath(“/”); cookie.setDomain(“192.168.2.70”);//跨域的地址不用跨域不用设置
response.addCookie(cookie); response.addHeader(“P3P”,“CP=CAO PSAOUR”);//p3p跨域携带信息
二、CAS名词
对接第三方的登录,对方算是cas server 我们算是cas client。cas server与cas client是一对多的关系。
这下面的概念比较重要一定要先了解记住,项目开发时你会要去理解这些。
1、service是我们(即:cas client)这边的服务ip也可以说就是一个接口。对于cas server 就是一个服务。
2、TGT(Ticket Grangting Ticket) 是cas server为每个登录用户创建的一个登录令牌。TGT封装了SessionCookie值以及Cookie值对应的用户信息。
3、ST(serviceticket)是cas server 为cas client端签发的认证令牌。用户访问第三方应用(指的就是casclient端),client端没有st就会重定向到登录然后获取st。CASServer发现用户有TGT,则签发一个ST,返回给用户。用户使用ST作为ticket参数去访问service,service拿ST去CASServer验证,验证通过后,得到当前登录用户的登录名。
4、TGC(Ticket Grangting Cookie)是casserver返回给浏览器的一个cookie值,该值是挂在cas server ip的地址上,用于下次请求casserver时携带(ip相同会自动携带)过去验证是否登录。cookie值是否正确,都正确则不用登录,否则调整登录。
5、TGT和ST,是一对多的关系。一个TGT会维护一个 services列表,每当为用户创建一个ST并认证通过后,会将这个ST添加到TGT的services列表中。这样,在CASServer端,这个services列表实际维护了一个用户登录过的所有CASClient。这就为实现统一注销打下了基础。
三、代码逻辑步骤:
共三个filter顺序为(SingleSignOutFilter->AuthenticationFilter->DefaultTicketValidateFilter)
如果对接方提供jar包的,去maven仓库(https://mvnrepository.com/)中找cas client的jar包引入
3.1:先进SingleSignOutFilter单点登出fitler
判断是否存在ticket:
1)没有ticket
1、判断是不是登出请求 如果是就清除session
2、否则直接进入下一个filter。
2)有ticket
1、先清除sessionId
2、新增一个sessionId
代码如下:
package org.jasig.cas.client.session;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLEncoder;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
public final class SingleSignOutFilter extends AbstractConfigurationFilter {
private static final SingleSignOutHandler handler = new SingleSignOutHandler();
private static String clusterUrls = null;
public SingleSignOutFilter() {
}
public void init(FilterConfig filterConfig) throws ServletException {
handler.init();
}
public void setSessionMappingStorage(SessionMappingStorage storage) {
handler.setSessionMappingStorage(storage);
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
if (handler.isTokenRequest(request)) {
handler.recordSession(request);
} else {
if (handler.isLogoutRequest(request)) {
handler.destroySession(request);
try {
String logout = request.getParameter("logout");
String param = "logout=" + URLEncoder.encode(logout, "UTF-8");
String url = "url";
send(url, param);
}
} catch (Exception var11) {
var11.printStackTrace();
}
return;
}
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
protected static SingleSignOutHandler getSingleSignOutHandler() {
return handler;
}
}
3.1.2:进入单点登录的filter( SSOFiter(本地自己实现的filter))
1、判断session在本地是否存在
1)、存在判断是不是登出地址是的话重定向到登录地址。
2)、进入下一个filter: filterChain.doFilter(servletRequest, servletResponse);//这个方法指的是进入下一filter
2、本地不存在session进入AuthenticationFilter
1)判断session中是否带有_const_cas_assertion_
2)有就进入下一个filter不存在就重定向到第三方登录页面
3)登录成功后第三方登录会携带ticket重定向回我们自己的项目中然后重复上面步骤
4)第一登录成功后SingleSignOutFilter会有ticket数据然后进入AuthenticationFilter此时_const_cas_assertion_依然没有数据进入下一个filter(ticketValidateFilter)进行ticket校验(内部方法constructValidationurl)拼接校验接口后调用retrieveResponseFromServer方法后端请求第三方登录认证服务的接口返回数据,如果失败报错。如果正常校验通过返回Assertion。给request和session上设置_const_cas_assertion_。然后进入自己继承DefaultTicketValidateFilter的ticketValidationFilter类进行assertion内容的解析,获取用户验证本地库中是否存在,存在下发token返回true,不存在返回false重定向到登录页面。
代码如下:
package org.jasig.cas.client.authentication;
import java.io.IOException;
import java.net.URLEncoder;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.validation.Assertion;
public class AuthenticationFilter extends AbstractCasFilter {
private String casServerLoginUrl;
public AuthenticationFilter() {
}
protected void initInternal(FilterConfig filterConfig) throws ServletException {
}
public void init() {
super.init();
}
public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
HttpSession session = request.getSession(false);
Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;
String queryString = request.getQueryString();
if (assertion != null) {
filterChain.doFilter(request, response);
} else {
String serviceUrl = this.constructServiceUrl(request, response);
String ticket = utils.getTicket(request, "ticket");
if (!CommonUtils.isNotBlank(ticket)) {
String modifiedServiceUrl= serviceUrl;
String urlToRedirectTo = "http://caseserver.com";
String stService = "http://" + LocalHost + ":" + LocalPort + request.getContextPath() + "/";
try {
if (urlToRedirectTo.indexOf("?") > 0) {
urlToRedirectTo = urlToRedirectTo + "&stService=" + URLEncoder.encode(stService, "UTF-8");
} else {
urlToRedirectTo = urlToRedirectTo + "?stService=" + URLEncoder.encode(stService, "UTF-8");
}
} catch (Exception var19) {
}
response.sendRedirect(urlToRedirectTo);
} else {
filterChain.doFilter(request, response);
}
}
}
}
四、CAS大致工作流程:
CAS server生成cookie(叫TGC),写入浏览器,同时生成一个TGT对象,放入自己服务器的缓存中,TGT对象的ID就是cookie的值。
Cookie中的CASTGC:向cookie中添加该值的目的是当下次访问www.cas.server.com时,浏览器将Cookie中的TGC携带到服务器,服务器根据这个TGC,查找与之对应的TGT。从而判断用户是否登录过了,是否需要展示登录页面。TGT与TGC的关系就像SESSION与Cookie中SESSIONID的关系。
CAS server 注册成功后会返回一个TGC放在cookie中,下次登录只要浏览器不关闭不禁用cookie服务端重启也不影响TGC在casserver的注册(TGT)
登录后返回TGC存在浏览器的会话中。下次请求path /ssologin或/ssologin/XXX都会携带这个cookie过去,然后casserver验证这个TGC是否存在,判断用户要不要进入登录页面。
五、业务流程:
对接的话第三方的登录的话,你只需要关心cas client。对方的系统或者首页中有一个跳转你们业务系统的按钮,这个按钮调用你后端写的一个接口(controller),我这的实现是为了不影响原有的业务系统所以加了个单独的项目去做这个登录的操作,然后因为有过滤器Filter的存在。所以调用你接口时就会先走Filter。也就是上面所述的Filter流程。都验证完成后就进入你的接口进行重定向到业务系统页面(如果有token就携带token可以通过地址携带)就基本完成了单点登录。然后就是退出,对于我们业务系统来说,退出时调用后端退出接口,然后后端调用第三方的退出接口这一步主要是为了把对方ip/域名下的cookie中的TGC给清空掉。同时他们那边的TGT也会把这个TGC给清除掉。
图解:见该文章第四点 https://www.cnblogs.com/lihuidu/p/6495247.html
插曲:如果是web.xml格式的cas配置文件的话,你的项目又是springboot时用配置类代替web.xml。类内容如下
package com.xxx.sso;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.springframework.beans.factory.annotation.Value;
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.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
@ComponentScan
public class WebMvcConfiguration implements WebMvcConfigurer {
@Value("${casServerLoginUrl}")
private String casServerLoginUrl;
@Value("${casServerUrlPrefix}")
private String casServerUrlPrefix;
@Value("${ssoLogoutUrl}")
private String ssoLogoutUrl;
//Filter CAS Single Sign Out Filter
@Bean
public FilterRegistrationBean SingleSignOutFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new SingleSignOutFilter());//创建上面的自定义的WebFilter对象
registration.addUrlPatterns("/*");
registration.setName("SingleSignOutFilter");
registration.setOrder(1);//启动时候的优先级
return registration;
}
@Bean
public FilterRegistrationBean Filter() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new SSOFilter());
registration.addUrlPatterns("/*");//filter地址
registration.addInitParameter("casServerLoginUrl", casServerLoginUrl);//相当于web.xml中的<param-name>、<param-value>。可以添加n个
registration.addInitParameter("casServerUrlPrefix", casServerUrlPrefix);
registration.addInitParameter("ssoLogoutUrl", ssoLogoutUrl);
registration.setName("Filter");//FilterName
registration.setOrder(2);//启动时候的优先级
return registration;
}
//listener
@Bean
public ServletListenerRegistrationBean myListener() {
ServletListenerRegistrationBean myListener = new ServletListenerRegistrationBean<>();
myListener.setListener(new Listener());
return myListener;
}
}