文章目录
一 . Session与Cookie区别
1.1 Cookie
简单介绍:
Cookie(小甜饼)是浏览器保存在本地的文本内容,常常搭配Session来保持用户登陆态
特性(不只一点):
- 只能携带同一域名下或子域名(可通过修改domain)的cookie到服务端
localhost与127.0.0.1虽然意思相同但是它们却属于跨域访问:无法携带cookie到另一域下
1.2 Session
简单介绍:
Session是服务器端使用的一种记录客户端状态的机制,使用上比Cookie简单一些,相应的也增加了服务器的存储压力。
特性:
- 将数据存储在服务器端且安全性较高
- Session的数据存储在Tomcat服务器的内存中,具有时效性
二 . 传统Session方式
1.1 传统session
场景: nginx做负载均衡,同一个应用部署到了两台服务器,实现登陆
当用户请求登陆接口时,通过nginx里配置的不同的策略将请求打到某一台服务器上。
问题 1: session存储在内存中到底是由服务器管理还是tomcat管理呢?
明确一点:session存储在服务器的内存中,是tomcat来管理的。
我们知道内存中的数据在重启的时候将丢失,如果有服务器管理session,想要session丢失是不是要重启服务器呢?事实上,在实践中我们发现想要让session丢失是重启tomcat
问题 2: 当用户请求登陆后由nginx负载到了服务器1并确认登陆信息,tomcat生成session保存在服务器上,当该用户再次登陆的时候,登陆请求有可能打到服务器2,但是 服务器2并没有该用户的session信息,因此用户就需要再次登陆,降低了用户的体验
解决方案1:
有人提出当同一个用户请求时,通过nginx的一些配置(IP HASH)将请求打到同一台服务器上,这样就不会造成上述问题,如果采用该种方案将会导致架构不是高可用的,如果此时服务器1挂掉了,那么是不是此用户就没办法访问该服务了呢?
解决方案2:
通过加入一台Redis服务器,当用户请求时,每台服务器都去同一个Redis里查询该用户是否已经登陆过了
1.2 传统Session实践1.0
用户第一次登陆的时候,将用户名和密码传到后端,后端进行校验成功后,再将该用户信息保存在session中,并且tomcat会生成一个sessionId,在响应头部加入set-cookies:sessionId,返回给浏览器,浏览器将sessionid保存在本地,以后该用户在同一域名下请求该服务其它接口时都会携带着该cookie(sessionid),后端收到sessionid,在内存中查找该session,然后在后台代码进行校验或返回信息
1.3 传统session实践2.0
问题1: 验证cookie的不允许跨域携带cookie的特性?
验证:
- 创建项目的过程省略
- Controller层代码:
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/login")
public String login(@RequestParam String userName, @RequestParam String password, HttpSession session) {
session.setAttribute("login_user", userName);
return "登陆成功";
}
@GetMapping("/info")
public String info(HttpSession session) {
return "当前登陆的是:" + session.getAttribute("login_user");
}
}
- 启动项目
- 访问
1.首先在浏览器中一localhos:8081/user/login?userName=ray&password=123来登录
2.然后我们来获取信息:localhost:8081/user/info 浏览器收到信息:当前登陆的是ray
3.但如果我们在同一浏览器中跨域来访问接口:以127.0.0.1:8081/user/info 浏览器收到信息:当前登陆的是null
结论:两种结果很明显验证了在从127.0.0.1:8081/user/info来访问时,事实上是没有将cookie传到服务 器,以此证明了cookie的特性
那么如果我们就是想跨域来访问这个用户的信息呢?
解决方式:将从浏览器的控制台中获取出localhost域名下的sessionId将其复制粘贴到127.0.0.1域名下随请求一并发送到服务端
**问题2:**验证session是tomcat管理而不是服务器管理的?
- 创建项目省略
- 上一个实践的同一套代码启动两个服务8081&8082端口
1. 首先我们以localhost:8081/user/login?userName=ray&password=123来登陆
2. 获取到sessionId并将其复制到localhost:8082/user/info的请求里,结果发现浏览器展示的信息 是:当前登陆的是null
3. 问题1看见我们已经将sessionId复制了,应该可以成功获取到信息了 啊,但是并没有获取到信息,可见session肯定不是服务器来管理了,如果是那么同一台服务器获取到sessionId就应该返回信息,因此session不是服务器管理的
4. 总结:springboot应用内嵌了tomcat,由于用户是在8081服务登陆的session是由8081的tomcat管理的,而8082服务中没有session信息,所 以就没办法查到
三 . Spring-Session
1.1 简述
旨在解决分布式session问题,详情参看Spring-session官网
1.2 实践SpringBoot+docker+redis
步骤:
- 服务器用docker安装redis
docker run -d -p 6379:6379 redis:6.0
- 创建springboot项目,在pom.xml中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- application.yml写配置
spring:
redis:
port: 6379
host: 47.97.214.211
session:
store-type: redis
- controller 层代码依然是上文所提到的
- 重启服务
访问login接口,打开浏览器控制台发现除了正常的cookie,session机制中有的JSESSION之外还有SESSION,并且我们通过连接服务器redis查看0号数据库中保存了spring:session键值,过期时间等
- 再次重启服务器,不登陆直接访问info接口,发现我们仍然可以获得信息
我们启动两个不同端口的相同服务,依然会发现不再出现传统session的问题,即用户明明登陆了,确还需要再次登陆的问题
- 参考源码:SessionRepositoryRequestWrapper类:原因在于Spring-session再http请求时加了个拦截器来寻找redis中spring-session,如果存在则证明该用户已经登陆
四 . Token+Redis
步骤:
- docker安装redis
docker run -d -p 6379:6379 redis:6.0
- 创建springboot项目,在pom.xml中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- application.yml写配置
spring:
redis:
port: 6379
host: 47.97.214.211
- controller层
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("loginWithToken")
public String loginWithToken(@RequestParam String userName, @RequestParam String password) {
// 省略一些数据库校验,假设账号密码都正确
String key = "token_" + UUID.randomUUID().toString();
redisTemplate.opsForValue().set(key, userName, 3600, TimeUnit.SECONDS);
return key;
}
@GetMapping("infoWithToken")
public String infoWithToken(@RequestParam String token) {
return "当前登陆的是:" + redisTemplate.opsForValue().get(token);
}
- 总结:实际上原理和spring-session类似,引入三方服务器来存储用户信息,每次访问时都来看第三方服务器是否存在该用户的信息
五 . JWT
1.1 简述
- Json Web token:实际上就是后端返回加密的token给前端,前端请求接口时,在header里携带这个token到服务端,服务端用解密算法解密即可
- JWT不需要Redis就可以做到分布式Session,原因在于:服务端的加密解密
- 通过网站查看加密后的token
- JWT里的内容可以被解析,但是不能被篡改
- JWT和Spring-Session/普通token+redis的本质区别在于:内容可以被解析,普通的token是不可以被解析的同时也不能被篡改
1.2 实践
步骤:
- 创建项目pom.xml引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.16.0</version>
</dependency>
- Controller层
@GetMapping("/loginWithJwt")
public String loginWithJwt(@RequestParam String userName, @RequestParam String password) {
Algorithm algorithm = Algorithm.HMAC256(JWT_KEY);
String token = JWT.create()
.withClaim("login_user", userName)
.withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
.sign(algorithm);
return success;
}
@GetMapping("infoWithJwt")
public String infoWithJwt(@RequestHeader String token) {
Algorithm algorithm = Algorithm.HMAC256(JWT_KEY);
JWTVerifier verifier = JWT.require(algorithm)
.build(); //Reusable verifier instance
try {
DecodedJWT jwt = verifier.verify(token);
return jwt.getClaim("login_user").asString();
} catch (TokenExpiredException e) {
// token过期
} catch (JWTDecodeException e) {
// 解码失败,token错误
}
return "error";
}
- 测试启动两台服务8081、8082端口,先已8081访问loginWithJwt这个接口获得token,然后已8082端口访问infoWithJwt,并在header中加入之前的token,发现可以获得信息
改造JWT里的Controller层代码:
- 考虑在拦截器中统一处理token:
public class LoginIntercepter extends HandlerInterceptorAdapter {
public static final String JWT_KEY = "secret";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
if (StringUtils.isEmpty(token)) {
// 应该抛出自定义的异常
throw new RuntimeException("token为空");
}
Algorithm algorithm = Algorithm.HMAC256(JWT_KEY);
JWTVerifier verifier = JWT.require(algorithm).build();
try {
DecodedJWT verify = verifier.verify(token);
// 这里将jwt里的数据解码出来并转接到controller中
request.setAttribute("login_user", verify.getClaim("login_user").asString());
return true;
} catch (TokenExpiredException e) {
throw new RuntimeException("token过期");
} catch (JWTDecodeException e) {
throw new RuntimeException("解码失败,token错误");
}
}
}
- controller层
//使用@RequestAttribute来进行获取我们在拦截器中转接属性
@GetMapping("/address")
public String address(@RequestAttribute String login_user) {
return login_user;
}
六. 总结
JWT方案
优点:
- 天然去中心化模式,会话状态由令牌本身自解释,简单粗暴
- 服务器端不用存储token,节约资源
缺点:
- 一旦服务端下发了token便不受服务端控制
- 如果发生token泄露,服务器也只能人气蹂躏,只能等到它自己过期
问题: 如果用JWT实现可以管理下发的token呢?
可以考虑增加一张表,在表里存放当前颁发的token以及是否允许继续使用这个token,每次校验时再去表里判断即可,但是这样使用不就和传统的Session一样了么 emmm
因此:选择什么方案应该根据具体的场景
Redis+Token方案
优点:
- 可以有效的管理服务器颁发的token
缺点:
- 如果只有一台Redis服务器就容易出现单点故障
- 服务器需要存储token,耗费资源