登录注册和登录状态维护场景总结(一)

90 阅读9分钟

1.登录注册的场景分析

根据功能性分析:登录注册场景主要可以有账号密码登录注册,短信登录注册,微信扫码登录注册等等,在本文仅仅探讨第一种情况,后两种情况将在下周更新,敬请期待本专栏的更新。

根据服务器场景的分析:又分为了单机登录注册和分布式登录注册,这两种情况下使用的技术不同,并且保持登录状态的用法也不同。

2.session实现单机登录

使用session进行实现登录和状态保持的优点就是简单。

首先我们从session入门,实现基于session的单机登录注册。

如果不知道session是什么我将会下周出一个文章详细介绍一下,这里我们先简单介绍一下session是什么。

2.0session是什么?

session是一种基于cookie的,为了记录HTTP连接时,服务端和客户端会话的一种技术手段。

2.1session实现单机账号登录的流程

2.1.1登录流程

一般使用session进行登录的时候遵循以下的登录流程,首先进行发起发起登录请求,服务器先去数据库查询信息校验信息是否合规,如果合规就会去存储用户的登录态,并生成一个sessionID(tomcat生成的是jsessionid),将sessionID返回给前端后,前端基于Cookie进行存储这个sessionID,等下次再进行发送这个请求的时候方便携带sessionID校验登录态。

2.1.2校验登录态的流程

用户进行登录之后,向服务端再次发送请求的时候会携带sessionID(以Cookie的形式),当服务端收到sessionID的时候,可以通过sessionID去查询用户的登录态(可能是用户ID也可能是用户DTO对象),如果查询到数据,放行,如果没有根据sessionID查询到数据,拒绝访问。

image.png

image.png

2.2session实现单机账号登录的代码实现

定义接口之类的代码就不呈现了,这里只呈现核心代码。

2.2.1session实现登录

一般都会往session中存入一个脱敏后的用户对象作为登录态。如果你使用的是SpringBoot项目,Controller层会自动往接口方法中注入HttpServletRequest request,只需要进行在接口方法参数内传入这个即可。

接收前端传递的sessionID框架已经帮我们做了这件事,我们只需要进行获取到session,往服务器sessionID对应的数据里存入键值对即可。

获取session进行设置属性需要使用request.getSession().setAttribute(key, value)。

1738765104736.png

2.2.2session实现登录鉴权

使用这行代码就可以获取到以前设置的sessionID对应的值的数据。

request.getSession.getAttribute(key)。 1738765145805.png

2.2.3session实现退出登录

实现退出登录也很简单,只需要把以前设置的键值对删除就好了。

request.getSession.removeAttribute(key)。

1738765357883.png

3.session实现分布式账号登录

3.1单机session的缺点

单机session

image.png

分布式session

image.png

3.2分布式session的设计

根据上面的流程图我们可以很容易看明白,服务器之间是不回知道彼此存储的session的,当然以前tomcat也给出了解决方案,可以进行配置服务器前互相交换彼此的sessionID,但是这已经不是流行的解决方案,目前流行的解决方案就是采用redis进行实现分布式session的存储,因为我们必须要一个公共的地方进行存储数据,而且还能有高的IO能力,所以使用redis这种基于内存的NOSQL数据库,就十分适合,可以统一存储sessionID,并且IO能力也强。

3.3实现分布式session

3.3.1引入依赖

引入spring-boot-starter-data-redis的依赖目的是使用springboot快速整合redis进行使用,第二个依赖是Spring提供了使用redis作为sessionID存储的工具,帮我们快速简单的解决了问题。

1738765798877.png 3.3.2配置session使用redis进行存储sessionID

当我们引入以上依赖之后,仅仅需要在application.yml中进行以下简单的配置,就可以使用redis存储sessionID啦。

1738765832179.png

3.4登录态信息我们要如何存储?

登录态无需进行存储,我们只需要进行每次请求的时候从request中进行获取登录态信息即可,当然如果感觉麻烦,可以使用拦截器进行统一获取request请求头,然后统一将登录态信息存储到ThreadLocal里面进行存储,这种思路我们在下一种token令牌登录中进行介绍。

4.使用TOKEN令牌实现单机/分布式登录

4.1登录流程

进行登录的时候的流程分以下几个步骤:

1.前端发起登录请求。

2.后端根据前端提交的数据信息去数据库查询信息并进行校验。

3 校验通过后采用一定的加密算法将生成好的TOKEN令牌返回给前端进行使用

image.png

下一次再登录的时候前端会携带令牌而来

1.前端携带Token进行发起登录请求

2.后端进行解密TOKEN,解密后取出代表登录态的信息,进行根据登录态的有无处理请求。

image.png

4.2Token令牌的设计

在这里我们采用JWT进行加密生成Token,我们先来介绍一下JWT的组成。

4.2.1JWT的组成

JWT是一种信息标准,是json信息加密后生成的Token令牌,经常用于信息交换和授权。

由三部分进行组成,头部(header).载荷(payload).签证(signature)

头部就是json信息,会进行记录JWT的加密算法等,代表使用的是HS256对称加密算法,类型是iJWT。

1738765974009.png

载荷是记录信息,一般是一个json形式的对象信息,一般作为token令牌的时候会作为存储登录态信息的地方,一般会进行存储一些用户的信息。

 {"username": "哈哈哈"}

生成签名:使用选择的算法和密钥对头部和负载进行签名。签名确保了令牌的安全性。

合并三部分:将eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjA5NjIxMDYsInVzZXJuYW1lIjoi5ZOI5ZOI5ZOI5ZOIIn0前两部分进行连接,使用密钥进行和前两部分一起加密进行生成签名。

 @Test
void testJWT() {
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("username", "哈哈哈哈");
String jwt = JwtUtil.createJWT("daidhasuihcioasdas46d5as", 1, hashMap);
System.out.println(jwt);
}

4.2.2JWT生成工具类

进行引入io.jsonwebtoken依赖,使用这个工具类可以帮助我们快速的生成我们需要的JWT令牌。

 <dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>

封装JWT加密工具类。

  import io.jsonwebtoken.Claims;
  import io.jsonwebtoken.JwtBuilder;
  import io.jsonwebtoken.Jwts;
  import io.jsonwebtoken.SignatureAlgorithm;

  import java.nio.charset.StandardCharsets;
  import java.util.Date;
  import java.util.Map;

  public class JwtUtil {
  public static String createJWT(String secretKey, long ttlMillis, Map<String, Object>           claims) {
    // 指定签名的时候使用的签名算法,也就是header那部分
    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 生成JWT的时间
    long expMillis = System.currentTimeMillis() + ttlMillis;
    Date exp = new Date(expMillis);

    // 设置jwt的body
    JwtBuilder builder = Jwts.builder()
            // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
            // 进行设置的是第二部分,进行设置用户的登录态信息
            .setClaims(claims)
            // 设置签名使用的签名算法和签名使用的秘钥
            .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
            // 设置过期时间
            .setExpiration(exp);

    return builder.compact();
}


/**
 * Token解密
 *
 * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
 * @param token     加密后的token
 * @return
 */
public static Claims parseJWT(String secretKey, String token) {
    // 得到DefaultJwtParser
    Claims claims = Jwts.parser()
            // 设置签名的秘钥
            .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
            // 设置需要解析的jwt
            .parseClaimsJws(token).getBody();
    return claims;
}

}

4.3使用Token令牌实现登录功能

4.3.1用户登录时进行颁发token令牌

使用工具进行生成好token令牌后,将token令牌返回给前端进行保存。

4.3.2封装ThreadLocal统一存储ID数据

将登录态数据均存储在ThreadLocal中,进行调用登录态的时候也进行使用。

package com.chenhai.context;

public class BaseContext {

public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

public static void setCurrentId(Long id) {
    threadLocal.set(id);
}

public static Long getCurrentId() {
    return threadLocal.get();
}

public static void removeCurrentId() {
    threadLocal.remove();
}

}

4.3.3用户登录的时候统一进行使用拦截器获取token令牌

为什么需要进行配置一个拦截器进行统一获取token令牌呢?当然我们也可以像上面那种方式一样每次请求的时候都从request中进行读取信息,登录鉴权,但是这样太麻烦了,我们想要统一登录鉴权和处理获取token令牌,解析出来登录态之后统一存储到ThreadLocal中进行存储,需要使用登录态数据的时候,就直接取ThreadLocal中取,这样岂不是特别方便?

进行封装拦截器

进行校验的时候,直接进行解密即可,如果密钥解密失败,那就是信息违法,校验失败,解密成功即可通过校验。

 package com.chenhai.interceptor;

 import com.chenhai.constant.JwtClaimsConstant;
 import com.chenhai.context.BaseContext;
 import com.chenhai.properties.JwtProperties;
 import com.chenhai.utils.JwtUtil;
 import io.jsonwebtoken.Claims;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.web.method.HandlerMethod;
 import org.springframework.web.servlet.HandlerInterceptor;

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

 /**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

@Autowired
private JwtProperties jwtProperties;

public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String AUTHORIZATION_SCHEMA = "Bearer ";
/**
 * 校验jwt
 *
 * @param request
 * @param response
 * @param handler
 * @return
 * @throws Exception
 */
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //判断当前拦截到的是Controller的方法还是其他资源
    if (!(handler instanceof HandlerMethod)) {
        //当前拦截到的不是动态方法,直接放行
        return true;
    }

    //1、从请求头中获取令牌
    String token = request.getHeader(jwtProperties.getUserTokenName());
    //2、校验令牌

    log.info("jwt校验:{}", token);

    try {
        log.info("jwt校验:{}", token);
        Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
        Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
        log.info("当前用户id:", userId);
        // 进行将用户态信息存储到ThreadLocal中
        BaseContext.setCurrentId(userId);
        //3、通过,放行
        return true;
    } catch (Exception ex) {
        //4、不通过,响应401状态码
        response.setStatus(401);
        return false;
    }

   }


 }

进行注册拦截器

1738766260479.png 1738766297795.png 4.3.4进行使用ThreadLocal中存储的登录态信息

// 1. 将当前用户的所有地址都修改为非默认地址 update address_book set is_default = ? where     user_id = ?
addressBook.setIsDefault(0);
addressBook.setUserId(BaseContext.getCurrentId());
addressBookMapper.updateIsDefaultByUserId(addressBook);

 // 2. 将当前地址改为默认地址 update address_book set is_default = ? where id = ?
addressBook.setIsDefault(1);
addressBookMapper.update(addressBook);

5.结语

又听了一天白羊....

今天分析了一些登录的场景和应用,下一期我会在更新更多的登录和保持登录态的技术实现,也会横向对比一些登录的优缺点,近期准备更新session的原理,加密技术,ThreadLocal深入理解等。 —