有状态登录与无状态登录:原理与对比

在现代Web应用中,用户认证和授权是确保系统安全的重要环节。常见的用户认证方式包括有状态登录和无状态登录。本文将详细介绍这两种登录方式的原理、实现机制、优缺点,并进行对比分析。

1.有状态登录

有状态登录是指在用户完成身份验证后,服务器会为该用户创建一个会话(Session),并将用户的认证信息存储在服务器端。每次用户请求时,都会携带一个会话标识符(如Cookie中的Session ID),服务器通过这个标识符来识别用户,并从服务器端获取用户的认证信息,从而判断用户是否已经登录。

1.1 Cookie

Cookie 是一种客户端会话跟踪技术,它将数据存储在客户端浏览器中。通过使用 Cookie,我们可以在浏览器第一次发起请求时,在服务器端设置一个 Cookie。例如,当用户第一次请求登录接口时,登录接口执行完成后,我们可以在服务器端设置一个 Cookie,其中存储用户的相关数据信息,如用户名和用户ID。

1.1.1 设置和使用 Cookie 的流程

1. 首次请求登录接口

  • 用户通过浏览器发起登录请求,提交用户名和密码。
  • 服务器验证用户名和密码的正确性。
  • 验证成功后,服务器创建一个 Cookie,并将用户的认证信息(如用户名和用户ID)存储在 Cookie 中。
  • 服务器通过响应头 Set-Cookie 将 Cookie 发送给浏览器。

2. 浏览器接收并存储 Cookie

  • 浏览器接收到服务器的响应后,会自动将 Set-Cookie 头中的 Cookie 数据存储在浏览器本地。

3. 后续请求携带 Cookie

  • 在后续的每次请求中,浏览器会自动将存储在本地的 Cookie 数据通过请求头 Cookie 发送到服务器。

4. 服务器获取并验证 Cookie

  • 服务器接收到请求后,通过解析 Cookie 头中的数据,获取并验证用户的认证信息。
  • 如果 Cookie 存在且有效,服务器认为用户已登录,处理请求并返回结果。
  • 如果 Cookie 不存在或无效,服务器认为用户未登录,返回未授权的响应。

1.1.2 为什么这一切都是自动化进行的?

这一切自动化过程的原因在于 Cookie 是 HTTP 协议中定义的一种标准技术,各大浏览器厂商都遵循这一标准。HTTP 协议提供了两个关键的头部字段来支持 Cookie 的设置和传递:

  • 响应头 Set-Cookie:用于设置 Cookie 数据。
  • 请求头 Cookie:用于携带 Cookie 数据。

1.1.3 Cookie 的基本属性

属性

描述

Name

Cookie 的名称。

Value

Cookie 的值。

Domain

指定 Cookie 的有效域名。例如,example.com。

Path

指定 Cookie 的有效路径。例如,/ 表示整个域名下的所有路径。

Expires

指定 Cookie 的过期时间。格式为 Wdy, DD-Mon-YYYY HH:MM:SS GMT。

Max-Age

指定 Cookie 的最大生存时间(以秒为单位)。

Secure

指定 Cookie 只能通过 HTTPS 协议传输。

HttpOnly

指定 Cookie 不能通过 JavaScript 访问,增加安全性。

SameSite

控制 Cookie 在跨站请求中的发送行为,可以设置为 Strict、Lax 或 None。

 假设我们要设置一个名为 userSession 的 Cookie,有效期为 1 小时,仅限于 example.com 域名下的 /user 路径,且只能通过 HTTPS 传输,不能通过 JavaScript 访问,并且在跨站请求中不发送:

Set-Cookie: userSession=123456; Domain=example.com; Path=/user; Max-Age=3600; Secure; HttpOnly; SameSite=Lax

说明:

  • Name: userSession
  • Value: 123456
  • Domain: example.com
  • Path: /user
  • Max-Age: 3600 秒(1小时)
  • Secure: true,表示只能通过 HTTPS 传输
  • HttpOnly: true,表示不能通过 JavaScript 访问
  • SameSite: Lax,表示在跨站请求中不发送

1.1.4 具体实现示例

1.1.4.1 设置 Cookie
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class CookieController {
    @GetMapping("/setCookie")
    public String setCookie(HttpServletResponse response, String username) {
        // 创建Cookie
        Cookie userCookie = new Cookie("username", username);
        userCookie.setDomain("localhost");
        userCookie.setPath("/");
        userCookie.setMaxAge(3600); // 单位为秒,1小时
        userCookie.setSecure(false);
        userCookie.setHttpOnly(true);
        // 添加到响应头
        response.addCookie(userCookie);
        return "Cookie已设置";
    }
}

访问setCookie接口,设置Cookie, http://localhost:8080/setCookie?username=zhangsan,访问结果如下:

我们可以看到,设置的cookie,通过响应头Set-Cookie响应给浏览器,并且浏览器会将Cookie,存储在浏览器端。

 1.1.4.2 获取 Cookie
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class CookieController {
    @GetMapping("/getCookie")
    public String getCookie(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("username".equals(cookie.getName())) {
                    return cookie.getValue();
                }
            }
        }
        return "Cookie不存在";
    }
}

 访问getCookie接口 http://localhost:8080/getCookie,此时浏览器会自动的将Cookie携带到服务端,是通过请求头Cookie,携带的。

 1.1.5 Cookie 的优缺点

优点:

  • Cookie 的设置和读取都非常简单,只需要在HTTP响应头和请求头中操作即可。
  • Cookie 是HTTP协议的一部分,各大浏览器都支持这一标准,无需额外的库或框架。

缺点:

  • 每个Cookie的最大数据量为4KB,不适合存储大量数据。
  • 每个域名下的Cookie数量也有限制,通常是20个左右。
  • 如果Cookie没有设置HttpOnly属性,可以通过JavaScript读取,容易受到跨站脚本攻击(XSS)。
  • Cookie默认会随请求发送,容易受到跨站请求伪造(CSRF)攻击。
  • 如果Cookie没有设置Secure属性,可以通过HTTP协议传输,容易被中间人攻击截获。
  • 一些用户可能禁用Cookie,导致功能受限。
  • 服务器需要处理和验证Cookie,增加了服务器的负担。
  • Cookie默认只能在设置它的域名下使用,跨域访问需要特殊处理。

拓展:

1. 跨域介绍:

        跨域(Cross-Origin Resource Sharing,简称 CORS)是指从一个域名的网页去请求另一个域名的资源。在Web开发中,跨域问题通常出现在前后端分离的架构中,前端和后端运行在不同的域名或端口上。由于浏览器的安全策略(同源策略),跨域请求会受到限制。

        同源策略是浏览器的一种安全机制,用于限制一个源(origin)的文档或脚本如何与另一个源的资源进行交互。同源策略要求请求的协议、域名和端口必须完全相同。如果这三个部分有任何一个不同,就被认为是跨域请求。

        推荐文章:https://www.ruanyifeng.com/blog/2016/04/cors.html

2. 跨域问题与 Cookie:
       在现在的项目中,前后端分离已成为常见的架构模式。前后端通常会分开部署,前端和后端分别运行在不同的服务器上。假设我们的项目配置如下:

  • 前端:部署在服务器 192.168.150.200,端口 80。
  • 后端:部署在服务器 192.168.150.100,端口 8080。

       当我们打开浏览器直接访问前端工程时,URL 为 http://192.168.150.200/login.html。在该页面上,我们会发起请求到后端服务,后端服务的地址不再是 localhost,而是服务器的 IP 地址 192.168.150.100,假设访问的接口地址为 http://192.168.150.100:8080/login

       在这种情况下,就会出现跨域操作的问题。因为我们在 http://192.168.150.200/login.html 页面上访问了 http://192.168.150.100:8080/login 接口。由于这两个地址的域名和端口不同,浏览器会将其视为跨域请求。

3. Cookie解决跨域问题

为了使 Cookie 能够在跨域请求中正常工作,可以采取以下措施:

1)在后端服务器上启用 CORS,并设置 Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials 头。例如,在 Spring Boot 中,可以通过配置 CorsConfiguration 来实现:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class GlobalCorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        //1. 添加 CORS配置信息
        CorsConfiguration config = new CorsConfiguration();
        // 放行哪些原始域
        config.addAllowedOrigin("*");
        // 是否发送 Cookie
        config.setAllowCredentials(true);
        // 放行哪些请求方式
        config.addAllowedMethod("*");
        // 放行哪些原始请求头部信息
        config.addAllowedHeader("*");
        // 暴露哪些头部信息
        config.addExposedHeader("*");
        //2. 添加映射路径
        UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
        corsConfigurationSource.registerCorsConfiguration("/**",config);
        //3. 返回新的CorsFilter
        return new CorsFilter(corsConfigurationSource);
    }

}

2)设置 withCredentials

在前端请求中,设置 withCredentials 为 true,以允许浏览器在跨域请求中携带 Cookie。例如,在 Axios 中:

axios.post('http://192.168.150.100:8080/login', {
         username: 'user',
         password: 'pass'
     }, {
         withCredentials: true
     })
     .then(response => {
         console.log(response.data);
     })
     .catch(error => {
         console.error(error);
     });

通过以上配置,可以确保在跨域请求中,浏览器能够正确地发送和接收 Cookie,从而实现会话管理。

1.2 Session

Session 是一种在服务器端存储用户会话信息的技术,用于在多个请求之间保持用户的状态。与 Cookie 不同,Session 的数据存储在服务器端,客户端只保存一个标识符(通常是 Session ID),通过这个标识符来访问服务器上的会话数据。这种方式可以提高数据的安全性和可靠性。

1.2.1 会话跟踪流程

1. 首次请求

  • 用户请求:用户通过浏览器发起请求,例如登录请求。
  • 服务器验证:服务器验证用户的凭证(如用户名和密码)。
  • 创建 Session:验证成功后,服务器创建一个新的 Session,并生成一个唯一的 Session ID。
  • 响应 Cookie:服务器将 Session ID 通过 Set-Cookie 响应头发送给客户端。响应头中包含 Set-Cookie: JSESSIONID=123456。

2. 客户端存储 Session ID

  • 存储 Cookie:浏览器接收到响应后,会自动将 Set-Cookie 头中的 JSESSIONID 存储在浏览器本地。

3. 后续请求

  • 携带 Cookie:在后续的每次请求中,浏览器会自动将存储在本地的 JSESSIONID 通过 Cookie 请求头发送到服务器。
  • 查找 Session:服务器接收到请求后,通过解析 Cookie 头中的 JSESSIONID,查找并恢复对应的会话数据。

4. 会话结束

  • 会话超时:会话可以由服务器设置超时时间,超过这个时间没有活动,会话将自动失效。
  • 手动注销:用户也可以通过注销操作,手动结束会话。

1.2.2 具体实现示例

1.2.2.1 设置 Session
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@RestController
public class SessionController {

    @GetMapping("/setSession")
    public String setSession(HttpServletRequest request, String username) {
        // 获取Session
        HttpSession session = request.getSession(true); // true 表示如果不存在则创建新的Session
        log.info("HttpSession-setSession: {}", session.hashCode());
        session.setAttribute("username", username);
        session.setMaxInactiveInterval(3600); // 单位为秒,1小时
        return "Session 设置成功";
    }
}

访问setSession接口:http://localhost:8080/setSession

请求完成之后,在响应头中,就会看到有一个Set-Cookie的响应头,里面响应回来了一个Cookie,就是JSESSIONID,这个就是服务端会话对象 Session 的ID。

 1.2.2.2 读取 Session
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@RestController
public class SessionController {

    @GetMapping("/getSession")
    public String getSession(HttpServletRequest request) {
        // 获取Session
        HttpSession session = request.getSession(false); // false 表示如果不存在则返回null
        if (session != null) {
            log.info("HttpSession-getSession: {}", session.hashCode());
            String username = (String) session.getAttribute("username");
            return "Session 获取成功,用户名:" + username;
        } else {
            return "Session 不存在";
        }
    }
}

访问 getSession 接口:http://localhost:8080/getSession

         接下来,在后续的每次请求时,都会将Cookie的值,携带到服务端,那服务端呢,接收到Cookie之后,会自动的根据JSESSIONID的值,找到对应的会话对象Session。

        那经过这两步测试,在控制台中输出如下日志:

1.2.3 Session 的优缺点

优点:

  • 会话数据存储在服务器端,客户端只保存一个标识符(Session ID),减少了数据泄露的风险。
  • 服务器端可以存储更多的会话数据,不受客户端 Cookie 的大小限制(4KB)。
  • 可以设置会话的超时时间、会话锁定、会话迁移等,提供了更灵活的会话管理机制。

缺点:

  • 会话数据存储在服务器端,随着用户数量的增加,会占用更多的服务器资源。
  • 在分布式系统中,需要确保多个服务器之间共享会话数据,增加了系统的复杂性。
  • 如果 Session ID 被恶意用户获取,可能会导致会话劫持。
  • 需要合理设置会话的超时时间,过短可能导致用户体验不佳,过长则可能增加服务器负担。
  • 虽然 Session 本身存储在服务器端,但客户端仍然需要通过 Cookie 来传递 Session ID。如果用户禁用了 Cookie,Session 将无法正常工作。

拓展:

1. 为什么服务器集群环境无法直接使用Session?

在服务器集群环境中,使用传统的Session会话跟踪方式会遇到以下几个主要问题:

1. 会话状态不一致

        在集群环境中,用户的请求可能被负载均衡器分发到不同的服务器上。例如,用户第一次请求被分发到服务器A,生成了一个Session,并将JSESSIONID通过Cookie返回给浏览器。当用户发起第二次请求时,负载均衡器可能将请求分发到服务器B。此时,服务器B无法找到与JSESSIONID对应的Session,导致会话状态不一致。

2. 性能瓶颈

        为了保证Session的一致性,通常需要将Session数据存储在一个共享的位置(如数据库或分布式缓存)。这种做法虽然可以解决一致性问题,但同时也引入了额外的网络开销和潜在的性能瓶颈。

3. 扩展性问题

        随着集群规模的扩大,集中式存储Session的方法可能会成为系统扩展的障碍,因为存储层需要能够处理更多的读写请求,这可能导致存储层成为性能瓶颈。

2. 在分布式环境中如何使用Session

  • 使用分布式缓存: 将Session数据存储在分布式缓存中,如Redis。所有服务器节点都可以访问同一个缓存,从而确保会话状态的一致性。
  • 会话粘滞性:通过配置负载均衡器(如Nginx),使同一个客户端的请求总是被转发到同一台服务器,从而保持会话状态的一致性。

2. 无状态的登录

状态登录是一种在Web应用中实现用户认证的方法,其主要特点是服务器不保存任何与用户会话相关的状态信息。这种方式通常通过令牌(Token)来实现,最常见的是JSON Web Token (JWT)。

2.1 实现步骤

1. 用户登录:

  • 用户提交用户名和密码。
  • 服务器验证用户凭据,如果正确,则生成一个Token并返回给客户端。

2. 客户端存储Token:

  • 客户端将Token存储在本地(如浏览器的LocalStorage或Cookie中)。

3. 后续请求:

  • 客户端在每个请求的HTTP头部(通常是Authorization头)中附加Token。
  • 服务器接收到请求后,解析并验证Token,确认用户身份。

4. Token刷新:

  • 如果Token有有效期,客户端需要在Token过期前请求新的Token。
  • 服务器可以提供一个专门的接口用于刷新Token。

2.2 JWT令牌(JSON Web Token)

JSON Web Token (JWT) 是一种开放标准 (RFC 7519),用于在各方之间安全地传输信息作为JSON对象。JWT常用于身份验证和信息交换,特别适用于无状态的认证机制。JWT定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

说明:

  • 简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。
  • 自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。

简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。

官网:JSON Web Tokens - jwt.io

2.2.1 JWT 的结构

JWT 由三部分组成,每部分用点号(.)分隔:

1. Header(头部)

头部通常包含两部分:令牌类型(即JWT)和所使用的签名算法(如HMAC SHA256或RSA)。

{
  "alg": "HS256",
  "typ": "JWT"
}

2. Payload(载荷)

 载荷包含声明(claims),声明是关于实体(通常是用户)和其他数据的声明。声明分为三种类型:

  • 注册声明:预定义的声明,如iss(发行人)、exp(过期时间)、sub(主题)等。
  • 公共声明:可以自定义的声明,但为了避免冲突,建议在IANA JSON Web Token Registry中注册。
  • 私有声明:为特定使用场景自定义的声明,不会与其他应用冲突。
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516239022
}

3. Signature(签名)

 签名用于验证消息在传输过程中没有被更改,并且对于使用私钥签名的令牌,还可以验证发送者的身份。签名的生成方式如下:

  • 将编码后的头部和载荷用点号连接起来。
  • 使用指定的算法(如HMAC SHA256)和密钥对上述字符串进行签名。

2.2.2 具体实现示例

导入依赖:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>
2.2.2.1 生成令牌
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
public class JWTTokenController {

    @GetMapping("/setToken")
    public String setToken() {
        Map<String,Object> claims = new HashMap<>();
        claims.put("id",1);
        claims.put("username","Tom");

        String jwt = Jwts.builder()
                .setClaims(claims) //自定义内容(载荷)
                .signWith(SignatureAlgorithm.HS256, "mySignKey") //签名算法
                .setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期
                .compact();

        return jwt;
    }
}

运行调用方法:

eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNzMwNDQ0NTUzLCJ1c2VybmFtZSI6IlRvbSJ9.KGJQuKrnGCROPW8rfFZGchzoBPFjRM-sAv18XaV1vUQ

输出的结果就是生成的JWT令牌,,通过英文的点分割对三个部分进行分割,我们可以将生成的令牌复制一下,然后打开JWT的官网,将生成的令牌直接放在Encoded位置,此时就会自动的将令牌解析出来。

第一部分解析出来,看到JSON格式的原始数据,所使用的签名算法为HS256。

第二个部分是我们自定义的数据,之前我们自定义的数据就是id,还有一个exp代表的是我们所设置的过期时间。

由于前两个部分是base64编码,所以是可以直接解码出来。但最后一个部分并不是base64编码,是经过签名算法计算出来的,所以最后一个部分是不会解析的。

2.2.2.2 校验令牌
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
public class JWTTokenController {

    @GetMapping("/getToken")
    public Claims getToken() {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNzMwNDQ0NTUzLCJ1c2VybmFtZSI6IlRvbSJ9.KGJQuKrnGCROPW8rfFZGchzoBPFjRM-sAv18XaV1vUQ";
        // 获取Session
        Claims claims = Jwts.parser()
                .setSigningKey("mySignKey")//指定签名密钥(必须保证和生成令牌时使用相同的签名密钥)
                .parseClaimsJws(token)
                .getBody();

        return claims;
    }
}

 运行调用方法:

{"id":1,"exp":1730444553,"username":"Tom"}

 下面我们做一个测试:把令牌签名最后一位Q修改为K,运行测试方法后发现报错:

  • 原签名: KGJQuKrnGCROPW8rfFZGchzoBPFjRM-sAv18XaV1vUQ
  • 修改为: KGJQuKrnGCROPW8rfFZGchzoBPFjRM-sAv18XaV1vUK

使用JWT令牌时需要注意:

  • JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。
  • 如果JWT令牌解析校验时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。

 2.3 优缺点

优点:

  • 服务器不需要维护会话状态,减少了内存和数据库的负担。
  • Token可以通过HTTP头部传递,支持跨域请求,适合现代的前端应用架构,如SPA(单页应用)。
  • Token通过加密签名确保其完整性和防篡改性。
缺点:
  • 每次请求都需要携带Token,增加了HTTP请求的大小,可能影响性能,特别是在移动设备上。
  • Token中包含的信息越多,Token的大小越大,可能会超过某些存储方式(如Cookie)的限制。
  • 一旦Token被发出,除非过期,否则无法直接撤销。这在用户注销或Token被盗的情况下是一个问题。
  • 如果客户端存储Token的方式不安全(如未使用HttpOnly Cookie),Token可能被XSS攻击窃取。
  • 虽然使用HTTPS可以保护Token在传输过程中的安全,但如果配置不当,仍可能存在风险。
  • 需要处理Token的生成、验证、刷新和撤销等逻辑,增加了系统的复杂性。
  • 需要处理Token过期、无效等情况,增加了错误处理的复杂性。
  • 客户端需要正确管理和存储Token,如果客户端实现不当,可能会影响用户体验和安全性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

可儿·四系桜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值