Cookie,Session,Token,Jwt的使用
都可以用来获取用户登录状态。
一.Cookie
cookie是在服务端创建的但是保存在客户端浏览器上直到本次会话结束过期(可以设置过期时间),每一次发起请求都会带着浏览器的所有cookie访问服务端。
测试使用代码:
@Controller
@RequestMapping("cookie")
public class CookieDemo {
//测试cookie使用
@RequestMapping("c")
@ResponseBody
public void test(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding("GBK"); //设置请求编码格式
response.setCharacterEncoding("GBK"); //设置响应编码格式
response.setContentType("text/html;charset=utf-8"); //设置响应格式(不然要返回一个文档什么的显示不了)
PrintWriter out = response.getWriter();
Cookie[] cookies = request.getCookies(); //获取客户端请求中的cookie
if(cookies!=null){ //判断是否有cookie
out.write("你上一次访问时间是:");
for (int i = 0; i < cookies.length; i++) { //遍历所有cookie
Cookie cookie = cookies[i];
if (cookie.getName().equals("lastLoginTime3")){ //获取cookie名字
long lastLoginTime = Long.parseLong(cookie.getValue()); //将cookie的value值转成长整形
Date date = new Date(lastLoginTime); //将长整形转化为日期格式
out.write(date.toLocaleString());
}
}
}else {
out.write("这是您第一次访问本站");
}
Cookie cookie = new Cookie("lastLoginTime3", System.currentTimeMillis() + "");
//cookie.setMaxAge(60*60*24); //设置cookie存活时间为一天(如果不设置就是关闭浏览器自动删除cookie,设为0也能理解为删除)
response.addCookie(cookie); //给客户端响应一个cookie
}
}
二.Session
session是在服务端创建的,session对象创建完后会保存在服务器上直到本此会话结束,并且将session对象的ID以cookie的形式返回给浏览器,这样浏览器每次请求都会带着session的ID过去,服务端会先查看请求当中是否有sessionId,如果没有sessionId,则创建一个session对象;如果有sessionId,则依据该sessionId去查找对应的session对象,如果找到了,则返回该session对象,如果没有找到,则创建一个新的session对象。(不同的浏览器请求发过来都会先创建一个session对象)
测试使用代码:
@Controller
@RequestMapping("session")
public class SessionDemo {
//测试session使用
@RequestMapping("c")
@ResponseBody
public void test(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.setCharacterEncoding("GBK"); //设置请求编码格式
response.setCharacterEncoding("GBK"); //设置响应编码格式
response.setContentType("text/html;charset=utf-8"); //设置响应格式(不然要返回一个文档什么的显示不了)
HttpSession session = request.getSession(); //先查看请求当中是否有sessionId,如果没有sessionId,则创建一个session对象;如果有sessionId,则依据该sessionId去查找对应的session对象,如果找到了,则返回该session对象,如果没有找到,则创建一个新的session对象。(相当不同的浏览器请求发过来都会先创建一个session对象)
// Session创建的时候微了什么事情; Cookie cookie = new Cookie( "JSESSIONID",sessionId);response.addCookie(cookie);
long o = System.currentTimeMillis();
session.setAttribute("date", o); //设置这个session的key和value
String sessionId = session.getId(); //获取session的id
if (session.isNew()){ //判断session是不是新创建的
response.getWriter().write("session创建成功,ID:"+sessionId);
}else {
response.getWriter().write("session已经在服务器中存在,ID:"+sessionId);
}
//session.invalidate(); 注销session相当于关闭浏览器的操作
}
}
总结:session只在一次会话中有效(也可以设置session过期时间),当浏览器关闭之后重新访问,服务器就会创建新的session对象保存到内存中,当用户访问量很大的时候,考虑服务器性能的方面使用cookie比较合适(因为cookie是保存在客户端的),但是Session有一个致命的缺点就是服务器集群无法共享,通过负载均衡后请求可能打到不同的服务器上,所以需要配合Redis来使用。
三.Token
会话机制(Session)是用于识别用户身份的方法之一,但是由于需要服务器保存所有人的session id,如果访问服务器多了,就得有成千上万或者几十万个。这对服务器是一个巨大的开销,严重的限制了服务器的拓展能力。因此诞生了token(令牌)。
Token认证流程:
-
用户输入用户名和密码,发送给服务器。
-
服务器验证用户名和密码,正确的话就返回一个签名过的token并存入redis数据库(token 可以认为就是个长长的字符串,也可以用uuid作为token),浏览器客户端拿到这个token。
-
后续每次请求中,浏览器会把token作为http header发送给服务器,服务器获取请求头中的token并到redis数据库中查询token是否存在。
-
如果存在就返回客户端需要的数据。 特点: 这种方式的特点就是客户端的token中自己保留有大量信息,服务器没有存储这些信息。
-
更改过滤器信息,获取请求头中的token值,跟redis里面的值进行对比,相同的话放行都会更新redis中token过期时间。
-
前端保存token值(存到localStorage中),并让请求头携带token值
ajax中设置属性–> beforeSend: function (XMLHttpRequest) {
XMLHttpRequest.setRequestHeader(“token”, “token的value”);
}
7.注销功能(退出登录):删除localStorage(前端)和redis(后端)中的token值。
(登录)示例代码:
//使用token前先把redis配置好并启动
@Controller
@RequestMapping("token")
public class TokenDemo {
@Autowired
@Qualifier("redisTemplate") //使用自己的redisTemplate
private RedisTemplate redisTemplate;
@Autowired
private RedisUtil redisUtil; //使用封装好的redis工具类
//测试Token使用(需要用到Redis)
@RequestMapping("c")
@ResponseBody
public String test(@RequestParam String userName,@RequestParam String passWord){
String MysqlUserName = "cjy"; //模拟数据库账号
String MysqlPassWord = "123456"; //模拟数据库密码
if (userName.equals(MysqlUserName)&&passWord.equals(MysqlPassWord)){ //登录成功
String token = UUID.randomUUID()+""; //生成令牌
redisUtil.set(token,userName,60*30); //存入redis中,key为token,value为用户名(也可以是一个对象),过期时间设为30分钟
return token;
}
return "登录失败!";
}
}
使用Token的缺点:每次都需要根据token去后端redis中查询真实内容,对服务器端的压力就会很大。
四.JWT(Json Web Token)
Jwt是由以下三部分组成的一个字符串:(Header和Payload的数据是通过Base64编码形成的,Verify signature是通过MD5进行加密的)
- Header(头部):存放加密算法的类型
例:
{
Typ=“jwt” --------> 类型为jwt
Alg=“HS256” --------> 加密算法为hs256
} - Payload (有效载荷–>装载的数据):就是jwt存放的数据内容,就是token存在在redis数据库中的value值(不建议存一些敏感数据比如passWord,phoneNumber等等,因为数据是直接存放在客户端的,不安全)
例:
{
“userName”:“cjy”
“birthday”:“1999-11-20”
“过期时间”:“2023年4月13日星期四 下午3:25:17”
}
常用属性名如下:
iss: jwt签发者
sub: jwt所面向的用户
aud:接收jwt的一方
exp; jwt的过期时间,这个过期时间必须要大于签发时间
nbf:定义在什么时间之前,该jwt都是不可用的
iat: jwt的签发时间
jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- Verify signature(验证签名):防止别人篡改Payload中的数据
这部分数据是是将payload中的数据拼接上签名(加盐)后通过MD5加密后返回的,通过MD5加密后的数据是不可逆的,所以返回给前端后如果数据被改动了传给后端的这个Jwt就会解析不成功。
Jwt认证流程
-
用户输入用户名和密码,发送给服务器。
-
校验用户名和密码是否正确,如果正确通过Jwt加密算法并把用户信息放入Jwt中并返回给客户端(浏览器)存放起来(可以存在cookie或者localStorage中)。
-
后续每次请求中,浏览器会把Jwt作为http header发送给服务器,服务器直接将Jwt解析。
-
如果成功解析则代表此令牌有效,否则无效(如果Jwt被篡改,会解析失败),解析成功之后可直接使用Jwt中存储的信息
根据原理手写jwt(了解即可)
@Controller
@RequestMapping("jwt")
public class JwtDemo {
//手写Jwt案例
@Test
public void Jwt() throws UnsupportedEncodingException {
//header
JSONObject heafer = new JSONObject();
heafer.put("Alg","HS256");
//payload
JSONObject payload = new JSONObject();
payload.put("userName","cjy");
payload.put("userAge","23");
payload.put("userId","1564");
//payload.put("sjs", System.currentTimeMillis()); //设置随机数让每次生成的payload不一样
//对数据进行base64编码
String jwtHeader = Base64.getEncoder().encodeToString(heafer.toJSONString().getBytes());
String jwtPayLoad = Base64.getEncoder().encodeToString(payload.toJSONString().getBytes());
//设置盐值(一般情况下都是设置随机的盐值)
String signingKey = "cjy";
//签名 后面加盐(秘钥) (是存放在服务器端) (不加盐的话有可能会被暴力破解)
String sign = MD5Utils.md5(payload.toJSONString() + signingKey);
String jwt = jwtHeader + "."+ jwtPayLoad + "." + sign; //拼接生成jwt
System.out.println(jwt);
//解密(判断前端中jwt的payload是否被更改)
String jwtPayloadStr = jwt.split("\\.")[1]; //获取jwt中的payload的值
String jwtSignStr = jwt.split("\\.")[2]; //获取jwt中签名的值
//payload进行base64解码
String deJwtPayloadStr = new String(Base64.getDecoder().decode(jwtPayloadStr), "UTF-8");
System.out.println(MD5Utils.md5(deJwtPayloadStr + signingKey).equals(jwtSignStr));
}
}
真实使用步骤
- 引入pom依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
- 验证登录成功后创建jwt
@Controller
@RequestMapping("jwt")
public class JwtUseDemo {
private long time = 1000 * 60 * 60 * 24; //定义jwt存活时间为一天
private String signature = "admin"; //定义签名信息(盐值)
//Jwt使用案例
@Test
public void createJwt(){
JwtBuilder jwtbuilder = Jwts.builder(); //创建一个jwt
String jwtToken = jwtbuilder
//header
.setHeaderParam("typ","JWT")
.setHeaderParam("alg","HS256")
//payload
.claim("userName","cjy")
.claim("role","admin")
.setSubject("admin-test")
.setExpiration(new Date(System.currentTimeMillis()+time))
.setId(UUID.randomUUID().toString())
//signature(签名)
.signWith(SignatureAlgorithm.HS256,signature)
//凭借字符串
.compact();
System.out.println("生成的jwt值为:"+ jwtToken);
}
}
- 解密jwt并获取payload里的值
@Controller
@RequestMapping("dejwt")
public class JwtUseDemo {
private String signature = "admin"; //定义签名信息(盐值)
//jwt解密
@Test
public void DecodeJwt(){
String jwtStr = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImNqeSIsInJvbGUiOiJhZG1pbiIsInN1YiI6ImFkbWluLXRlc3QiLCJleHAiOjE2NDk5OTQ4MjIsImp0aSI6IjE4YzdkZDI0LTAzMTUtNDI1Yi04NGMxLTQzNzg5MTk2NGExMyJ9.Ku1xIM6SG7utEFxvQF_eURSY1-tkG7om_zT4DDomyN0";
JwtParser jwtParser = Jwts.parser(); //通过这个对象进行解密
//传入签名(盐值)对jwt进行解密并返回payload集合
Jws<Claims> claimsJws = jwtParser.setSigningKey(signature).parseClaimsJws(jwtStr);
//取得集合里的数据
Claims claims = claimsJws.getBody();
System.out.println(claims.get("userName"));
System.out.println(claims.get("role"));
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getExpiration());
}
}
Jwt本身就是Token的优化版本,认证流程几乎相同,只是在生成token时进行加密,解析成功后大部分情况可直接拿Jwt中的信息进行使用
Jwt与 token最大的区别:
token依赖于Redis查询数据信息,token存放 value数据比较安全的(因为数据存放在redis中其他的服务器连接不到而且key是一个不重复的token值很难获取其值)。
而Jwt不需要依赖于服务器端,是将数据信息直接存放在客户端(浏览器)。
Jwt优点:
- 无需在服务器存放用户的数据,减轻服务器端压力
- 轻量级、json风格比较简单,跨语言
Jwt缺点:
- Token一旦生成后期无法修改,也无法更新token有效期
- 无法销毁一个jwt(所以建议将jwt存活时间设置短一点)