vue+springboot实现JWT登录验证

前言

最近在研究SSO(单点登录)系统,对于内部是如何实现登录验证的产生了兴趣,而现在终于研究出它是如何验证成功的,接下来我将讲解如何通过vue和springboot实现Jwt验证登录
🌺🌹🥀🌺🥀🌹🌺🌹🥀🌺🥀🌹

概念

在正式开始之前,我同样会讲解一下概念
单点登录:

单点登录(Single Sign-On, SSO)是一种身份认证授权机制,允许用户在多个应用程序或系统中进行登录,并在登录后可以无需重新输入凭据就能访问其他应用程序。通过SSO,用户只需登录一次,即可获得对多个相关但独立的软件系统或资源的访问权限。

那么这篇文章,只会讲解如何实现身份认证,并不会讲解如何实现SSO

🟠🟡🔴🟠🟣🔵🟡🟠🟣


JWT:

JWT全称为JSON Web Token,是一种开放标准(RFC 7519),定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息。它通常用于在用户和服务之间传递身份认证信息和声明。JWT通常被用作实现身份验证和授权的标准方法。

JWT的本质就是一个字符串,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改

🌺🌹🥀🌺🥀🌹🌺🌹🥀🌺🥀🌹

实际演示

路由信息

在我的项目中,我的router页面有这些:

export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/main',
    component: () => import('@/views/main'),
    children: [
      {
        path: '',
        name: 'dashBorad',
        component: () => import('@/views/dashBorad')
      },
      {
        path: '/menuPage',
        name: 'menuPage',
        component: () => import('@/views/menuPage')
      },
      {
        path: '/userDataManage',
        name: 'userDataManage',
        component: () => import('@/views/user/userDataManage')
      }
    ]
  },
  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },
  // 404 page must be placed at the end !!!
  {path: '*', redirect: '/404', hidden: true}
]
初始访问登录界面

初始我访问界面,会到登录界面
在这里插入图片描述
假如我在未登录的情况下想访问其他路由,会禁止:
在这里插入图片描述
自动会跳转到登录界面
在这里插入图片描述

登录验证

假如输入账号密码登录之后,才能进入到系统内:
在这里插入图片描述
这个时候能够切换到不同的界面:
在这里插入图片描述
并且能够调用后端接口查询数据:
在这里插入图片描述

验证过期

但是一旦jwt验证过期,为演示方便,这边将手动把token修改错误

此时再跳转界面和查数据都会,报错,且跳转到登录页:
在这里插入图片描述
在这里插入图片描述

🧡💚💛🧡💜🧡🧡💚💛🧡💜🧡
🌺🌷🌻🌼🌷🌺🌷🌻🌼🌷🌻🌼~~~~~~~~
🧡💚💛🧡💜🧡🧡💚💛🧡💜🧡

vue实现

🌴🌳🍀🌲🥀🍁

依赖引入

在我的项目中,涉及相关JWT验证的有如下:

// axios
npm i axios@1.5.0
// elementui
npm i element-ui -S
// router
npm i vue-router
// js-cookie
npm i js-cookie

🧡🧡🧡🧡🧡🧡🧡🧡🧡🧡🧡🧡

main.js

这时main.js的代码如下:

import Vue from 'vue'
import App from './App'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false

Vue.use(ElementUI)
new Vue({
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

💛💛💛💛💛💛💛💛💛💛💛💛

获取和设置token工具类

这里写一个工具类专门获取和设置工具类
建一个token.js

import Cookies from 'js-cookie'
// 获取token的key,需要和后端一致
const TokenKey = 'Authorization'
// 获取token
export function getToken () {
  return Cookies.get(TokenKey)
}
// 设置token
export function setToken (token) {
  return Cookies.set(TokenKey, token)
}
// 移除token
export function removeToken () {
  return Cookies.remove(TokenKey)
}

💙💙💙💙💙💙💙💙💙💙💙💙

登录方法

因为我的登录方法存在别的逻辑,如验证码,记住我等等,因此,这里演示只给出最纯粹的登录逻辑

实体
// 设置用户名和密码登录
      loginForm: {
        username: 'admin',
        password: 'admin',
      },

登录方法涉及到引入

// 注意,这里文件的位置请根据自己实际项目文件位置进行修改
import { getToken, setToken } from '@/utils/token'
import {login} from '../../api/login'

🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸

登录方法

点击按钮调用登录方法

    submitLogin () {
      if (!this.loginForm.username) {
        this.$message.error('用户名不能为空!')
        return
      }
      if (!this.loginForm.password) {
        this.$message.error('密码不能为空!')
        return
      }
      login(this.loginForm.username, this.loginForm.password).then(res => {
        if (res.header.code !== 0) {
          this.$message.error(res.header.message)
          return
        }
        // 设置token
        setToken(res.value.token)
        // 根据自己实际项目跳转主界面
        this.$router.push('/main')
      })
    },

当这里登录之后会在cookie位置新增数据(F12):
在这里插入图片描述
💐💐💐💐💐💐💐💐💐💐💐💐💐

axios请求

新增axios的工具类,进行封装,在这里会在调用之前确认是否验证过期
request.js: 新建js代码,代码如下:

import axios from 'axios'
import { Message, MessageBox, Notification } from 'element-ui'
import { getToken } from '@/utils/token'
import errorCode from '@/utils/errorCode'
import router from '../router/index'
import {removeToken} from './token'

axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'

// 创建axios实例
const service = axios.create({
  // 配置后端请求路径,根据自己实际项目修改
  baseURL: process.env.VUE_APP_BASE_API,
  // 请求超时时间 withCredentials: true,
  timeout: 30000
})

// 请求统一拦截处理
service.interceptors.request.use(config => {
  // 是否需要设置 token
  if (getToken()) {
    config.headers['Authorization'] = getToken() // 请求均需携带自定义token
  }
  return config
},
error => {
  // 请求失败
  console.log(error) // for debug
  // return Promise.reject(error)
  return Promise.reject(error)
}
)

// 响应拦截器
service.interceptors.response.use(res => {
  console.log('res.data', res.data)
  // 未设置状态码则默认成功状态
  const code = res.data.header.code || 200
  // 获取错误信息
  const msg = errorCode[code] || res.data.header.message || errorCode['default']
  if (code === 401) {
    return new Promise((resolve, reject) => {
      MessageBox.confirm('登录状态已过期,请重新登录', '系统提示', {
        confirmButtonText: '重新登录',
        showCancelButton: false,
        type: 'warning'
      }).then(() => {
        removeToken()
        router.push('/login')
        resolve() // 手动解决 Promise,避免重复导航
      })
    })
  } else if (code === 500) {
    Message({
      message: msg,
      type: 'error'
    })
    return Promise.reject(new Error(msg))
  } else if (code !== 0 && code !== 200) {
    Notification.error({
      title: msg
    })
    // eslint-disable-next-line prefer-promise-reject-errors
    return Promise.reject('error')
  } else {
    return res.data
  }
}, error => {
  let { message } = error
  if (message === 'Network Error') {
    message = '服务端连接异常'
  } else if (message.includes('timeout')) {
    message = '系统接口请求超时'
  } else if (message.includes('Request failed with status code')) {
    message = '系统接口' + message.substr(message.length - 3) + '异常'
  }
  Message({
    message: message,
    type: 'error',
    duration: 5 * 1000
  })
  return Promise.reject(error)
}
)

export default service

上述会捕获后端返回的code,做不同的事情,是否可调用接口

针对上述的errorCode ,为新建的封装报错代码js,根据自己需要可做可不做
errorCode.js: 代码如下:

export default {
  '401': '认证失败,无法访问系统资源',
  '403': '当前操作没有权限',
  '404': '访问资源不存在',
  'default': '系统未知错误,请反馈给管理员'
}

上述登录方法涉及到的axios的api如下
login.js

import request from '@/utils/request'

// 登录方法
export function login (account, password) {
  const data = {
    account,
    password
  }
  return request({
    url: '/idle/login',
    method: 'post',
    data: data
  })
}

// 验证token是否有效
export function verify (token) {
  let data = {
    token
  }
  return request({
    url: '/idle/verify',
    method: 'get',
    params: data
  })
}

router配置

跳转路由,拦截请求是否已经过期
新建router文件夹,在其中建立index.js
代码如下:

import Vue from 'vue'
import Router from 'vue-router'
// eslint-disable-next-line standard/object-curly-even-spacing
import {getToken, removeToken } from '@/utils/token'
import {verify} from '@/api/login'
import { Message } from 'element-ui'

Vue.use(Router)

export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/main',
    component: () => import('@/views/main'),
    children: [
      {
        path: '',
        name: 'dashBorad',
        component: () => import('@/views/dashBorad')
      },
      {
        path: '/menuPage',
        name: 'menuPage',
        component: () => import('@/views/menuPage')
      },
      {
        path: '/userDataManage',
        name: 'userDataManage',
        component: () => import('@/views/user/userDataManage')
      }
    ]
  },
  {
    path: '/404',
    component: () => import('@/views/404'),
    hidden: true
  },
  // 404 page must be placed at the end !!!
  {path: '*', redirect: '/404', hidden: true}
]

const createRouter = () => {
  const router = new Router({
    mode: 'hash',
    scrollBehavior: () => ({y: 0}),
    routes: constantRoutes
  })

  router.beforeEach((to, from, next) => {
    let token = getToken()
    if (!token) {
      // 如果未登录并且不是去往登录页,则跳转到登录页
      if (to.path !== '/login') {
        next('/login')
      } else {
        next() // 如果是去往登录页,则直接放行
      }
    } else {
      // 已登录状态
      verify(token).then(res => {
        let isVerify = res.value
        // 判断是否token验证成功,验证成功则跳转要去的路由,否则报错,跳回登录界面
        if (isVerify) {
          next()
        } else {
          removeToken()
          setTimeout(() => { next('/login') }, 1500)
        }
      }).catch(() => {
        next('/login') // 异步操作失败后再手动重定向
      })
    }
  })

  return router
}

const router = createRouter()

export function resetRouter () {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router

通过以上router.jsrequest.js,即可在跳转页面以及访问后端接口的时候进行拦截验证

🌼🌼🌼🌼🌻🌻🌻🌻🌻🌷🌷🌷🌷🌷🌷🌷🌼🌼🌼🌼🌻🌻🌻🌻🌻

springboot实现

依赖引入

同样,为了实现JWT,我们后端也需要做一些引入,注意:本次引入只涉及到JWT相关,其他自己项目相关请额外进行引入

    <dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>3.4.0</version>
    </dependency>

🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵

JWT工具类

新建工具类,命名JwtTokenUtil:
代码如下:

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import java.util.Date;

public class JwtTokenUtil {


  //定义token返回头部
  public static final String AUTH_HEADER_KEY = "Authorization";


  //token前缀
  public static final String TOKEN_PREFIX = "Bearer ";


  //签名密钥
  public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x";

  //有效期默认为 2hour
  public static final Long EXPIRATION_TIME = 1000L * 60 * 60 * 2;


  /**
   * 创建TOKEN
   */
  public static String createToken(String content) {
    return TOKEN_PREFIX + JWT.create()
        .withSubject(content)
        .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
        .sign(Algorithm.HMAC512(KEY));
  }


  /**
   * 验证token
   */
  public static String verifyToken(String token) throws Exception {
    try {
      return JWT.require(Algorithm.HMAC512(KEY))
          .build()
          .verify(token.replace(TOKEN_PREFIX, ""))
          .getSubject();
    } catch (TokenExpiredException e) {
      throw new Exception("token已失效,请重新登录", e);
    } catch (JWTVerificationException e) {
      throw new Exception("token验证失败!", e);
    }
  }

  public static Boolean verify(String token) throws Exception {
    try {
      JWT.require(Algorithm.HMAC512(KEY))
          .build()
          .verify(token.replace(TOKEN_PREFIX, ""))
          .getSubject();
      return true;
    } catch (Exception e) {
      return false;
    }
  }
}

忽视jwt验证注解

新建一个注解,用于忽视验证,比如登录,注册方法

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtIgnore {
  boolean value() default true;
}

🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱

拦截器逻辑

import cn.hutool.json.JSONObject;
import com.pearl.Interface.JwtIgnore;
import com.pearl.utils.JwtTokenUtil;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

@Slf4j
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
      throws Exception {
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=utf-8");
    // 从http请求头中取出token
    final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
    //如果不是映射到方法,直接通过
    if (!(handler instanceof HandlerMethod)) {
      return true;
    }
    //如果方法有JwtIgnore注解,直接通过
    HandlerMethod handlerMethod = (HandlerMethod) handler;
    Method method = handlerMethod.getMethod();
    if (method.isAnnotationPresent(JwtIgnore.class)) {
      JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);
      if (jwtIgnore.value()) {
        return true;
      }
    }
    // 执行认证
    if (StringUtils.isEmpty(token)) {
      JSONObject res = new JSONObject();
      res.put("code", 401);
      res.put("msg", "无token,请重新登录");
      res.put("data", false);
      PrintWriter out = response.getWriter();
      out.append(res.toString());
      return false;
    }
    if (!JwtTokenUtil.verify(token)) {
      JSONObject res = new JSONObject();
      res.put("code", 401);
      res.put("msg", "token验证失败,请重新登录");
      res.put("data", false);
      PrintWriter out = response.getWriter();
      out.append(res.toString());
      return false;
    }
    return true;
  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
      Object handler, Exception ex) throws Exception {

  }
}

🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳

跨域&调用拦截器配置

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class GlobalWebMvcConfig implements WebMvcConfigurer {

  /**
   * 重写父类提供的跨域请求处理的接口
   */
  @Override
  public void addCorsMappings(CorsRegistry registry) {
    // 添加映射路径
    registry.addMapping("/**")
        .allowedOriginPatterns("*")  // 允许所有来源
        .allowCredentials(true)      // 允许发送身份验证凭据
        .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD")
        .allowedHeaders("*")
        .exposedHeaders("Server", "Content-Length", "Authorization", "Access-Token",
            "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials");
  }

  // 添加拦截器,我的项目的基础路径为sso,登录接口路径为/sso/idle/login
  // addPathPatterns是拦截所有路径,excludePathPatterns是排除需要拦截的路径
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new AuthenticationInterceptor())
        .addPathPatterns("/**")
        .excludePathPatterns("/sso/idle/login");
  }
}

🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴

登录接口&验证token接口


import com.pearl.Interface.JwtIgnore;
import com.pearl.entitys.beans.UserLoginData;
import com.pearl.entitys.dataBaseTable.User;
import com.pearl.responseEntity.Response;
import com.pearl.service.LoginService;
import com.pearl.utils.db.PrimeDB;
import java.sql.Connection;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/idle")
public class LoginController {
// 我的数据库连接类
  @Autowired
  private PrimeDB primeDB;
// service层,
  @Resource
  private LoginService loginService;

  /**
   * 登录
   */

  @JwtIgnore
  @PostMapping("/login")
  public Response<Map<String, Object>> login(@RequestBody UserLoginData userDto,
      HttpServletResponse response)
      throws Exception {
    try (Connection conn = primeDB.create()) {
      Map<String, Object> map = loginService.login(conn, userDto, response);
      return new Response<>(0, map, "登录成功");
    } catch (Exception e) {
      return new Response<>(1, e.getMessage());
    }
  }

  @JwtIgnore
  @GetMapping("/verify")
  public Response<Boolean> verify(@RequestParam("token") String token) {
    try {
      return new Response<>(0, loginService.verify(token), "验证成功!");
    } catch (Exception e) {
      return new Response<>(1, false, "验证失败");
    }
  }
}

🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾
其中,验证接口的逻辑很简单,单纯调用JWT工具类进行判断即可,而登录方法根据不同的项目,可能各有区别,因此登录逻辑给出来只有参考意义.如下是loginService代码:

import com.alibaba.fastjson.JSONObject;
import com.pearl.db.UserDao;
import com.pearl.entitys.beans.UserLoginData;
import com.pearl.entitys.beans.UserToken;
import com.pearl.entitys.dataBaseTable.User;
import com.pearl.utils.AesUtil;
import com.pearl.utils.AssertUtils;
import com.pearl.utils.JwtTokenUtil;
import java.sql.Connection;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

@Service
public class LoginService {

  public Map<String, Object> login(Connection conn, UserLoginData userLoginData) throws Exception {
    try {
      Map<String, Object> map = new HashMap<>();
      /**
       * 校验账号
       * */
      UserDao userDao = new UserDao(conn);
      AssertUtils.notNull(userLoginData, "请求参数不能为空!");
      AssertUtils.isError(StringUtils.isEmpty(userLoginData.getAccount()), "账号不能为空!");
      AssertUtils.isError(StringUtils.isEmpty(userLoginData.getPassword()), "密码不能为空!");
      User user = userDao.selectbyUserId(userLoginData.getAccount());
      AssertUtils.notNull(user, "该账号不存在!");
      // 判断账号是否失效
      AssertUtils.isError(user.getStatus() != 1, "账号:" + user.getUserId() + "已失效!请联系管理员恢复!");
      // 验证账密
      Boolean isTruePass = new AuthService()
          .checkPassword(userLoginData.getPassword(), user.getPassword(), user.getSalt());
      AssertUtils.isError(!isTruePass, "用户名或密码错误!");
      //TODO 获取用户权限
      
      UserToken userToken = new UserToken();
      BeanUtils.copyProperties(user, userToken);
      String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken));
      map.put("user", user);
      map.put("token", token);
      return map;
    } catch (Exception e) {
      throw new Exception(e.getMessage());
    }
  }

  public Boolean verify(String token) throws Exception {
    try {
      return JwtTokenUtil.verify(token);
    } catch (Exception e) {
      throw new Exception(e.getMessage());
    }
  }
}

🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上我有封装响应实体,我的响应实体代码如下:

public class Response<T> {

  public Header header;

  public T value;

  public Response() {
  }

  public Response(T value) {
    this.header = new Header();
    this.value = value;
  }

  public Response(int code, Exception ex) {
    if (ex.getMessage() == null) {
      this.header = new Header(code, ex.toString());
    } else {
      this.header = new Header(code, ex.getMessage());
    }
    this.value = null;
  }

  public Response(int code, String message) {
    this.header = new Header(code, message);
    this.value = null;
  }

  public Response(int code, T value, Exception ex) {
    if (ex.getMessage() == null) {
      this.header = new Header(code, ex.toString());
    } else {
      this.header = new Header(code, ex.getMessage());
    }
    this.value = value;
  }

  public Response(int code, T value, String message) {
    this.header = new Header(code, message);
    this.value = value;
  }

  // 请求头,包含响应码和响应提醒信息
  public static class Header {

    public int code;

    public String message;

    public Header() {
      this.code = 0;
      this.message = "";
    }

    public Header(int code, String message) {
      this.code = code;
      this.message = message;
    }
  }
}

如上我的调用登录数据结构如下:

{
    "header": {
        "code": 0,
        "message": "登录成功"
    },
    "value": {
        "user": {
            "userId": "admin",
            "avatar": null,
            "userName": "超级管理员",
            "password": "t3zluLHlyip9A8TcXrR05Q==",
            "email": null,
            "phone": null,
            "sex": null,
            "age": 0,
            "status": 1,
            "createTime": "2024-04-07 08:11:43",
            "updateTime": "2024-04-07 08:11:43"
        },
        "token": "Bearer xxx"
    }
}

因此前端可获取token数据,进行赋值设置
🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲

结语

以上,为vue+springboot实现JWT登录验证过程

相关推荐

  1. gin使用jwt登录验证

    2024-04-09 22:18:05       38 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-09 22:18:05       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-09 22:18:05       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-09 22:18:05       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-09 22:18:05       18 阅读

热门阅读

  1. 热更新框架2-能够使用框架进行开发

    2024-04-09 22:18:05       12 阅读
  2. Redis是单线程,但为什么快

    2024-04-09 22:18:05       12 阅读
  3. vue-pdf只显示一页问题解决

    2024-04-09 22:18:05       15 阅读
  4. 数据驱动决策的秘密武器:一探FineBI的核心功能

    2024-04-09 22:18:05       14 阅读
  5. 边界框转化

    2024-04-09 22:18:05       12 阅读
  6. Istio-learning-note-about-Traffic Shifting(三)

    2024-04-09 22:18:05       14 阅读