目录
1、什么是localStorage和sessionStorage?
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于同一浏览器,以便在同一次会话的多次请求间共享数据。服务器需要用来识别请求是否来自同一个浏览器 服务器用来识别浏览器的过程,这个过程就是会话跟踪 服务器识别浏览器后就可以在同一个会话中多次请求之间来共享数据。
1、什么是localStorage和sessionStorage?
这两是web Storage 的其中两个,是HTML 5引入的一个重要的功能,在前端开发的过程中会经常用到,它可以在客户端本地存储数据,类似cookie,但其功能却比cookie强大的多。cookie的大小只有4Kb左右(浏览器不同,大小也不同),而web Storage的大小有5MB。
localStorage和sessionStorage都是保存在浏览器中,它们最主要的区别是:
生命周期不同:sessionStorage类似于session作用域为一次会话(从打开浏览器到关闭浏览器)
localStorage类似于cookie持久化存储在本地。
1、localStorage创建存储数据后,除非手动删除数据,不然都会一直存储在浏览器中
2、sessionStorage创建存储数据后,页面刷新、同源打开其他页面,代码跳转能够访问到当前的
3、seesionStorage。如果关闭浏览器,sessionStorage就会消失。实际上我们可以理解成一种会话机制。
提供的方法有以下几种:
- setItem(key,value):设置storage中的键值对,存储值
- getItem(key):获取storage中的键值对
- removeItem(key):删除storage中的键值对
- clear():清楚storage中的内容
可以将对象以JSON字符串的格式存储在Storage中,可以使用JSON.stringify();将对象转化为JSON字符串。需要使用storage中的值的时候先使用getItem(key)来获取storage中的JSON字符串,然使用JSON.parse();将JSON字符串解析为json对象,这样就可以获取对象中的值了。
2、使用localStorage实现会话跟踪技术
登录流程:
1、首次登录时,后端服务器判断用户账号密码正确之后,根据用户id、用户名、定义好的秘钥、过期时间生成 token ,返回给前端;
2、前端拿到后端返回的 token ,存储在浏览器的localStorage 里;
3、前端每次路由跳转,判断 localStorage 有无 token ,没有则跳转到登录页,有则请求获取用户信息,改变登录状态;
4、每次请求接口,在 Axios 请求头里携带 token;
5、后端接口判断请求头有无 token,没有或者 token 过期,返回标记信息;前端得到标记信息,重定向到登录页面;
1、前端发起登录请求
login(){
let para={"username":this.name,"password":this.password};
this.$http.post("/user/login",para).then((res)=> {
let {success,msg,result}=res.data;
if(!success){//登录失败
this.errorMsg=msg;
}else {//登录成功
this.errorMsg="";//清空errormsg
//获取登录用户
let loginUser=result.loginUser;
let token=result.token;
//保存到浏览器
localStorage.setItem("token",token)
localStorage.setItem("loginUser",JSON.stringify(loginUser))
location.href="index.html";
console.debug(loginUser,token);
}
});
},
登录成功后,将后端返回的token和loginUser存储在localStorage中。
2、后端接受并处理请求
后端生成一个token字符串作为key值,将登录的user转为json字符串作为value值。存储在redis服务器中,有效期30分钟,然后将这两个值返回给前端。前端接收到,使用localStorage保存到浏览器。
controller层:
/**
* 通过邮箱或者手机号登陆
* @return
*/
@PostMapping("/login")
public AjaxResult login(@RequestBody User user){
try {
//通过用户名和密码查询并进行校验
AjaxResult ajaresult = userService.login(user);
//校验通过
if(ajaresult.getSuccess()) {
//通过用户名查找user
User loginuser = userService.loadByUsername(user.getUsername());
//随机产生redis的key值
String token = UUID.randomUUID().toString();
//将loginuser转化为json字符串 存入redis 有效期30分钟
RedisUtils.INSTANCE.set(token, JSONObject.toJSONString(loginuser),30*60);
Map<String,Object> map=new HashMap<>();
//把token存储起来
map.put("token",token);
//把对象存储起来
map.put("loginUser",loginuser);
//把token和loginUser存储起来返回到前端
return new AjaxResult().setResult(map);
}
//为通过校验直接返回
return ajaresult;
} catch (Exception e) {
e.printStackTrace();
return new AjaxResult(false,e.getMessage());
}
}
Service层:
/**
* 通过邮箱或者手机号的的登录
* @param user
* @return
*/
@Override
public AjaxResult login(User user){
if (StringUtils.isBlank(user.getUsername())||StringUtils.isBlank(user.getPassword()))
return new AjaxResult(false,"请输入用户名和密码!!");
//通过用户名查询用户 查询到之后获取用户盐值
User byusername = userMapper.loadByUsername(user.getUsername());
if(byusername==null)
return new AjaxResult(false,"用户名错误!!");
//获取通过md5盐值加密的密码
String s = MD5Utils.encrypByMd5(user.getPassword()+byusername.getSalt());
user.setPassword(s);
User loginu = userMapper.login(user);
if(loginu==null)
return new AjaxResult(false,"密码错误!!");
if(loginu.getState()==UserConstant.DISABLED)//用户未激活
return new AjaxResult(false,"用户未激活,请激活用户后登录!!");
return new AjaxResult();
}
3、配置前端前置拦截器
接下来,前端每次发送请求都要携带这个token值,需要判断通过是否存在token判断用户是否登录,之前使用cookie发起请求时是会自动携带所有cookie信息。
在前端页面加一个前置拦截器,就是发送任何请求前先执行这个前端拦截器函数,这里面主要从localStorage里获取token值,然后将其添加到请求头信息里 .每次发起请求 请求头中都会包含U-TOKEN 这个信息。
在common.js中配置:
//前置拦截器
//给axios请求添加一个前置拦截器 每次发起请求都先执行下面这个函数
//1 使用axios前置拦截器,让所有的请求都携带uToken
axios.interceptors.request.use(config=>{
//携带token
let uToken = localStorage.getItem("token");
if(uToken){//如果token有值 我就在请求头信息里添加一个教U-TOKEN的值
config.headers['U-TOKEN']=uToken;
}
return config;
},error => {
Promise.reject(error);
})
4、后端配置拦截器
自定义拦截器LoginInterceptor:
package com.rk.pethome.user.interceptor;
import com.rk.pethome.basic.util.RedisUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 登录拦截器
*/
@Component
public class LoginInterceptor implements HandlerInterceptor{
//前置拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
//1 放行 /login /logout /dfs /user/phoneRegister
String uri = request.getRequestURI();
System.out.println(uri);
/* if (CollectUtils.contain(uri, Arrays
.asList("/user/login","/logout","/dfs","/user/phoneRegister")))
return true;*/
// 2 拦截处理
//2.1 判断请求头里面是否携带U-TOKEN A-TOKEN,如果没有携带返回没有用户的错误
String uToken = request.getHeader("U-TOKEN");
//用户没有登录
if(StringUtils.isBlank(uToken)){
writeNoUserError(response,"noUser");
return false;
}
//2.2 判断redis中是否可以获取U-TOKEN A-TOKEN,如果获取不到没有用户的错误
String user = RedisUtils.INSTANCE.get(uToken);
//登录的用户已经过期了
if( StringUtils.isBlank(user)){
writeNoUserError(response,"expireUser");
return false;
}
//如果没有过期,刷新一下过期时间
//2.2.1 刷新session过期-redis里面的一个值
RedisUtils.INSTANCE.set(uToken,user,30*60);
return true;
}
/**
* {"success":false,"message":"noUser"}
* response.getWriter().write("{'success':false,messge:noUser}");
* @param response
*/
private void writeNoUserError(HttpServletResponse response,String message) {
PrintWriter writer = null;
try {
response.setCharacterEncoding("utf-8"); //返回编码格式
response.setContentType("application/json; charset=utf-8"); // json方式放回
writer = response.getWriter();
writer.write("{\"success\":false,\"message\":"+"\""+message+"\""+"}");
} catch (IOException e) {
e.printStackTrace();
}finally {
if (writer != null) {
writer.close();
}
}
}
}
添加配置ItsourceWebMvcConfigurer:
/user/*路径下的请求放行
@Configuration
public class ItsourceWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
/*
/* :拦截所有的请求,请求只有一级
/** :拦截所有的请求 可以拦截多级请求
*/
//添加拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor).addPathPatterns("/**")
.excludePathPatterns("/user/*");
}
}
此时请求到了(/user/login请求不会被拦截)后端拦截器,拦截器首先从请求头获取token值,如果为空,说明未登录,拦截请求并且返回{success:false,message:noUser}。
如果获取到token值,说明用户登录过,因为存储在前端localStorage的值是短时间内不会过期,除非手动清空缓存。但是保存在redis的该用户只在30分钟内有效。也就是常见的登录到一个页面长时间过后需要再次登录。此时根据前端传过来的token去redis服务器中获取值,如果没有获取到值,说明登录已过期。就拦截请求并且返回{success:false,message:expireUser}。如果获取到了值,就将token和获取到的value值(上次登录的用户)再次存储到redis服务器,刷新过期时间。
5、配置前端后置拦截器
在conmmon.js中:
//后置拦截器
//2 使用axios后置拦截器,处理没有登录请求
axios.interceptors.response.use(result=>{
let data = result.data;
console.debug(data)
console.log(data.success);
console.log(data.message);
//用户没有登录
if(!data.success && data.message==="noUser") {
alert("用户未登录!");
location.href = "/login.html";
}
//后台redis用户过期了 清空localStorage里面的值
if(!data.success&&data.message==="expireUser"){
alert("登录已过期!");
localStorage.removeItem("token");
localStorage.removeItem("loginUser");
location.href = "/login.html";
return;
}
return result;
},error => {
Promise.reject(error);
})
请求返回后,前端界面再配置一个后置拦截器,所有返回值都会先经过这个后置拦截器。拦截器判断success和message 如果是false和noUser就说明用户未登录,就跳转到登录界面。接下来再判断message的值是否是expireUser,如果是则 登录的用户已过期,就清空localStorage里面存储的数据,然后跳转到登录界面。如果用户已登录并且未过期,则将后端返回的result返回给前端调用接口的函数。