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

77 阅读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

2.2session实现单机账号登录的代码实现
定义接口之类的代码就不呈现了,这里只呈现核心代码。

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

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

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

 // 用户脱敏
User safetyUser = getSafetyUser(user);
// 记录用户的登录态
request.getSession().setAttribute(USER_LOGIN_STATE, safetyUser);

2.2.2session实现登录鉴权 使用这行代码就可以获取到以前设置的sessionID对应的值的数据。

request.getSession.getAttribute(key)。

 User user = (User) request.getSession().getAttribute(USER_LOGIN_STATE);

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

    request.getSession.removeAttribute(key)。

request.getSession().removeAttribute(USER_LOGIN_STATE); 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存储的工具,帮我们快速简单的解决了问题。

1738671101204.png

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

1738671056020.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。

1738671212448.png

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

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

    合并三部分:将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令牌。

io.jsonwebtoken jjwt 封装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; }

    }

}

    进行注册拦截器

import com.chenhai.interceptor.JwtTokenAdminInterceptor; import com.chenhai.interceptor.JwtTokenUserInterceptor; import com.chenhai.json.JacksonObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket;

import java.util.List;

/**

  • 配置类,注册web层相关组件 */ @Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired private JwtTokenUserInterceptor jwtTokenUserInterceptor;

    /**

    • 注册自定义拦截器
    • @param registry */ protected void addInterceptors(InterceptorRegistry registry) { log.info("开始注册自定义拦截器..."); registry.addInterceptor(jwtTokenUserInterceptor) .addPathPatterns("/user/") .excludePathPatterns("/user/user/login") .excludePathPatterns("/user/user/register") .excludePathPatterns("/user/user/verifiCode") .excludePathPatterns("/user/shop/status") .excludePathPatterns("/user/ourstory/"); } }

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深入理解等。

————————————————

                        版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
                    

原文链接:blog.csdn.net/2301_791087…