问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Session登陆实践:从单机到分布式解决方案

创作时间:
作者:
@小白创作中心

Session登陆实践:从单机到分布式解决方案

引用
CSDN
1.
https://blog.csdn.net/m0_62963408/article/details/136591556

Session登录是一种常见的Web应用程序身份验证和状态管理机制。本文将详细介绍Session登录的基本流程,并通过Spring Boot框架展示单机Session登录和分布式Session登录的具体实现。

Session登陆实践

Session登录是一种常见的Web应用程序身份验证和状态管理机制。当用户成功登录到应用程序时,服务器会为其创建一个会话(session),并在会话中存储有关用户的信息。这样,用户在与应用程序交互的整个会话期间都可以被识别,并且可以维护其状态。

以下是Session登录的基本流程:

  1. 用户提交登录请求:用户在应用程序的登录页面输入用户名和密码,然后提交登录表单。
  2. 服务器验证身份:应用程序服务器接收到登录请求后,会验证用户提供的用户名和密码是否匹配数据库中存储的凭据。如果验证成功,将进入下一步;否则,用户将收到身份验证失败的通知。
  3. 创建Session:一旦用户身份验证成功,服务器会为该用户创建一个唯一的会话标识符(Session ID)。通常,这个Session ID会被存储在用户的浏览器中,例如通过Cookie或URL参数的方式。
  4. 存储用户信息:服务器将与用户相关的信息(如用户ID、角色等)存储在该Session中。这些信息可以在整个会话期间用于标识用户和维护其状态。
  5. 返回登录成功响应:服务器向用户的浏览器返回登录成功的响应,可能包括一些用户信息或重定向到用户的个人资料页面。
  6. 保持会话状态:在用户与应用程序交互的过程中,服务器会根据Session ID识别用户,并使用存储在Session中的信息来维护用户的状态。这可以包括用户的登录状态、权限、购物车内容等。
  7. 注销处理:用户在应用程序中选择注销时,服务器会销毁与用户关联的Session,用户需要重新进行身份验证才能再次访问受保护的资源。

单机Session登陆

单机(单节点)Session登录是指在单一服务器环境中进行用户身份验证和会话管理的方式。这种情况下,用户的身份信息和会话状态仅存储在单个服务器上,而不涉及多个服务器之间的共享

实践

以下案例基于SpringBoot

Controller

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    /**
     * 用户登录
     *
     * @param userLoginRequest
     * @param request
     * @return
     */
    @PostMapping("/login")
    public BaseResponse<UserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
        if (userLoginRequest == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        String userAccount = userLoginRequest.getUserAccount();
        String userPassword = userLoginRequest.getUserPassword();
        if (StringUtils.isAnyBlank(userAccount, userPassword)) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        User user = userService.userLogin(userAccount, userPassword, request);
        UserVO userVO = new UserVO();
        BeanUtils.copyProperties(user, userVO);
        return ResultUtils.success(userVO);
    }
        
    /**
     * 用户注销
     *
     * @param request
     * @return
     */
    @PostMapping("/logout")
    public BaseResponse<Boolean> userLogout(HttpServletRequest request) {
        if (request == null) {
            throw new BusinessException(ErrorCode.PARAMS_ERROR);
        }
        boolean result = userService.userLogout(request);
        return ResultUtils.success(result);
    }
  
}
  

Service

/**
 * 用户登陆
 */
@Override
public User userLogin(String userAccount, String userPassword, HttpServletRequest request) {
    // 1. 校验
    if (StringUtils.isAnyBlank(userAccount, userPassword)) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
    }
    if (userAccount.length() < 4) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号错误");
    }
    if (userPassword.length() < 8) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
    }
    // 2. 加密
    String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
    // 查询用户是否存在
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("userAccount", userAccount);
    queryWrapper.eq("userPassword", encryptPassword);
    User user = userMapper.selectOne(queryWrapper);
    // 用户不存在
    if (user == null) {
        log.info("user login failed, userAccount cannot match userPassword");
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误");
    }
    // 3. 记录用户的登录态 
    request.getSession().setAttribute(USER_LOGIN_STATE, user); // USER_LOGIN_STATE是常量可自定义
    return user;
}
/**
 * 用户注销
 *
 * @param request
 */
@Override
public boolean userLogout(HttpServletRequest request) {
    if (request.getSession().getAttribute(USER_LOGIN_STATE) == null) {
        throw new BusinessException(ErrorCode.OPERATION_ERROR, "未登录");
    }
    // 移除登录态
    request.getSession().removeAttribute(USER_LOGIN_STATE);
    return true;
}
/**
 * 获取当前登录用户
 *
 * @param request
 * @return
 */
@Override
public User getLoginUser(HttpServletRequest request) {
    // 先判断是否已登录
    Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
    User currentUser = (User) userObj;
    if (currentUser == null || currentUser.getId() == null) {
        throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
    }
    // 从数据库查询(追求性能的话可以注释,直接走缓存)
    long userId = currentUser.getId();
    currentUser = this.getById(userId);
    if (currentUser == null) {
        throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR);
    }
    return currentUser;
}
  

流程解答

HttpServletRequest

中的

Session

是一个表示用户会话的对象,它属于Java Servlet API 中的

HttpSession

接口。

HttpSession

提供了一种在请求之间存储和检索用户特定数据的方式,允许在整个用户会话期间保持状态信息。

HttpServletRequest

中,你可以通过调用

getSession()

方法来获取与当前请求相关联的

HttpSession

对象。例如:

HttpSession session = request.getSession();

getSession()

方法会检查请求中是否存在与会话相关的标识符(通常是Cookie中的

JSESSIONID

),如果存在,则返回与该标识符相关联的

HttpSession

对象;如果不存在,则创建一个新的

HttpSession

对象,并在响应中将新的会话标识符(

JSESSIONID

)发送给客户端。

HttpSession

的结构是一个键值对的存储结构,类似于一个

Map

。你可以使用

setAttribute

getAttribute

方法来设置和获取会话中的属性。例如:

// 设置会话属性
session.setAttribute("USER_LOGIN_STATE", "userId");
// 获取会话属性
String userId = (String) session.getAttribute("USER_LOGIN_STATE");

这里的会话属性是根据键值对存储在

HttpSession

中的,你可以根据需要在会话中存储和检索数据,以便在用户的整个会话期间保持状态。

会话通常在用户访问应用程序时被创建。当用户首次访问应用程序时,Servlet容器会为其创建一个新的

HttpSession

对象,并将其与请求关联。这个会话对象将持续存在,直到会话过期、用户注销或关闭浏览器。会话的过期时间可以通过配置进行调整。

总而言之,

HttpServletRequest

中的

Session

是一个

HttpSession

对象,它在用户访问应用程序时被创建,用于在请求之间共享和保持用户状态信息。

设置Sesion有效期

在项目的Resource目录下的

application.yml

application.properties

中修改配置

spring:
  #session的失效时间 86400s = 1天
  session:
    timeout: 86400

分布式Session登陆

单机模式下,不同的Session对象都被保存在同一个服务器中,服务器根据保存在用户浏览器Cookie中的SessionId查找Session对象

假如我们的服务端是分布式的,也就是有多台服务器同时提供服务,假如在用户登陆请求发送到服务器A,服务器A保存了<SessionId, 用户Id>;突然服务器A宕机,接下来当前用户的所有请求将要发送到服务器B,但此时服务器B中没有当前保存当前用户的登陆态,显然单机Session登陆不太适合分布式下的用户登陆

解决方案:

单机Session登陆的局限在于,Session保存在一台服务器上,其他服务器无法获取用户是否登陆;那么把用户登陆的Session保存在所有服务器都能获取的地方不久好了嘛

这里介绍最常用的解决方案也就是使用Redis存储Session

实践

将单机Session登陆改为分布式Session登陆只需要以下配置👇

需要安装Redis,这里就不介绍怎么安装了,可自行上网搜索

在项目的pom.xml文件中引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

spring-boot-starter-data-redis

是SpringBoot简化操作Redis的依赖,

spring-session-data-redis

是简化将Session保存在Redis 的依赖

在项目的Resource目录下的

application.yml

application.properties

中修改配置

spring:
  session:
      storeType: redis # 使用Redis存储Session
    timeout: 86400 #session的失效时间 86400s = 1天
  redis:
    port: 6379 #Redis所在的端口号
    host: xxx.xxx.xxx.xxx # Redis的远程地址,部署在单机可以写localhost
    database: 0
    password: xxx #Redis没有密码,可以删除此行

只需要添加依赖和修改一下配置即可将单机Session登陆改为分布式Session登陆,这就是SpringBoot的强大之处🐮
此时我们登陆一下就可以看见Redis中存储了用户的Session

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号