vue+springboot实现JWT登录验证

news2024/11/27 5:27:21

目录

  • 前言
    • 概念
    • 实际演示
        • 路由信息
        • 初始访问登录界面
        • 登录验证
        • 验证过期
    • vue实现
      • 依赖引入
      • main.js
      • 获取和设置token工具类
      • 登录方法
        • 实体
        • 登录方法
        • axios请求
      • router配置
    • springboot实现
      • 依赖引入
      • JWT工具类
      • 忽视jwt验证注解
      • 拦截器逻辑
      • 跨域&调用拦截器配置
      • 登录接口&验证token接口
  • 结语

前言

最近在研究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登录验证过程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1579821.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法打卡day36|动态规划篇04| 01背包理论基础、416. 分割等和子集

目录 01背包理论基础 01背包问题描述 01背包解法 二维数组 一维数组 算法题 Leetcode 416. 分割等和子集 个人思路 解法 动态规划 01背包理论基础 不同的背包种类&#xff0c;虽然有那么多中南背包&#xff0c;但其中01背包和完全背包是重中之重&#xff1b; 01背包问…

flutter多入口点entrypoint

native中引擎对象本身消耗内存(每个引擎对象约莫消耗42MB内存) 多引擎&#xff1a;native多引擎>启动>flutter多入口点entrypoint>多main函数>多子包元素集>多(子)程序 单引擎(复用)&#xff1a;native单引擎>复用启动>flutter多入口点entrypoint>多m…

六、企业级架构缓存篇之memcached

一、memcached概述 1、网站架构优化流程&#xff1a; LNMP架构中网站应用访问流程&#xff1a; 浏览器 (app) → web 服务器 → 后端服务 (php) → 数据库 (mysql) 访问流程越多&#xff0c;访问速度越慢&#xff0c;出现问题的几率也越大。 网站访问流程优化思路&#xff1…

HarmonyOS 开发-PixelMap深拷贝案例

介绍 在图片开发过程中经常会涉及到PixelMap的深拷贝&#xff0c;本例通过使用PixelMap的readPixelsToBuffer方法来实现深拷贝。在创建源PixelMap的时候&#xff0c;需要将解码参数设置为BGRA_8888&#xff0c;而在深拷贝创建目标PixelMap的时候需要将解码参数设置为RGBA_8888…

首发!澳门地区OSGB数据V0.2版免费分享

之前分享了澳门OSGB V0.1版数据(独家&#xff0c;澳门地区OSGB数据免费分享&#xff01;)&#xff0c;得到了众多读者的认同&#xff0c;也有读者在数据使用过程中发现的问题&#xff0c;向我进行了反馈与交流&#xff0c;基于此&#xff0c;我在V0.1版本的基础上进行了修正与更…

vue3+Ts+Ant Design Vue +天地图组件封装

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来Vue3TsAntDesign-Vue组件天地图组件的封装 示例图 首先,在index.html引入天地图资源,vue3选择v4版本 <script src"http://api.tianditu.gov.cn/api?…

JVM 内存溢出排查

说明&#xff1a;记录一次JVM内存溢出的排查过程&#xff1b; 场景 项目开发完成后&#xff0c;首次提交到测试环境。测试、产品同事反馈页面先是操作响应慢&#xff0c;抛出超时异常&#xff0c;最后直接无法使用。查看日志后得知是内存溢出。 重启服务后&#xff0c;我对前…

2024,还没掌握 JS 发布者、订阅者模式吗

面试中经常出现问到如何实现JS 发布者、订阅者模式。下面是ES5实现发布订阅模式。 1、直接上代码。 function EventEmitter() {this.events {}; }; // 订阅者 EventEmitter.prototype.on function(ename, callback) {if (!this.events[ename]) {// 初始化创建订阅&#xff…

算法训练营第二十一天(二叉树part7)

算法训练营第二十一天&#xff08;二叉树part7&#xff09; 530.二叉搜索树的最小绝对差 力扣题目链接(opens new window) 题目 给你一棵所有节点为非负值的二叉搜索树&#xff0c;请你计算树中任意两节点的差的绝对值的最小值。 示例&#xff1a; 提示&#xff1a;树中至…

9.枚举类与注解

文章目录 1. 枚举类1.1 自定义枚举类1.2 enum关键字定义枚举类1.3 Enum类主要方法 2. 注解2.1 注解作用2.1.1 生成文档相关注解2.1.2 JDK内置基本注解-在编译时进行格式检查2.1.3 跟踪代码依赖性&#xff0c;实现替代配置文件功能 2.2 自定义Annotation2.3 JDK中的元注解2.3.1 …

Python零基础从小白打怪升级中~~~~~~~流程控制语句

第三节&#xff1a;Python的流程控制语法 一、Python条件语句的语法 if 条件1:条件1成立执⾏的代码一条件1成⽴执⾏的代码二...... elif 条件2&#xff1a;条件2成立执⾏的代码三条件2成立执⾏的代码四...... ...... else:以上条件都不成⽴&#xff0c;执行的代码五以上条件都…

海外盲盒系统开发,盲盒出口成为企业新机遇

随着盲盒的兴起&#xff0c;国内消费市场形成了万物皆可盲盒的态势。并且&#xff0c;盲盒自带社交属性&#xff0c;也成为了年轻人的社交神器。 除了在国内&#xff0c;盲盒在海外也掀起了一股热潮&#xff0c;呈现出了爆发式增长形势&#xff0c;国内热门盲盒企业也出口到了…

Redis的三种部署方案

文章目录 单机模式主从复制哨兵模式分片集群 在Redis中提供的集群方案总共有三种&#xff1a;单机模式&#xff0c;主从复制集群、哨兵模式&#xff0c;Redis分片集群 单机模式 Redis 只运行在一台服务器上&#xff0c;并且所有的数据都存储在这一台服务器的内存中。 主从复制…

小程序商城免费搭建之java商城 电子商务Spring Cloud+Spring Boot+二次开发+mybatis+MQ+VR全景

1. 涉及平台 平台管理、商家端&#xff08;PC端、手机端&#xff09;、买家平台&#xff08;H5/公众号、小程序、APP端&#xff08;IOS/Android&#xff09;、微服务平台&#xff08;业务服务&#xff09; 2. 核心架构 Spring Cloud、Spring Boot、Mybatis、Redis 3. 前端框架…

黄金白银价格上涨是投资机会吗?

黄金和白银&#xff0c;作为历史悠久的贵重金属&#xff0c;一直以来都被投资者视为避险资产。近年来&#xff0c;随着全球经济环境的变动&#xff0c;我们观察到黄金与白银的价格不断攀升&#xff0c;这是否预示着投资机会的到来&#xff1f;今天&#xff0c;就让我们来深度探…

【保姆级讲解SQL Server的详细使用教程】

&#x1f308;个人主页: 程序员不想敲代码啊 &#x1f3c6;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f44d;点赞⭐评论⭐收藏 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共…

ATG-2021B功率信号源可以应用在哪些方面

功率信号源是一种能够产生一定功率的信号源&#xff0c;广泛应用于各个领域。下面将介绍功率信号源在电子、通信、工业和科研等方面的应用。 在电子行业中&#xff0c;功率信号源是一种重要的测试工具。它可以产生各种波形的信号&#xff0c;如正弦波、方波、脉冲波等&#xff…

vue中实现表格的局部刷新

背景&#xff1a; 列表中展示所有审核任务信息&#xff0c;包括审核状态、审核进度等&#xff0c;原来的实现是【查询】按钮绑定了一个定时器&#xff0c;定时查询整个列表&#xff0c;但是需要用户手动开启 领导觉得这种方式用户体验不好&#xff0c;希望能够实现【审核进度…

Golang | Leetcode Golang题解之第16题最接近的三数之和

题目&#xff1a; 题解&#xff1a; func threeSumClosest(nums []int, target int) int {sort.Ints(nums)var (n len(nums)best math.MaxInt32)// 根据差值的绝对值来更新答案update : func(cur int) {if abs(cur - target) < abs(best - target) {best cur}}// 枚举 a…

人工智能支持节能的七种方式

2024年2月&#xff0c;OpenAI公布了名为“Sora”的人工智能模型。从去年的chatgpt到今天Sora发布&#xff0c;OpenAI的每⼀次重要的动作和发布&#xff0c;都伴随着⼀场场精妙绝伦精巧的事件营销&#xff0c;它的设置议题的能⼒&#xff0c;节奏控制&#xff0c;公众引导堪称创…