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

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查询到数据,拒绝访问。

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

        分布式session

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存储的工具,帮我们快速简单的解决了问题。

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.6.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
    <version>2.6.3</version>
</dependency>

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

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

spring:
  session:
    timeout: 86400
    store-type: redis
  # redis 配置
  redis:
    port: 6379
    host: localhost
    password:
    database: 2
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

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

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

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

4.1登录流程

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

                1.前端发起登录请求。

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

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

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

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

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

4.2Token令牌的设计

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

4.2.1JWT的组成

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

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

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

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

        载荷是记录信息,一般是一个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;
        }

    }


}
        进行注册拦截器
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深入理解等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值