Spring security之JWT

JWT

这里写目录标题

  • JWT
  • 一级目录
    • 二级目录
      • 三级目录
      • 1.什么是JWT
    • 2.JWT的组成部分
    • 3.编码/解码
    • 4.特点
    • 5. 为什么使用JWT
      • 5.1传统的验证方式
    • 5.2基于JWT的验证方式
    • 6.JWT进行登录验证
      • 6.1依赖安装
      • 6.2编写UserDetailServiceImpl类
      • 6.3编写UserDetailsImpl类
      • 6.4 实现config.SecurityConfig类
      • 6.5实现utils.JwtUtil类
      • 6.6实现config.filter.JwtAuthenticationTokenFilter类
      • 6.7配置config.SecurityConfig类
    • 7.常见的面试题

一级目录

二级目录

三级目录

1.什么是JWT

  • 全称 JSON Web Token,简称JWT,是一个基于RFC7519的爱看i发数据标准,定义了一种宽松且紧凑的数据组合方式
  • 是一种加密后的数据载体,可在各应用之间进行数据传输
  • 最常应用于授权认证,一旦用户登录,每个请求都将包含JWT,系统每次处理用户请求都会及逆行JWT安全检验,通过之后在进行处理

2.JWT的组成部分

JWT由3部分组成,使用.拼接

  • Header

    • 声明类型,默认JWT
    • 声明加密算法,HMAC,RSA,ECDSA等常用算法
    {
    "alg": "HS256",
    "typ": "JWT"
    }
    
    • alg:表示签名的算法,默认HMAC SHA256(写成HS256)
    • type:表示声明类型,默认JWT

    使用Base64编码后构成JWT,的第一部分

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    
  • Payload

    • 存放有效信息的地方

    标准载荷

    建议但不强制使用

    标准载荷介绍
    iss (issuer)签发人(谁签发的)
    exp (expiration time)过期时间,必须要大于签发时间
    sub (subject)主题(用来做什么)
    aud (audience)受众(给谁用的)比如:http://www.xxx.com
    nbf (Not Before)生效时间
    iat (Issued At)签发时间
    jti (JWT ID)编号,JWT 的唯一身份标识

    自定义载荷

    可以添加任何信息,一般添加用户的相关信息或业务所需要的必要信息。但不建议添加敏感信息

    {//第一部分用户信息"user_info": [{"id": "1"//id},{"name": "dafei"//姓名},{"age": "18"//年龄}],"iat": 1681571257,//签发时间"exp": 1682783999,//过期时间"aud": "xiaofei",//受众"iss": "dafei",//签发人"sub": "alluser"//主题
    }
    

    使用Base64编码后构成了第二部分

    eyJ1c2VyX2luZm8iOlt7ImlkIjoiMSJ9LHsibmFtZSI6ImRhZmVpIn0seyJhZ2UiOiIxOCJ9XSwiaWF0IjoxNjgxNTcxMjU3LCJleHAiOjE2ODI3ODM5OTksImF1ZCI6InhpYW9mZWkiLCJpc3MiOiJkYWZlaSIsInN1YiI6ImFsbHVzZXIifQ
    
  • signature

    • 私钥:只有服务器才知道
    • 公钥:按照以下算法产生
    signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
    

    用Base64编码,构成第三部分

    将三部分拼接成一个字符串,便是jwt了

由于密钥的存在,即使调用方修改了前两部分内容,在验证不放呢会出现签名不一致的情况,保证了全性

3.编码/解码

编码工具

https://tooltt.com/jwt-encode/

解码工具

https://tool.box3.cn/jwt.html

4.特点

优点

  • 无状态

    JWT 不需要在服务端存储任何状态,客户端可以携带 JWT 来访问服务端,从而使服务端变得无状态。这样,服务端就可以更轻松地实现扩展和负载均衡。

  • 自定义

    JWT 的载荷部分可以自定义,可以存储任何 JSON 格式的数据。这意味着我们可以使用 JWT 来实现一些自定义的功能,例如存储用户喜好、配置信息等等。

  • 扩展性强

    JWT 有一套标准规范,因此很容易在不同平台和语言之间共享和解析。此外,开发人员可以根据需要自定义声明(claims)来实现更加灵活的功能。

  • 调试性好

    由于 JWT 的内容是以 Base64 编码后的字符串形式存在的,因此非常容易进行调试和分析。

缺点

  • 安全性取决于密钥管理

    JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。因此,在使用 JWT 时,一定要注意密钥的管理,包括生成、存储、更新、分发等等。

  • 无法撤销

    由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。如果用户在使用 JWT 认证期间被注销或禁用,那么服务端就无法阻止该用户继续使用之前签发的 JWT。因此,开发人员需要设计额外的机制来撤销 JWT,例如使用黑名单或者设置短期有效期等等。

  • 需要缓存到客户端

    由于 JWT 包含了用户信息和授权信息,一般需要客户端缓存,这意味着 JWT 有被窃取的风险。

  • 载荷大小有限制

    由于 JWT 需要传输到客户端,因此载荷大小也有限制。一般不建议载荷超过 1KB,会影响性能。

5. 为什么使用JWT

5.1传统的验证方式

  • 传统的认证方式,是基于Session的认证

Cookie和Session

  • Cookie:一种让程序员在本地存储数据的能力,让数据在客户端这边更持久化
  • Cookie里面存的是键值对的格式数据,键值对用“;”分割,键和值之间用“,”分割

登录的认证

Session

  • session是在服务器的一种机制,因为cookie是客户端保存的数据,而这些数据又是跟用户强烈相关联的,显然保存在客户端这边就不太合适(太多,也占资源),所以把数据都保存在服务器这边就比较的合适;保存的方式就是通过session的方式来进行保存的。键值对结构

  • 使用之后,客户端只需要保存 sessionId就可以了,后续的请求带上 sessionId,服务器就会根据 sessionId 就会找到对应的用户数据详细的信息。

认证流程

image-20230808162008667

  1. 客户端:发起一个认证请求
  2. 服务器端:认证通过存储一个session的用户信息;把sessionid写入客户端cookie
  3. 客户端再次访问:自动携带sessionid,找到对应session

问题分析

  • 通常来说session保存在服务器的内存中,随着认证用户增多,会加大服务器的开销

  • 因为session保存在服务器内存中,所以对于分布式应用来说,限制了应用的扩展能力

  • cookie被截取,会容易受造跨站请求伪造攻击

  • 在前后端分离的系统中更加痛苦,比如后端是集群分布,需要设计共享机制、

5.2基于JWT的验证方式

image-20230808165551764

  1. 客户端: 发起一个请求认证,带有用户名,密码
  2. 服务器:认证通过,生成JWT,响应给客户端
  3. 客户端:将Token保存到本地

日后客户端再次认证

  1. 客户端:携带JWT发送给服务器
  2. 服务器:进行验证,通过,展示数据;否则返回错误信息

6.JWT进行登录验证

6.1依赖安装

  • spring-boot-starter-security
  • jjwt-api
  • jjwt-impl
  • jjwt-jackson

meaven仓库地址:Maven Repository: Search/Browse/Explore (mvnrepository.com)

6.2编写UserDetailServiceImpl类

  • 继承UserDetailsService接口
  • 作用:接入数据库信息,主要是用来根据用户名查找用户
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {QueryWrapper<User> queryWrapper=new QueryWrapper<>();queryWrapper.eq("username",username);User user=userMapper.selectOne(queryWrapper);if(user==null){throw new RuntimeException("用户不存在");}return new UserDetailsImpl(user);}
}

6.3编写UserDetailsImpl类

  • 使用户的一个接口类,用来判断用户的密码,权限是否失效,过期。
package com.kob.backend.service.impl.utils;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;
import  com.kob.backend.pojo.User;@Data
@AllArgsConstructor
@NoArgsConstructorpublic class UserDetailsImpl implements UserDetails {private User user;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return null;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

6.4 实现config.SecurityConfig类

  • 用来实现用户密码的加密存储
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

6.5实现utils.JwtUtil类

  • 是jwt工具类的一种
  • 作用:用来创建,解析jwt token
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;@Component
public class JwtUtil {public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14;  // 有效期14天public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac";public static String getUUID() {return UUID.randomUUID().toString().replaceAll("-", "");}public static String createJWT(String subject) {JwtBuilder builder = getJwtBuilder(subject, null, getUUID());return builder.compact();}private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;SecretKey secretKey = generalKey();long nowMillis = System.currentTimeMillis();Date now = new Date(nowMillis);if (ttlMillis == null) {ttlMillis = JwtUtil.JWT_TTL;}long expMillis = nowMillis + ttlMillis;Date expDate = new Date(expMillis);return Jwts.builder().setId(uuid).setSubject(subject).setIssuer("sg").setIssuedAt(now).signWith(signatureAlgorithm, secretKey).setExpiration(expDate);}public static SecretKey generalKey() {byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256");}public static Claims parseJWT(String jwt) throws Exception {SecretKey secretKey = generalKey();return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(jwt).getBody();}
}

6.6实现config.filter.JwtAuthenticationTokenFilter类

  • 进行jwt token的验证,如果成功,将用户信息注入上下文中
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate UserMapper userMapper;@Overrideprotected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("Authorization");if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) {filterChain.doFilter(request, response);return;}token = token.substring(7);String userid;try {Claims claims = JwtUtil.parseJWT(token);userid = claims.getSubject();} catch (Exception e) {throw new RuntimeException(e);}User user = userMapper.selectById(Integer.parseInt(userid));if (user == null) {throw new RuntimeException("用户名未登录");}UserDetailsImpl loginUser = new UserDetailsImpl(user);UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request, response);}
}

6.7配置config.SecurityConfig类

  • 放行登录,注册页面
import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()//在这里方形的登录注册页面,填写访问地址.antMatchers("/user/account/token/", "/user/account/register/").permitAll().antMatchers(HttpMethod.OPTIONS).permitAll().anyRequest().authenticated();http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}
}

7.常见的面试题

问:什么是JWT?解释一下它的结构。

JWT是一种开放标准,用于在网络上安全地传输信息。它由三部分组成:头部、载荷和签名。头部包含令牌的元数据,载荷包含实际的信息(例如用户ID、角色等),签名用于验证令牌是否被篡改。

问:JWT的优点是什么?它与传统的session-based身份验证相比有什么优缺点?

JWT的优点包括无状态、可扩展、跨语言、易于实现和良好的安全性。相比之下,传统的session-based身份验证需要在服务端维护会话状态,使得服务端的负载更高,并且不适用于分布式系统。

问:在JWT的结构中,分别有哪些部分?每个部分的作用是什么?

JWT的结构由三部分组成:头部、载荷和签名。头部包含令牌类型和算法,载荷包含实际的信息,签名由头部、载荷和密钥生成。

问:JWT如何工作?从开始到验证过程的完整流程是怎样的?

JWT的工作流程分为三个步骤:生成令牌、发送令牌、验证令牌。在生成令牌时,服务端使用密钥对头部和载荷进行签名。在发送令牌时,将令牌发送给客户端。在验证令牌时,客户端从令牌中解析出头部和载荷,并使用相同的密钥验证签名。

问:什么是JWT的签名?为什么需要对JWT进行签名?如何验证JWT的签名?

JWT的签名是由头部、载荷和密钥生成的,用于验证令牌是否被篡改。签名使用HMAC算法或RSA算法生成。在验证JWT的签名时,客户端使用相同的密钥和算法生成签名,并将生成的签名与令牌中的签名进行比较。

问:什么是JWT的令牌刷新?为什么需要这个功能?

令牌刷新是一种机制,用于解决JWT过期后需要重新登录的问题。在令牌刷新中,服务端生成新的JWT,并将其发送给客户端。客户端使用新的JWT替换旧的JWT,从而延长令牌的有效期。

问:JWT是否加密?如果是,加密的部分是哪些?如果不是,那么它如何保证数据安全性?

JWT本身并不加密,但可以在载荷中包含敏感信息。为了保护这些信息,可以使用JWE(JSON Web Encryption)对载荷进行加密。如果不加密,则需要在生成JWT时确保不在载荷中包含敏感信息。

问:在JWT中,如何处理Token过期的问题?有哪些方法可以处理?

JWT过期后,客户端需要重新获取新的JWT。可以通过在JWT中包含过期时间或使用refresh token等机制来解决过期问题。

问:JWT和OAuth2有什么关系?它们之间有什么区别?

JWT和OAuth2都是用于身份验证和授权的开放标准。JWT是一种身份验证机制,而OAuth2是一种授权机制。JWT用于在不同的系统中安全地传输信息,OAuth2用于授权第三方应用程序访问受保护的资源。

问:JWT在什么场景下使用较为合适?它的局限性是什么?

JWT在单体应用或微服务架构中的使用比较合适。它的局限性包括无法撤销、令牌较大、无法处理并发等问题。在需要针对每次请求进行访问控制或需要撤销令牌的情况下,JWT可能不是最佳选择。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

redis分布式集群-redis+keepalived+ haproxy

redis分布式集群架构&#xff08;RedisKeepalivedHaproxy&#xff09;至少需要3台服务器、6个节点&#xff0c;一台服务器2个节点。 redis分布式集群架构中的每台服务器都使用六个端口来实现多路复用&#xff0c;最终实现主从热备、负载均衡、秒级切换的目标。 redis分布式集…

服务器安装Tomcat

下载Tomcat 下载地址在这&#xff1a; Tomcat官网 下载完成以后把压缩包上传到服务器中&#xff08;我传到了www/java&#xff09;,进行解压(解压到)&#xff0c;如果没有进行指定解压到哪里&#xff0c;默认是到root文件夹中 tar -zxvf /www/java/apache-tomcat-9.0.103.tar.…

《格斗之王AI》使用指南

目录 一、说明 二、步骤 1. 下载 2.配置环境 3.替换 4.测试 5.训练 一、说明 该项目是 针对B站UP主 林亦LYi 的作品 格斗之王&#xff01;AI写出来的AI竟然这么强&#xff01;的使用指南&#xff0c;目的是在帮助更多小白轻松入门&#xff0c;一起感受AI的魅力。 林亦LYi…

如何定位线上CPU飙高的问题

1.问题情景 我们的接口卡死&#xff0c;CPU飙高到打不开的网页 2.问题定位 2.1 top指令 通过top命令找到CPU耗用最厉害的那个进程的PID 直接输入top Linux下的100%代表一个核心&#xff0c;如果是八核&#xff0c;最高可以到800%&#xff0c;这样才算满 然后通过PID找到CP…

微服务——es数据聚合+RestClient实现聚合

数据聚合 聚合的种类 DSL实现Bucket聚合 如图所示&#xff0c;设置了10个桶&#xff0c;那么就显示了数量最多的前10个桶&#xff0c;品牌含有7天酒店的有30家&#xff0c; 品牌含有如家的也有30家。 修改排序规则 限定聚合范围 DSL实现Metrics聚合 如下案例要求对不同的品…

EVE-NG 隐藏没有镜像的模板

eve-ng 默认情况下&#xff0c;在添加node时&#xff0c;会列出所有的模板&#xff0c;这样用着很不方便。 通过以下方式&#xff0c;可以使没有设备的模板不可见 cp /opt/unetlab/html/includes/config.php.distribution /opt/unetlab/html/includes/config.php 打开 config…

遍历集合List的五种方法以及如何在遍历集合过程中安全移除元素

一、遍历集合List的五种方法 测试数据 List<String> list new ArrayList<>(); list.add("A");list.add("B");list.add("C");1. 普通for循环 普通for循环&#xff0c;通过索引遍历 for (int i 0; i < list.size(); i) {Syst…

React实现点击切换组件

实现如上组件 组件代码&#xff1a; import { SwapOutlined } from "ant-design/icons" import React, { useState } from "react" import ./index.lessinterface ISwitchTypeProps {onChange?: (val) > booleanactiveKey?: stringleft: { key: str…

路由器和交换机的区别

交换机和路由器的区别 交换机实现局域网内点对点通信&#xff0c;路由器实现收集发散&#xff0c;相当于一个猎头实现的中介的功能 路由器属于网络层&#xff0c;可以处理TCP/IP协议&#xff0c;通过IP地址寻址&#xff1b;交换机属于中继层&#xff0c;通过MAC地址寻址(列表)…

Fiddler模拟请求发送和修改响应数据

fiddler模拟伪造请求 方法一&#xff1a;打断点模拟HTTP请求 1、浏览器页面填好内容后&#xff08;不要操作提交&#xff09;&#xff0c;打开fiddler&#xff0c;设置请求前断点&#xff0c;点击菜单fiddler,”Rules”\”Automatic Breakpoints”\”Before Requests” 2、在…

实验二十七、电压传输特性的测量

一、题目 利用 Multisim 测试图1 所示各电路的电压传输特性。 图 1 电压比较器 图1\,\,电压比较器 图1电压比较器 二、仿真注意事项 &#xff08;1&#xff09;仿真电路所有的 A \textrm A A 均采用虚拟电压比较器。合理选择稳压管的限流电阻&#xff0c;使其既稳压又不至于…

JavaScript应用:五子棋游戏实战开发

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;全栈领域新星创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责…

Python Selenium 设置带账号密码的socks5代理,启动浏览器

selenium添加带有账密的socks5代理 我们都知道在使用selenium开发爬虫的时候不可避免的会使用socks5高匿名代理。一般情况下我们使用方法如下(开发语言为python)&#xff1a; from selenium import webdriver chrome_options webdriver.ChromeOptions() chrome_options.add_…

【探索SpringCloud】服务发现-Nacos使用

前言 在聊服务注册中心时&#xff0c;便提到了Nacos。这次便来认识一下。当然&#xff0c;这自然没有官方介绍那般详尽&#xff0c;权当是学习了解Nacos原理的一个过程吧。 Nacos简介 Nacos&#xff0c;全名&#xff1a;dynamic Naming And Configuration Service. 而这个名…

PHP智能人才招聘网站mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP智能人才招聘网站 是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 下载地址 https://download.csdn.net/download/qq_41221322/88199392 视频演示 PH…

【MAC】 M2 brew安装 docker 运行失败 解决

MAC 安装 brew install --cask docker 之后一直显示docker: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?. 网上看了一些文章 发现 这个不适用于M2 所以要从官网上下载 docker 安装成功

多线程与并发编程面试题总结

多线程与并发编程 多线程 线程和进程的区别&#xff1f; 从操作系统层面上来讲&#xff1a;进程(process)在计算机里有单独的地址空间&#xff0c;而线程只有单独的堆栈和局部内存空间&#xff0c;线程之间是共享地址空间的&#xff0c;正是由于这个特性&#xff0c;对于同…

产品进行逻辑梳理的三个重点

我们在进行产品方案设计时&#xff0c;需要进行逻辑梳理&#xff0c;这样才能保障产品方案的严谨性&#xff0c;降低后期返工几率。如果我们在接到需求时&#xff0c;直接开始画原型&#xff0c;没有经过逻辑梳理&#xff0c;往往容易造成产品方案纰漏丛生&#xff0c;直接影响…

成集云 | 用友U8采购请购单同步钉钉 | 解决方案

源系统成集云目标系统 方案介绍 用友U8是中国用友集团开发和推出的一款企业级管理软件产品。具有丰富的功能模块&#xff0c;包括财务管理、采购管理、销售管理、库存管理、生产管理、人力资源管理、客户关系管理等&#xff0c;可根据企业的需求选择相应的模块进行集…

【C++入门到精通】C++入门 —— vector (STL)

阅读导航 前言一、vector简介1. 概念2. 特点 二、vector的使用1.vector 构造函数2. vector 空间增长问题⭕resize 和 reserve 函数 3. vector 增删查改⭕operator[] 函数 三、迭代器失效温馨提示 前言 前面我们讲了C语言的基础知识&#xff0c;也了解了一些数据结构&#xff0…