往期文章一览
【7W字长文】使用LVS+Keepalived实现Nginx高可用,一文搞懂Nginx
【15W字长文】主从复制高可用Redis集群,完整包含Redis所有知识点
分布式会话与单点登录SSO
分布式会话介绍与实现
什么是会话
会话Session代表的是客户端与服务器的一次交互过程,这个过程可以是连续也可以是时断时续的。会话较多用于网络上,TCP的三次握手就创建了一个会话,TCP关闭连接就是关闭会话。会话Session代表的是客户端与服务器的一次交互过程,这个过程可以是连续也可以是时断时续的。曾经的Servlet时代(jsp),一旦用户与服务端交互,服务器用户创建一个session,同时前端会有一个jsessionid,每次交互都会携带。如此一来,服务器只要在接到用户请求时候,就可以拿到jsessionid,并根据这个ID在内存中找到对应的会话session,当拿到session会话后,那么我们就可以操作会话了。
会话存活期间,我们就能认为用户一直处于正在使用着网站的状态,一旦session超期过时,就可以认为用户已经离开网站,停止交互了。用户的身份信息,我们也是通过session来判断的,在session中可以保存不同用户的信息。
session的使用之前在单体部分演示过,代码如下:
@GetMapping("/setSession")public Object setSession(HttpServletRequest request) {HttpSession session = request.getSession();session.setAttribute("userInfo", "new user");session.setMaxInactiveInterval(3600);session.getAttribute("userInfo");// session.removeAttribute("userInfo");return "ok";}
无状态会话
HTTP请求是无状态的,用户向服务端发起多个请求,服务端并不会知道这多次请求都是来自同一用户,这个就是无状态的 。cookie的出现就是为了有状态的记录用户。
常见的前后端分离交互,小程序与服务端交互,安卓与服务端交互,他们都是通过http请求来调用接口,每次交互服务端都不会拿到客户端的状态,我们一般会在每次请求的时候都携带userId或者token,这样后台就可以根据用户ID或者token来获取响应的请求,这样每个用户的下一次请求都能被服务端识别来自同一个用户。
有状态会话
有状态的会话也是基于无状态会话的,Tomcat的会话,就是有状态的。一旦用户和服务器交互,就有会话,会话保存了用户的信息,这样用户就有状态了。服务端会和每个客户端都保持着这样的一层关系,这个由容器来管理,这个session会话是保存到内存空间的,如此一来,当不同的用户访问服务端,那么就能通过会话知道是谁了。tomcat会话的出现也是为了让http请求变得有状态。如果用户不再和服务端交互,那么会话就会超时而消失,结束了他的生命周期,如此一来,每个用户其实都会有一个会话被维护,这就是有状态的会话。
- 注:tomcat会话可以通过手段实现多系统之间的状态同步,但是会损耗一定的时间,一旦发生同步那么用户请求就会等待,这种做法不可取
单Tomcat会话
先来看一下单个tomcat会话,这个就有状态的,用户首次访问服务端,这个时候会话产生,并且会设置jsessionid放入cookie,后续每次请求都会携带jsessionid以保持会话状态
动静分离会话
用户请求服务端,由于前后端分离,前端发起http请求,不会携带任何状态,当用户第一次请求后,我们手动设置一个token,作为会话,放入Redis中,如此作为Redis-session,并且这个token设置后放入前端的cookie中,如此后续的交互,前端只需要传递token给后端,后端就能识别这个用户来自谁了。
集群分布系统会话
集群或者分布式系统本质是多个系统,假设这个里有两个服务器节点,分别是AB系统,一开始用户和A系统交互,那么这个时候的用户状态,我们可以保存到Redis中,作为A系统的会话信息,随后用户的请求进入B系统,那么B系统中的会话我也同样和Redis关联,如此AB系统的session就统一了。当然cookie是会随着用户的访问携带的。这个其实就是分布式会话,通过Redis来保存用户的状态
当我们后端 Web 应用扩展到多台后,我们就会碰到分布式一致性 Session 的问题,主流解决方案有四种:
Session 复制:利用 Tomcat 等 Web 容器同步复制功能(节点一多,复制时占用带宽大,每个节点都要维护所有的会话,占用内存大,不推荐)
Session 前端存储:利用用户浏览器中 Cookie 保存 Session 信息,即前端存储用户信息(需要考虑加密,容易被篡改,不安全,不推荐)
Session 粘滞方案:利用 Nginx 可以做四层 根据IP Hash 或七层 Hash 的特性,保证用户的请求都落在同一台机器上(同一个IP只会到同一个节点上,可以做临时方案)
Session 后端集中存储方案:利用 Redis 集中存储会话,Web 应用重启或扩容,Session 也不会丢失(主流方案,推荐)。
参考链接:
分布式会话和基于TOKEN的分布式会话
小白对话:4种分布式Session的实现方式
基于Redis分布式会话(推荐)
修改登录接口,在用户登录完成之后,保存用户信息(基本信息,角色,权限信息等)到Redis中,设置会话过期时间,若有操作则刷新过期时间,长时间不操作系统自动清除会话,key为UserId或者UUID,并返回给前端,前端请求接口时需携带此信息(使用UUID代表当前用户会话)。
前端请求方式
前端请求只携带UserId
这时key是UserId,可以在登录的时候判断Redis中是否已经存在这个UserId,若有可以阻止登录,保证了一个账号同一时间只能有同一个人登录系统;
前端请求只携带UUID
这时key是UUID,代表系统允许一个账号多个用户登录,因为每次登录请求过来时,都会生成一个UUID作为key,Redis中同样会再存一份相同的用户信息。
前端请求携带UserId和UUID
这时key为UserId,登录时生成的UUID和其他信息一同保存到Redis中。在验证时取出可以对比前端传的UUID和Redis中的UUID,相同则说明是同一个人操作登录的;若不同,则说明由另外的人在异地登录,可以做一个提示,让当前用户重新登录
修改退出登录接口,退出登录时需要删除Redis中保存的用户信息
修改用户信息或角色权限等信息时也要同时修改Redis中的信息
基于SpringSession分布式会话
引入session和security的依赖,因为session依赖了security的一些内容,所以必须导入security依赖
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-Redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
修改配置文件指定session存储的介质
spring:session:store-type: Redis
启动类上开启session功能
@EnableRedisHttpSession// 开启通过Redis管理用户会话@SpringBootApplication(exclude = {SecurityAutoConfiguration.class}) // 移除security依赖中自带的登录,若项目中本身就使用到了security则不需要配置public class StartApplication {public static void main(String[] args) {SpringApplication.run(StartApplication.class, args);}}
在controller层使用HttpServletRequest的getSession()就能获取到HttpSession对象进行赋值和取值了,与原来的使用方式相同。不同点的是原来是由springboot内置的tomcat管理,现在是由Redis统一管理。在分布式环境下,Redis就可以统一管理所有的Session了。
CAS系统简单实现
CAS是Central Authentication Service的缩写,中央认证服务,一种独立开放指令协议。CAS 是 耶鲁大学(Yale University)发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。
协议特点
- 开源的企业级单点登录解决方案。
- CAS Server 为需要独立部署的 Web 应用。
- CAS Client 支持非常多的客户端(这里指单点登录系统中的各个 Web 应用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等语言编写的各种web应用。
- CAS属于Apache 2.0许可证,允许代码修改,再发布(作为开源或商业软件)。
优点
- 多个系统中只需要登录一次,就可以用已登录的身份访问所有相互信任的多个系统。
- 解决子系统登录问题,直接在父系统统一进行登录。
- 企业级业务系统通用账号,只需登录一次。
原理和协议
从结构上看,CAS 包含两个部分: CAS Server 和 CAS Client。CAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server。图1是CAS最基本的协议过程:
CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护受保护的资源。
对于访问受保护资源的每个 Web 请求,CAS Client 会分析该请求的 Http 请求中是否包含 Service Ticket,如果没有,则说明当前用户尚未登录,于是将请求重定向到指定好的 CAS Server 登录地址,并传递 Service (也就是要访问的目的资源地址),以便登录成功过后转回该地址。
用户在第 3 步中输入认证信息,如果登录成功,CAS Server 随机产生一个相当长度、唯一、不可伪造的 Service Ticket,并缓存以待将来验证,之后系统自动重定向到 Service 所在地址,并为客户端浏览器设置一个 Ticket Granted Cookie(TGC),CAS Client 在拿到 Service 和新产生的 Ticket 过后,在第 5,6 步中与 CAS Server 进行身份核实,以确保 Service Ticket 的合法性。
在该协议中,所有与 CAS 的交互均采用 SSL 协议,确保,ST 和 TGC 的安全性。协议工作过程中会有 2 次重定向的过程,但是 CAS Client 与 CAS Server 之间进行 Ticket 验证的过程对于用户是透明的。
另外,CAS 协议中还提供了 Proxy (代理)模式,以适应更加高级、复杂的应用场景,具体介绍可以参考 CAS 官方网站上的相关文档。
什么是单点登录
单点登录又称之为Single Sign On,简称SSO,单点登录可以通过基于用户会话的共享。利用单点登录可以实现用户只登录一次就可以访问几个不同的网站。用户自始至终只在某一个网站下登录后,那么他所产生的会话,就共享给了其他的网站,实现了单点网站登录后,同时间接登录了其他的网站,那么这个其实就是单点登录,他们的会话是共享的,都是同一个用户会话。
例如:登录了qq,那么qq音乐,qq视频之类的就都不用登录了,可以直接访问。
相同顶级域名的单点登录解决方案
因为是相同的顶级域名,顶级域名和下级域名之间cookie是共享的,这样通过cookie+Redis就可以实现单点登录。
如果分布式会话后端是基于Redis的,此会话可以在后端的任意系统都能获取到缓存中的用户数据信息,前端通过使用cookie,可以保证在同域名的一级二级站点下获取,那么这样一来,cookie中的信息userid和token是可以在发送请求的时候携带上的,这样从前端请求后端后是可以获取拿到的,这样一来,其实用户在某一端登录注册以后,其实cookie和Redis中都会带有用户信息,只要用户不退出,那么就能在任意一个站点实现登录了。
那么这个原理主要也是cookie和网站的依赖关系,顶级域名 和下级域名的cookie值是可以共享的,可以被携带至后端的。
二级域名自己的独立cookie是不能共享的,不能被其他二级域名获取,比如:a.abc.com的cookie是不能被b.abc.com共享,两者互不影响,要共享必须设置为.abc.com。
不同顶级域名的单点登录解决方案
这个时候的cookie由于顶级域名不同,就不能实现cookie跨域了,每个站点各自请求到服务端,cookie无法同步。比如,www.aaa.com下的用户发起请求后会有cookie,但是他又访问了www.bbb.com,由于cookie无法携带,所以会要你二次登录。
因为不同顶级域名,cookie直接是不共享的,所以就不能用cookid+reids这种方式来做单点登录了,这个时候我们可以用CAS来实现单点登录。
各个系统之间的登录会通过一个独立的登录系统去做验证,它就相当于是一个中介公司,整合了所有人,你要看房经过中介允许拿钥匙就行,实现了统一的登录。那么这个就称之为CAS系统,CAS全称为Central Authentication Service即中央认证服务,是一个单点登录的解决方案,可以用于不同顶级域名之间的单点登录。
CAS系统详解
CAS全称为Central Authentication Service即中央认证服务,是一个单点登录的解决方案,可以用于不同顶级域名之间的单点登录。
CAS登录流程图
核心票据
用户会话(分布式会话)
TGT(Ticket Grangting Ticket):TGT是CAS为用户签发的登录票据,有TGT就表明用户在CAS上成功登录过。用户在CAS认证成功后,会生成一个TGT对象,放入自己的缓存中(Session),同时生成TGC以cookie的形式写入浏览器。当再次访问CAS时,会先看cookie中是否存在TGC,如果存在则通过TGC获取TGT,如果获取到了TGT则代表用户之前登录过,通过TGT及访问来源生成针对来源的ST,用户就不用再次登录,以此来实现单点登录。
全局门票
TGC(Ticket-granting cookie):TGC就是TGT的唯一标识,以cookie的形式存在在CAS Server三级域名下,是CAS Server 用来明确用户身份的凭证。
临时门票
ST(Service Ticket):ST是CAS为用户签发的访问某一客户端的服务票据。用户访问service时,service发现用户没有ST,就会重定向到 CAS Server 去获取ST。CAS Server 接收到请求后,会先看cookie中是否存在TGC,如果存在则通过TGC获取TGT,如果获取到了TGT则代表用户之前登录过,通过TGT及访问来源生成针对来源的ST。用户凭借ST去访问service,service拿ST 去CAS Server 上进行验证,验证通过service 生成 用户session,并返回资源。
初次登录流程解析
当用户访问到A网站的时候,网站会首先判断他是否在CAS系统登录过,主要是在cookie中看是否有登录信息。如果没有登录,网站会携带上自己的url去访问CAS系统登录接口。
当CAS系统接受到这样一个请求会,会查看自己是否有cookie,如果没有会弹出登录窗口让用户登录。
如果用户登录成功后,CAS系统会进行三步处理
创建用户会话,把用户的信息作为值,把用户的id作为健存入到Redis或者其他数据库中,用来在用户登录进来后获取用户信息
创建用户全局门票,把用户id作为值,随机数当作健存入Redis和cookie中,用以表示在CAS端是否登录
创建临时门票,把随机数当作健值存入Redis中,用于回跳回传
将临时票据作为参数回调回A网站
当A网站有临时票据后会把拿着这个票据访问CAS的兑换票据接口
CAS兑换票据接口会做这样几件事
- CAS会先判断这个票据是否是合法票据
- 如果是合法票据,就把这个临时票据销毁,然后通过CAS中的cookie信息去获取到用户id
- 在通过用户id获取到全局会话,把会话传递会A界面
当A界面获取到会话后,存入自己的cookie中,这样以后就不用再去请求CAS了
二次访问流程
当用户访问到B网站后,B网站也会判断他是否在CAS系统登录过,如果没有cookie信息(注意:这个时候会有两个cooklie信息,一个是用户的全局会话,一个是CAS的全局门票,这里说的没有cookie信息,说的是没有全局会话),网站会携带上自己的url去访问CAS系统登录接口。
因为这个时候CAS系统发现有cookie信息,证明用户已经登录过了,那就只要在创建一个临时票据回传给B网站就好了。
其他剩下操作与第一次访问相同。
参考链接:
一篇文章教你学会单点登录与CAS
CAS 中央认证服务 实现 单点登录(SSO)
代码实现
CAS系统简单实现,service层代码是随便写的,主要起一个抛砖引玉的作用
项目结构
Gradle依赖
plugins {id 'java'id 'org.springframework.boot' version '2.6.7'}apply plugin: 'io.spring.dependency-management'group 'cn.maolinyuan'version '1.0-build'sourceCompatibility = 1.8targetCompatibility = 1.8repositories {maven {url 'https://maven.aliyun.com/repository/central'}maven {url 'https://maven.aliyun.com/repository/public'}maven {url 'https://maven.aliyun.com/repository/jcenter'}maven {url 'https://maven.aliyun.com/repository/google'}maven {url 'https://maven.aliyun.com/repository/spring'}maven {url 'https://maven.aliyun.com/repository/spring-plugin'}maven {url 'https://maven.aliyun.com/repository/grails-core'}maven {url 'https://maven.aliyun.com/repository/apache-snapshots'}maven {url 'https://maven.aliyun.com/repository/gradle-plugin'}mavenCentral()}dependencies {implementation("junit:junit:4.12","org.springframework.boot:spring-boot-starter","org.springframework.boot:spring-boot-starter-web","org.springframework.boot:spring-boot-starter-aop","org.springframework.boot:spring-boot-starter-data-redis","org.springframework.boot:spring-boot-starter-thymeleaf")testImplementation("org.springframework.boot:spring-boot-starter-test")annotationProcessor("org.projectlombok:lombok:1.18.24")compileOnly("org.projectlombok:lombok:1.18.24","org.springframework.boot:spring-boot-dependencies:2.6.7")}
配置文件
application.yml
server:port: 8989servlet:context-path: /ssospring:thymeleaf:mode: HTMLencoding: UTF-8prefix: classpath:/templates/suffix: .htmlprofiles:include: redis
application-redis.yml
spring:# redis 配置redis:# 地址host: 192.168.5.223# 端口,默认为6379port: 6379# 数据库索引database: 0# 密码password: imooc# 连接超时时间timeout: 10slettuce:pool:# 连接池中的最小空闲连接min-idle: 0# 连接池中的最大空闲连接max-idle: 8# 连接池的最大数据库连接数max-active: 8# #连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms
资源文件
templates/login.html
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head><meta charset="UTF-8"><title>SSO LOGIN</title></head><body><h1>CENTRAL AUTHENTICATION SERVICE</h1><form action="doLogin" method="post"><label><input type="text" name="username" placeholder="请输入用户名"/></label><label><input type="password" name="password" placeholder="请输入密码"/></label><input type="hidden" name="returnUrl" th:value="${returnUrl}"/><input type="submit" value="提交登录"/></form><span style="color: red" th:text="${errmsg}"></span></body></html>
controller
SSOController
package cn.maolinyuan.controller;import cn.maolinyuan.po.User;import cn.maolinyuan.service.IUserService;import cn.maolinyuan.vo.UserVo;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.beans.BeanUtils;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import javax.annotation.Resource;import javax.servlet.http.Cookie;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.UUID;import java.util.concurrent.TimeUnit;/** * sso 认证服务 * * @author maolinyuan * @version 1.0 * @date 2022/5/5 16:27 */@Controller@RequestMapping("/auth")public class SSOController {@Resourceprivate IUserService service;@Resourceprivate RedisTemplate<Object, Object> redisTemplate;private final String REDIS_USER_TOKEN = "redis_user_token";private final String REDIS_USER_TICKET = "redis_user_ticket";private final String REDIS_TMP_TICKET = "redis_tmp_ticket";private final String COOKIE_USER_TICKET = "cookie_user_ticket";private final ObjectMapper objectMapper = new ObjectMapper();@GetMapping("/login")public String login(String returnUrl,Model model,HttpServletRequest request,HttpServletResponse response) {model.addAttribute("returnUrl", returnUrl);// 获取userTicket门票,如果cookie中能够获取说明用户登录过,此时签发一个一次性的临时门票String userTicket = getCookie(COOKIE_USER_TICKET, request);boolean isVerified = verifyUserTicket(userTicket);if (isVerified) {String tmpTicket = createTmpTicket();return "redirect:" + returnUrl + "" /> + tmpTicket;}// 用户从未登录过跳转到CAS的统一认证页面return "login";}/** * 验证用户全局门票是否有效 * * @param userTicket * @return */private boolean verifyUserTicket(String userTicket) {if (null == userTicket || "".equals(userTicket)) {return false;}// 获取用户会话String userId = String.valueOf(redisTemplate.opsForValue().get(REDIS_USER_TICKET + ":" + userTicket));if (null == userId || "".equals(userId)) {return false;}String userVoStr = String.valueOf(redisTemplate.opsForValue().get(REDIS_USER_TOKEN + ":" + userId));return null != userVoStr && !"".equals(userVoStr);}/** * CAS的统一登录接口 * 1. 登陆后创建用户全局会话 uniqueToken * 2. 创建用户全局门票,用以表示在CAS端是否登录 userTicket * 3. 创建用户的临时门票,用于会跳回传 tmpTicket * 用临时票据加上cookie的全局门票才可以获取到用户会话信息 * * @param username * @param password * @param returnUrl * @param model * @param request * @param response * @return * @throws JsonProcessingException */@PostMapping("/doLogin")public String doLogin(String username,String password,String returnUrl,Model model,HttpServletRequest request,HttpServletResponse response) throws JsonProcessingException {model.addAttribute("returnUrl", returnUrl);// 判断参数不能为空if (null != username && !"".equals(username) && null != password && !"".equals(password)) {model.addAttribute("errmsg", "用户名和密码不能为空");return "login";}// 判断用户User user = service.findUserByUsername(username);assert password != null;if (!password.equals(user.getPassword())) {model.addAttribute("errmsg", "用户名或密码错误");return "login";}// redis用户会话 用uuid来标识 使用用户id关联用户信息和创建用户会话的uuidString uniqueToken = UUID.randomUUID().toString().trim();UserVo userVo = new UserVo();BeanUtils.copyProperties(user, userVo);userVo.setUniqueToken(uniqueToken);redisTemplate.opsForValue().set(REDIS_USER_TOKEN + ":" + user.getId(), objectMapper.writeValueAsString(userVo));// 生成ticket门票,全局门票,代表用户在CAS端登录过String userTicket = UUID.randomUUID().toString().trim();// 用户全局门票需要放入CAS端的cookie中setCookie(COOKIE_USER_TICKET, userTicket, response);// userTicket关联用户id,并且放入redis中,代表用户有门票了,可以访问各个网站redisTemplate.opsForValue().set(REDIS_USER_TICKET + ":" + userTicket, user.getId());// 生成临时票据,会跳到调用端网站,是由CAS签发的一个一次性的临时ticketString tmpTicket = createTmpTicket();return "redirect:" + returnUrl + "?tmpTicket=" + tmpTicket;}/** * 验证临时票据,返回值可以使用统一返回类包装。 * * @param tmpTicket * @param request * @param response * @return */@PostMapping("/verifyTmpTicket")@ResponseBodypublic UserVo verifyTmpTicket(String tmpTicket,HttpServletRequest request,HttpServletResponse response) throws JsonProcessingException {// 使用一次性票据来验证用户是否登陆过,如果登录过,把用户会话返回给站点,使用完毕后销毁临时票据String tempTicketValue = String.valueOf(redisTemplate.opsForValue().get(REDIS_TMP_TICKET + ":" + tmpTicket));if (null == tempTicketValue || "".equals(tempTicketValue)) {// 用户票据异常 不返回用户会话信息return null;}// 如果临时票据有效,则需要销毁,并且拿到CAS端cookie中的全局userTicket,以此再获取用户的全局会话信息// 如果value值经过加密的,需要将value解密,或者将tmpTicket加密之后进行对比if (!tempTicketValue.equals(tmpTicket)) {// 用户票据异常return null;} else {// 销毁临时票据redisTemplate.delete(REDIS_TMP_TICKET + ":" + tmpTicket);}// 验证并且从cookie中获取用户的userTicketString userTicket = getCookie(COOKIE_USER_TICKET, request);// 获取用户会话String userId = String.valueOf(redisTemplate.opsForValue().get(REDIS_USER_TICKET + ":" + userTicket));if (null == userId || "".equals(userId)) {// 用户票据异常return null;}String userVoStr = String.valueOf(redisTemplate.opsForValue().get(REDIS_USER_TOKEN + ":" + userId));if (null == userVoStr || "".equals(userVoStr)) {// 用户会话信息异常return null;}// 返回用户会话信息到前端,前端需要保存此信息return objectMapper.readValue(userVoStr, UserVo.class);}/** * 退出登录 * * 当a网站点击了退出登录之后,会清除a网站本地的用户信息和服务器的分布式会话信息, * 而其他网站的所保存的本地用户会话信息还存在,所以,当所有网站请求后端服务时都 * 要做拦截器统一校验分布式会话是否存在,不存在则需要提示前端让他重新登录。 * @param userId * @param request * @param response */@PostMapping("/logout")@ResponseBodypublic void logout(String userId, HttpServletRequest request, HttpServletResponse response) {// 获取CAS中的用户门票String userTicket = getCookie(COOKIE_USER_TICKET, request);// 清除redis和cookie中的userTicket用户票据和会话信息deleteCookie(COOKIE_USER_TICKET,response);redisTemplate.delete(REDIS_USER_TICKET + ":" + userTicket);redisTemplate.delete(REDIS_USER_TOKEN + ":" + userId);}/** * 创建临时票据 * * @return */private String createTmpTicket() {String tmpTicket = UUID.randomUUID().toString().trim();// 其中value值可以进行加密后再保存redisTemplate.opsForValue().set(REDIS_TMP_TICKET + ":" + tmpTicket, tmpTicket, 600, TimeUnit.SECONDS);return tmpTicket;}/** * 设置CAS的cookie * * @param key * @param val * @param response */private void setCookie(String key, String val, HttpServletResponse response) {Cookie cookie = new Cookie(key, val);// 重要:设置CAS服务的域名或者ip,只有这样浏览器在访问CAS的时候才会自动带上 userTicketcookie.setDomain("sso.com");cookie.setPath("/");response.addCookie(cookie);}/** * 删除CAS的cookie * * @param key * @param response */private void deleteCookie(String key, HttpServletResponse response) {Cookie cookie = new Cookie(key, null);// 重要:设置CAS服务的域名或者ip,只有这样浏览器在访问CAS的时候才会自动带上 userTicketcookie.setDomain("sso.com");cookie.setPath("/");cookie.setMaxAge(-1);response.addCookie(cookie);}/** * 获取请求中的cookie * * @param key * @param request * @return */private String getCookie(String key, HttpServletRequest request) {Cookie[] cookies = request.getCookies();if (null == cookies || cookies.length == 0 || null == key) {return null;}for (Cookie cookie : cookies) {if (cookie.getName().equals(key)) {return cookie.getValue();}}return null;}}
service
IUserService
package cn.maolinyuan.service;import cn.maolinyuan.po.User;/** * @author maolinyuan * @version 1.0 * @date 2022/5/5 16:52 */public interface IUserService {/** * 根据username获取用户 * @param username * @return */User findUserByUsername(String username);}
UserServiceImpl
package cn.maolinyuan.service.impl;import cn.maolinyuan.po.User;import cn.maolinyuan.service.IUserService;import org.springframework.stereotype.Service;/** * @author maolinyuan * @version 1.0 * @date 2022/5/5 16:52 */@Servicepublic class UserServiceImpl implements IUserService {/** * 根据username获取用户 * * @param username * @return */@Overridepublic User findUserByUsername(String username) {User user = new User();user.setId(1L);user.setUsername("root");user.setPassword("admin");user.setState("0");return user;}}
po
User
package cn.maolinyuan.po;import lombok.Data;import java.io.Serializable;/** * @author maolinyuan * @version 1.0 * @date 2022/5/5 16:54 */@Datapublic class User implements Serializable {private Long id;private String username;private String password;private String state;}
vo
package cn.maolinyuan.vo;import lombok.Data;import java.io.Serializable;/** * @author maolinyuan * @version 1.0 * @date 2022/5/6 9:34 */@Datapublic class UserVo implements Serializable {private Long id;private String username;private String password;private String state;private String uniqueToken;}
filter
CrossFilter
package cn.maolinyuan.filter;import org.springframework.stereotype.Component;import javax.servlet.*;import javax.servlet.annotation.WebFilter;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;/** * @author maolinyuan * @version 1.0 * @date 2021/2/1 13:49 */@Component@WebFilter("/*")public class CrossFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {//都允许跨域HttpServletResponse response = (HttpServletResponse) servletResponse;HttpServletRequest request = (HttpServletRequest) servletRequest;response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE, PATCH");response.setHeader("Access-Control-Max-Age", "3600");response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));response.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With, Authentication , authentication, token, Token");response.setHeader("Access-Control-Allow-Credentials", "true");//如果是OPTIONS请求就return 往后执行会到业务代码中 他不带参数会产生异常if ("OPTIONS".equals(request.getMethod())) {return;}//第二次就是POST请求 之前设置了跨域就能正常执行代码了filterChain.doFilter(servletRequest, servletResponse);}@Overridepublic void destroy() {}}
config
package cn.maolinyuan.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.JsonTypeInfo;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.ObjectMapper;import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;import org.springframework.cache.annotation.CachingConfigurerSupport;import org.springframework.cache.annotation.EnableCaching;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;/** * redis配置 */@Configuration@EnableCachingpublic class RedisConfig extends CachingConfigurerSupport {@Bean@SuppressWarnings(value = {"unchecked", "rawtypes"})public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper mapper = new ObjectMapper();mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);serializer.setObjectMapper(mapper);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}}
启动类
CasStart
package cn.maolinyuan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;/** * @author maolinyuan * @version 1.0 * @date 2022/5/5 16:21 */@SpringBootApplicationpublic class CasStart {public static void main(String[] args) {SpringApplication.run(CasStart.class,args);}}