我们知道,起初的互联网是无状态的,人们只是使用互联网进行信息的查询,互联网也无需记住访问人的相关信息。但随着处理有状态的应用场景(例如登录吗,购物车等)的出现,人们需要互联网记住用户。
首先就出现了cookie,用户每次请求都携带cookie数据进行访问,客户端将cookie明文存储在HTTP响应中,每次客户端向服务器发送请求时,都会自动携带存储在本地的 Cookie,以便服务器识别和验证用户身份。
使用方法:
正常第一次登录来说,用户输入账号与密码,我们先验证账户是否存在,再验证密码是否存在,若是都存在并对上了,则设置cookie,因为cookie明文存储,所以使用最好进行加密(下面的例子并没有进行加密)。
设置cookie:
首先new一个cookie
再设置生命周期
再用HttpServletResponse.addCookie(cookie)进行添加Cookie
获取cookie:
在HttpServletRequest中 request.getCookies()可以获取cookies们遍历它们,判断key中的数据是否存在我们设定的数据,若存在则查询数据库。
注销cookie:
用户登出的话,获取cookie,再设置0秒生命周期,再写回浏览器,就算注销了cookie
在前后端分离项目中,cookie会出现跨域问题,导致前端只能接收cookie,并不能保存cookie(也就是在浏览器set-cookie中能看到值,但浏览器里并没有保存)
需要在前端请求接口添加 withCredentials: true表示接收cookie(在axios.create()里添加)
后端则需要加上跨域配置器(仅供参考,当时我试的时候加了前端的就好了,后端的根据不同情况来定)
我们刚刚说到,cookie用明文方式存储在浏览器会特别不安全,所以出现了session,对象信息存储在服务器中。
当用户首次访问网站时,服务器会为该用户创建一个唯一的会话标识符(通常称为会话ID),并将其以 cookie 的形式发送给客户端,通常称为“会话Cookie”。客户端浏览器将这个会话Cookie保存下来,并在随后的每个请求中将其发送回服务器。服务器通过会话ID来识别客户端,从而将客户端的请求关联到正确的会话数据。会话数据可以包含任意类型的信息,如用户的登录状态、用户设置、购物车内容等。
用法:
使用HttpSevletRequest request.getSession获取当前请求的Session对象(这个session是服务器端实际存储回话信息的地方。),之后setAttrubute来进行存放数据,getAttribute来进行获取数据
暂时没遇到特别的设置,一般设置了后端的CORS就可以了
session的存储,说的是会话数据本身(会话中的信息)是存储在服务器端的。
存储位置:用户信息(例如用户名、用户ID等)存储在服务器端的Session对象中。
运作过程:
随着前后端分离的技术流行,越来越需要一个东西来满足前后端交流,而不像cookie与session那样由后端主导。之后提出了JSON数据,进而提出后端把需要的上下文状态打包为一个token发给前端,前端在拿到后,在需要时(例如某个功能需要验证权限才能使用)携带token与请求发送给后端,后端对信息进行解包。
Token是一种用于身份认证和授权的令牌,它在Web应用程序和API中被广泛使用。通常,Token是一个字符串,由服务器生成并发送给客户端
为什么常存在redis数据库:token并不需要一直生效,而且设置了有效期能更好保证安全性,而存入redis不会长期占用主数据库的资源,也可以更好地控制其生命周期。(后面为了加强token的可靠性,一般用户信息会加密进token里,出现了JWT,也就脱离了redis的依赖)
在后续的请求中由客户端携带在请求头或请求参数中。Token的主要目的是验证用户身份,并授予用户访问特定资源的权限。
服务器将生成的Token发送给客户端,通常通过HTTP响应的头部或响应体的一部分。(放在客户端的通常都放在相应那边)
在使用Token进行身份验证时,客户端需要将Token放在HTTP头的“Authorization”字段中,或者放在请求的Cookie中,以便服务器识别和验证用户的身份。
后端:
@Bean
@SuppressWarnings("all")
public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
//Hash也采用这种序列方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
对于前端而言,需要前端进行存储并携带,这里使用pinia对token进行获取并存储
import {defineStore} from "pinia";
import {computed, ref} from "vue";
import {ElMessage} from "element-plus";
export const useTokenStore = defineStore('mytoken',()=>{
//把返回的token拿出来并转化为对象
const tokenJson = ref("")
// computed 相当于 getter(属性处理方法)
const token = computed(()=>{
try {
//要么在返回值获取到,要么从浏览器本地缓存中获取,要么为空
return JSON.parse(tokenJson.value || window.localStorage.getItem("TokenInfo") || "{}")
}catch (err)
{
ElMessage.error("json字符串格式不正确,转化失败")
//若格式错误,返回空
window.localStorage.setItem("TokenInfo","")
throw err
}
})
//function 相当于actions
function saveToken(data){
tokenJson.value = data
window.localStorage.setItem("TokenInfo",data);
}
//向外暴露
return {token, saveToken}
})
//在注册的vue页面中,就可以
import {useTokenStore} from "@/store/Mytoken";
const store = useTokenStore()
//进行保存
const login = ()=>{
api.Nlogin(loginForm).then(res=>{
//判断登录成功后.....
store.saveToken(res.data.data)
})
}
使用,首先了解前端请求拦截器,就是前端在发送请求之前,拦截进行处理的
这样我们每次使用时就可以先获取token再进行请求,避免每次写请求都需要写获取token
//这个是个人写的获取token的方法,它返回值其中一个就是token
import {useTokenStore} from "@/store/Mytoken";
//请求拦截器 request:指个人写的axios.create({})出来的请求
//config:代表请求的信息
request.interceptors.request.use((config)=>{
//首先判断请求里是否有headers,如果没有则创建(虽然好像显得有点多余)
if (!config.headers)
{
config.headers= {}
}
const store = useTokenStore() //获取token的方法
//把相应的token添加到请求头中
config.headers.Authorization = store.token.token //store.token会获取到token集合
//.token则获取到叫做token的token,一般我们根据不同的情况设置不同token代表权限
return config //一定要return
})
退出登录时需要清空token,就是把保存的值设空
useTokenStore().saveToken('')
后端也要删除redis里的数据
String authorization = request.getHeader("Authorization");
redisTemplate.delete(authorization);
而上面的方法都是基于,有一个refreshtoken,带着token用户的唯一标识符,并且存储时间比token长
因为长时间的token也无法避免用户在浏览时,token过期直接被踢去登录页,用户体验会很不好
而定期检查会浪费资源。
在请求提示token过期后,前端自动识别并重新获取token,再自动续上用户的请求。这样虽然多存储了refreshtoken,但对用户的体验是很好的,用户也无法察觉到。
所以使用方法二
先设置一个响应式拦截器
request.interceptors.response.use(
(response)=> response,
async (error) => {
if (error.response.status === 401)
{ //刷新token
const data = await refresh()
if (data.data.status ===200)
{
//保存新token
useTokenStore().saveToken(data.data.data)
//重新请求token,并且把结果返回
return request(error.config)
}
else {
//如果失败,则跳转到登录页
ElMessage.error("访问间隔时间过久,需要重新登录")
router.push("/")
return
}
}
return Promise.reject(error)
})
那如何只进行一次refresh呢
这里在请求那里做文章
就是设置一个值,如果请求正在进行中,则设为true,让其他晚来的被拦下
//刷新token,promise表示异步操作结果对象,
// 当我们发送异步请求时,返回的就是Promise对象,这里用来拓宽请求的操作
// (本来直接return回去,现在new一个接住它处理完我们事情后扔出去)
let promiseRT = Promise
let isRefreshing = false
export const refresh = ()=>{
if (isRefreshing)
{
return promiseRT
}
isRefreshing = true
promiseRT = request({
method:'POST',
url:'/user/refresh_token',
params: {
refreshtoken: useTokenStore().token?.refreshtoken
},
}).finally(()=>{
isRefreshing = false
})
return promiseRT
}
在后端,需要在token失效时返回特定错误码,这就需要设置请求状态码
response.setStatus(401);
并不是,因为token除了用户名,也有其他信息及签名,签名可以监测是否被修改。
Cookie是每次请求都会携带,而token是需要用到的时候,由服务器主动添加到请求头中
大小来说,cookie一般限制4kb(不同浏览器不同),而token不受浏览器限制
存储的数据而言,cookie只能存储字符串,而token可以存储任意形式的数据