Cookie、Session、Token详解和跨域问题的解决

1,003 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的4天,点击查看活动详情

Cookie

为了解决 HTTP 无状态的问题,当浏览器访问某个web服务器时,web服务器会将一个键值对通过HTT响应头发送到浏览器,浏览器将该键值对缓存到本地中,用来保存客户端的信息

优点:

  1. 解决了 HTTP 无状态的问题;
  2. 在客户端存储部分用户信息,每次访问都可以携带这部分的用户信息发送到服务器;

缺点:

  1. 解决HTTP无状态的问题,将额外的信息存储在客户端,但大小只有4kb,存储的大小有限
  2. 在如今前后端分离开发的模式下,cookie不支持跨域是最大的缺点;

Session

session相当于服务端的cookie,可以将用户的信息存在服务端中,Tomcat中session的实现其实是基于HashMap的实现。

优点:

  1. 相对于cookie,将用户信息存储在服务端,会相对安全;
  2. 减少了网络传输的消耗,直接从服务端内存中获取信息,速度更快;

缺点:

  1. 分布式 session 问题💛

在如今分布式的互联网架构中,一台服务端已经支持不了大多数业务的发展,动不动就要多台台服务器。假设现在拥有三台服务器,部署相同的代码,某一次访问到了一台服务器,在该台服务器的session中存储了用户信息,而用户的第二次访问却访问到了另一台服务器,但这台服务器上却没有在session中记录用户信息,这时就要显示用户未登录吗?这就是分布式session的问题

解决方案:

使用独立一台服务器,用来记录session的信息,每次都去该服务器获取session即可

  1. 内存容量问题💛

如果使用了独立一台服务器记录session 信息,那么在用户量过多的情况下,一台服务器明显是撑不住的,这时就要加机器。加机器就意味着加钱,同时服务器过多也会给后来的系统的维护带来困难。

Token

token是一串在服务端生成的字符串,通过一定的加密算法,将用户的信息保存在该字符串中,客户端每次发送请求都会在请求头中带上该字符串进行鉴权,并且可以从该字符串中获取到用户信息。

优点:

  1. 相对于cookie,存储在客户端的字符串,没有长度的限制,并且支持跨域
  2. 相对于session来说,它是存在于客户端的,减少服务器的存储压力,而且只要获得了该字符串,就可以向多台机器中进行用户信息的鉴权,不存在分布式session问题
  3. 请求流程

  1. token的使用(基于JWT的实现)
<dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.18.2</version>
</dependency>
package com.blog.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;

/**
 * token 工具类
 *
 * @author Ezreal
 */
@Slf4j
public class TokenUtils {
    // 一小时过期
    private static final long TIME = 1000 * 60 * 60;

    // 加密的密钥
    private static final String SECRET_KEY = "ezreal";


    /**
     * 生成token
     *
     * @param account
     * @return
     */
    public static String create(String account) {
        // 设置过期的时间
        Date date = new Date(System.currentTimeMillis() + TIME);
        // 算法
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        String token = JWT.create()
                .withClaim("account", account)
                .withExpiresAt(date)
                .sign(algorithm);
        log.info("生成token :" + token);
        return token;
    }

    /**
     * 验证token是否有效
     *
     * @param token
     * @return
     */
    public static boolean verify(String token) {
        // 算法
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT verify = verifier.verify(token);
            String string = verify.getClaim("account").toString();
            System.out.println(string);
            log.info("token 中的account为" + string);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

缺点:

安全问题,一旦该字符串被其他人获取,就相当于获取了该账号进行登录,保护 token 的存储是非常重要的。

跨域与同源

同源

协议 域名 端口三者相同,即称为同源

域名的组成:协议 + 子域名 + 主域名 + 端口号 + 请求资源的地址

www.baidu.com:80/index.html

协议:http://

子域名:www

主域名:baidu.com

端口号:80

请求资源的地址:index.html

同源的限制

  1. Ajax请求
  2. cookie
  3. 但是<link/> <script/> <img/>允许跨域请求资源;

跨域

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。

跨域解决方案

  1. 配置过滤器,添加响应头信息
package com.blog.filter;

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 跨域配置
 */
@Order(Ordered.HIGHEST_PRECEDENCE)
//@WebFilter(filterName = "crossFilter", urlPatterns = "/*")
@Component
public class CrossFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest servletRequest = (HttpServletRequest) request;
        HttpServletResponse servletResponse = (HttpServletResponse) response;
        servletResponse.setHeader("Access-Control-Allow-Origin", servletRequest.getHeader("Origin"));
        servletResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE,PATCH");
        servletResponse.setHeader("Access-Control-Allow-Headers", "DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization,SessionToken,Cookie");
        servletResponse.setHeader("Access-Control-Max-Age", "3600");
        servletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        servletResponse.setHeader("Access-Control-Expose-Headers", "*");

        //判断是否为可选择性批量操作的请求,设置状态码返回
        if ("OPTIONS".equals(servletRequest.getMethod())) {
            servletResponse.setStatus(HttpStatus.ACCEPTED.value());
            return;
        }
        filterChain.doFilter(request, response);
    }

    @Override
    public void destroy() {

    }
}
  1. 网关支持
# 跨域配置
location ^~ /api/ {
	proxy_pass http://127.0.0.1:8080/api/;
	add_header 'Access-Control-Allow-Origin' $http_origin;
	add_header 'Access-Control-Allow-Credentials' 'true';
	add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
	add_header Access-Control-Allow-Headers '*';
	if ($request_method = 'OPTIONS') {
		add_header 'Access-Control-Allow-Credentials' 'true';
		add_header 'Access-Control-Allow-Origin' $http_origin;
		add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
		add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
		add_header 'Access-Control-Max-Age' 1728000;
		add_header 'Content-Type' 'text/plain; charset=utf-8';
		add_header 'Content-Length' 0;
		return 204;
	}
}
  1. 配置@CrossOrigin 注解
@Controller
@CrossOrigin
public class Controller {
 
}
  1. 添加Web全局请求拦截器
@Configuration
public class WebMvcConfg implements WebMvcConfigurer {
	
	@Override
	public void addCorsMappings(CorsRegistry registry) {
		//设置允许跨域的路径
		registry.addMapping("/**")
			//设置允许跨域请求的域名
			//当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
			.allowedOrigins("http://localhost:9527", "http://127.0.0.1:9527", "http://127.0.0.1:8082", "http://127.0.0.1:8083")
			//是否允许证书 不再默认开启
			.allowCredentials(true)
			//设置允许的方法
			.allowedMethods("*")
			//跨域允许时间
			.maxAge(3600);
	}
}
  1. 返回新的CosFilter
@Configuration
public class CorsConfig {
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setMaxAge(3600L);
        corsConfiguration.setAllowCredentials(true);
        return corsConfiguration;
    }
 
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}