往期文章一览
【7W字长文】使用LVS+Keepalived实现Nginx高可用,一文搞懂Nginx
【15W字长文】主从复制高可用Redis集群,完整包含Redis所有知识点
会话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会话,这个就有状态的,用户首次访问服务端,这个时候会话产生,并且会设置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中,设置会话过期时间,若有操作则刷新过期时间,长时间不操作系统自动清除会话,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中的信息
引入session和security的依赖,因为session依赖了security的一些内容,所以必须导入security依赖
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-RedisartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
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是Central Authentication Service的缩写,中央认证服务,一种独立开放指令协议。CAS 是 耶鲁大学(Yale University)发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法,CAS 在 2004 年 12 月正式成为 JA-SIG 的一个项目。
从结构上看,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全称为Central Authentication Service即中央认证服务,是一个单点登录的解决方案,可以用于不同顶级域名之间的单点登录。
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兑换票据接口会做这样几件事
当A界面获取到会话后,存入自己的cookie中,这样以后就不用再去请求CAS了
当用户访问到B网站后,B网站也会判断他是否在CAS系统登录过,如果没有cookie信息(注意:这个时候会有两个cooklie信息,一个是用户的全局会话,一个是CAS的全局门票,这里说的没有cookie信息,说的是没有全局会话),网站会携带上自己的url去访问CAS系统登录接口。
因为这个时候CAS系统发现有cookie信息,证明用户已经登录过了,那就只要在创建一个临时票据回传给B网站就好了。
其他剩下操作与第一次访问相同。
参考链接:
一篇文章教你学会单点登录与CAS
CAS 中央认证服务 实现 单点登录(SSO)
CAS系统简单实现,service层代码是随便写的,主要起一个抛砖引玉的作用
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.8
targetCompatibility = 1.8
repositories {
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: 8989
servlet:
context-path: /sso
spring:
thymeleaf:
mode: HTML
encoding: UTF-8
prefix: classpath:/templates/
suffix: .html
profiles:
include: redis
application-redis.yml
spring:
# redis 配置
redis:
# 地址
host: 192.168.5.223
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password: imooc
# 连接超时时间
timeout: 10s
lettuce:
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 LOGINtitle>
head>
<body>
<h1>CENTRAL AUTHENTICATION SERVICEh1>
<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>
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 {
@Resource
private IUserService service;
@Resource
private 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=" + 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关联用户信息和创建用户会话的uuid
String 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签发的一个一次性的临时ticket
String tmpTicket = createTmpTicket();
return "redirect:" + returnUrl + "?tmpTicket=" + tmpTicket;
}
/**
* 验证临时票据,返回值可以使用统一返回类包装。
*
* @param tmpTicket
* @param request
* @param response
* @return
*/
@PostMapping("/verifyTmpTicket")
@ResponseBody
public 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中获取用户的userTicket
String 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")
@ResponseBody
public 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的时候才会自动带上 userTicket
cookie.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的时候才会自动带上 userTicket
cookie.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;
}
}
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
*/
@Service
public class UserServiceImpl implements IUserService {
/**
* 根据username获取用户
*
* @param username
* @return
*/
@Override
public User findUserByUsername(String username) {
User user = new User();
user.setId(1L);
user.setUsername("root");
user.setPassword("admin");
user.setState("0");
return user;
}
}
User
package cn.maolinyuan.po;
import lombok.Data;
import java.io.Serializable;
/**
* @author maolinyuan
* @version 1.0
* @date 2022/5/5 16:54
*/
@Data
public class User implements Serializable {
private Long id;
private String username;
private String password;
private String state;
}
package cn.maolinyuan.vo;
import lombok.Data;
import java.io.Serializable;
/**
* @author maolinyuan
* @version 1.0
* @date 2022/5/6 9:34
*/
@Data
public class UserVo implements Serializable {
private Long id;
private String username;
private String password;
private String state;
private String uniqueToken;
}
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 {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public 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);
}
@Override
public void destroy() {
}
}
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
@EnableCaching
public 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
*/
@SpringBootApplication
public class CasStart {
public static void main(String[] args) {
SpringApplication.run(CasStart.class,args);
}
}