在自己写单独项目时,遇到这样的问题,登录网站后,我不小心触碰到浏览器的x按钮,好了,重新打开还得输入账号密码验证码,好繁琐


对于不依赖任何插件库的情况下这是第一种解决方案:

前后端分离,VUE需要做的事情

在main.js中增加以下配置:
import axios from 'axios';
axios.defaults.withCredentials=true;
或者请求时:设置 withCredentials: true
axios({
  url: url,
  data: data,
  headers: {
    'Content-Type': 'multipart/form-data'
  },
  method: 'POST',
  withCredentials: true
}).then(response => {
  resolve(response)
}

java中需要做的

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //解决后续中文乱码
    response.setContentType("text/html;charset=utf-8");
    response.setCharacterEncoding("utf-8");
    //添加跨域CORS
    response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
    response.setHeader("Access-Control-Allow-Headers", getHeadersInfo(request));
    response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH");
    // 是否允许浏览器携带用户身份信息(cookie) 必须加
    response.setHeader("Access-Control-Allow-Credentials","true");
    return true;
}

//获取原有的头部原路返回,不然还会存在token等字段跨域失败
private String getHeadersInfo(HttpServletRequest request) {
    String headers="";
    Enumeration headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
        headers+=headerNames.nextElement()+",";
    }
    if(!headers.contains("content-type")){
        headers+="content-type,";
    }
    if(!headers.contains("token")){
        headers+="token,";
    }
    return headers;
}

需要注意的是,上述代码只会保持在同一个session,关闭浏览器重新打开保持登陆需要做下面的操作(此步骤适合前后端一体的程序)

在java的登陆成功方法中加入一个cookie丢到客户端,让客户端记住JSESSIONID,和java的session保持一致的存活时间

//new一个cookie,cookie的名字是JSESSIONID跟带id的cookie一样
Cookie cookie = new Cookie("JSESSIONID", HttpServletHelper.getSession().getId());
//设置cookie应用范围。getContextPath是获取当前项目的名字。
cookie.setPath(HttpServletHelper.getRequest().getContextPath());
//设置有效时间
cookie.setMaxAge(HttpServletHelper.getSession().getMaxInactiveInterval());
//用这个cookie把带id的cookie覆盖掉
response.addCookie(cookie);

最最需要注意的是:由于安全考虑,新版的浏览器加入了一个字段(Cookie的SameSite),仅仅使用上述还是无法解决,仍然存在跨域

该字段在spring2.1x中才引入。所以你要升级版本,而且就算你升级了版本,该参数SameSite的值设置为None,也必须要求你为https才能生效。

SameSite值有三个,可以自己了解一下

对于不想升级的用户,并且确保站点是https可以尝试如下代码,注意里面的位置不要改变

response.setHeader("Set-Cookie", "JSESSIONID=xxx;SameSite=None;Secure");

springboot2.1x代码

@Bean
public CookieSerializer httpSessionIdResolver() {
    DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
    cookieSerializer.setUseHttpOnlyCookie(false);
    cookieSerializer.setSameSite("None");
    cookieSerializer.setCookiePath("/");
    cookieSerializer.setUseSecureCookie(true);
    return cookieSerializer;
}

pom依赖

<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
	<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-core</artifactId>
	<version>2.1.4.RELEASE</version>
</dependency>


对于安全考虑,我觉得一就算了吧,太麻烦了。说不准哪天https也不支持了,反正都是谷歌来定义了(毕竟用户群体太多了)


说一下第二种,不依靠session,使用redis替换session,现在各个项目我觉得使用redis应该是家常便饭了吧

先说java部分,登陆成功后生成一个token放入redis

Map<String,Object> userMap=new HashMap<>(2);
userMap.put("user",user);
String token= StringUtils.getUuid();
userMap.put("token",token);
//一定要注意有效期,一定要放,仍然是和session会话时长一致 
redisHelper.hset("loginUser",token,user,HttpServletHelper.getSession().getMaxInactiveInterval());

参考第一步,跨域的代码仍然需要;下面是登录拦截代码

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
   //解决OPTIONS请求没有token导致跨域失败问题
   if(StringUtils.isNullOrEmpty(request.getContentType())){
      return true;
   }
   //失效等信息会在HttpServletHelper和全局错误拦截器中处理
   User user = HttpServletHelper.getUser(redisHelper);
   //这么做是刷新有效期
   HttpServletHelper.setUser(redisHelper,HttpServletHelper.getToken(),user);
   return true;
 }

重点来了,这个是对登录用户封装的方法

public class HttpServletHelper {

    public static HttpServletRequest getRequest() {
        return getRequestAttributes().getRequest();
    }

    public static HttpServletResponse getResponse() {
        return getRequestAttributes().getResponse();
    }

    public static HttpSession getSession() {
        return getRequest().getSession();
    }

    public static ServletRequestAttributes getRequestAttributes() {
        return (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    }

    public static ServletContext getServletContext() {
        return ContextLoader.getCurrentWebApplicationContext().getServletContext();
    }

    public static String getToken(){
        String token=getRequest().getHeader("token");
        if(StringUtils.isNullOrEmpty(token)){
            throw new BusinessException("400","token丢失!");
        }
        return token;
    }
    public static User getUser(RedisHelper redisHelper){
        User user=null;
        try{
            user=(User)redisHelper.hget("loginUser",getToken());
        }catch (BusinessException e){
        }
        if(user==null){
            throw new BusinessException("203","用户未登录");
        }
        return user;
    }
    public static User setUser(RedisHelper redisHelper,String token,User user){
        redisHelper.hset("loginUser",token,user, getSession().getMaxInactiveInterval());
        return user;
    }
}

有人就会问我了BusinessException这个找不到啊,我只能告诉你这是自定义异常类,到时候可以全局拦截这个异常,返回对应的提示

BusinessException这个类留给自己思考吧

说一下全局拦截异常处理

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    //这里就是自定义错误拦截的方法
    @ExceptionHandler(BusinessException.class)
    public ApiResponse BusinessExceptionHandler(BusinessException e){
        //拦截到自定义错误后,获取错误代码,还有错误信息,友好的返回前端
        return ApiResponse.status(Integer.parseInt(e.getCode()),e.getMessage());
    }
}


VUE部分只需要将token字段放入header带到后台即可

是不是So Easy