当前位置: 首页 > news >正文

springboot+satoken实现刷新token(值变化)

欢迎来到我的博客,代码的世界里,每一行都是一个故事


在这里插入图片描述

🎏:你只管努力,剩下的交给时间

🏠 :小破站

springboot+satoken实现刷新token

    • satoken是什么?支持什么?
    • 为什么需要?
    • token一直不变存在的问题
      • 1. 安全风险增加
      • 2. 难以撤销 Token
      • 3. 权限滥用和过期信息的风险
      • 4. 缺乏会话管理
      • 5. 影响用户隐私
      • 6. 无法确保设备和网络变化
      • 7. 用户体验不佳
    • 逻辑+代码实现
      • 代码实现
    • 拦截器知识补充
      • 1. 注册顺序决定执行顺序
      • 2. 拦截器方法的执行顺序
      • 3. 优先级控制
      • 总结

satoken是什么?支持什么?

satoken官网

借用官网的一句话, Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证权限认证单点登录OAuth2.0分布式Session会话微服务网关鉴权 等一系列权限相关问题。
这里我们只说明刷新token,也就是前后端分离的场景中常见的一种方案。

为什么需要?

在satoken中存在两个token,一个是真正的token有效期,还有一个是活跃token,也就是说当活跃token过期的时候是不能访问服务的,需要调用相关方法解除。而当正真的token过期的时候就需要登录重新获取凭证。

所以其实根据上面的表述也是可以实现刷新token的
前端后端约定,当后端因为活跃token过期返回给前端响应的状态值,前端拦截并重新调用相关方法,这样也是可以实现刷新token的。

token一直不变存在的问题

Token 长期不变或过期时间过长会带来一系列安全和用户体验方面的问题。以下是一些主要的风险和潜在问题:

1. 安全风险增加

  • Token 被截获的风险:如果攻击者通过某种方式截获了用户的 Token,那么在 Token 长期不变或过期时间过长的情况下,攻击者可以持续使用该 Token 访问用户的账号,而用户不会意识到。这极大增加了账号被恶意使用的风险。
  • 无法控制 Token 失效:在 Token 长期有效的情况下,即使用户想要主动注销或更改密码,攻击者手中的旧 Token 仍然有效,导致安全威胁无法解除。
  • 无法防御会话劫持:当用户登录后 Token 长期不变,一旦发生会话劫持,攻击者可以一直利用该 Token 冒充用户进行操作,直到 Token 失效。

2. 难以撤销 Token

  • Token 黑名单机制的缺乏:当 Token 的有效期非常长时,后端很难立即撤销某个 Token。即使用户账号被禁用或者注销,旧的 Token 可能依然可以继续访问系统,直到 Token 到期。
  • 缺乏灵活性:Token 如果是长期有效的,即便用户被强制下线,无法简单地让旧 Token 失效,除非重新设计 Token 管理系统。

3. 权限滥用和过期信息的风险

  • 权限变化后无法及时更新:当 Token 长时间有效,而用户的权限发生了变化(例如角色升级、降级或权限被撤销),旧 Token 可能依然具有不该有的权限。这可能导致用户获得不适当的访问权限。
  • 过期数据风险:如果用户 Token 长期不更新,系统可能无法捕捉到最新的用户状态变更,比如权限、角色、信息等,导致系统提供了不正确的访问权限或内容。

4. 缺乏会话管理

  • 无法追踪用户的活跃度:长期有效的 Token 会让系统无法准确跟踪用户的登录会话。系统无法判断用户是否活跃,用户的最后访问时间也无法准确追踪。
  • 无法强制用户重新登录:如果 Token 过期时间过长,用户可能在极长时间内不需要重新登录,丧失了会话管理的能力。对于需要更高安全性的场景,强制用户定期登录是必要的。

5. 影响用户隐私

  • 隐私泄露风险增加:Token 长期有效可能使得用户的个人信息在长时间内暴露于潜在的攻击面,增加了隐私泄露的风险。如果用户长期未使用系统,Token 应该过期以保护用户隐私。
  • 设备共享中的风险:如果用户在共享设备上登录,而 Token 长期不失效,其他人可以轻松访问用户账户,特别是在用户忘记登出或清理浏览器时。

6. 无法确保设备和网络变化

  • IP地址、设备等环境因素没有变化:一些 Token 通常会包含用户设备、IP 地址等信息来防止 Token 被滥用。如果 Token 长期不变,那么即使用户的网络环境发生了变化,系统也无法感知。这将导致 Token 在不可信的环境中继续使用,增加了安全风险。

7. 用户体验不佳

  • 无法提供个性化内容更新:长期不变的 Token 可能导致系统无法捕捉用户状态的实时变化,从而影响个性化内容推荐或提示用户更新信息的能力。
  • 会话管理不灵活:如果用户希望在不同设备上管理会话(如在一个设备上登出时使另一个设备上的 Token 失效),长期不变的 Token 可能无法支持这种场景。

逻辑+代码实现

可以采用双拦截器实现,第一个拦截器是自定义的,而这个拦截器总是返回true,第二个拦截器使用satoken的拦截器做一些登录或者权限的认证。

在这里插入图片描述

代码实现

package fun.acowbo.config;import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** @author <a href="https://acowbo.fun">acowbo</a>* @since 2024/8/27*/
@Slf4j
@Component
public class CustomInterceptor implements HandlerInterceptor{@Value("${sa-token.token-name}")private String tokenName;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {return true;}long tokenActivityTimeout = StpUtil.getTokenActivityTimeout();log.info("tokenActivityTimeout:{}", tokenActivityTimeout);long tokenTimeout = StpUtil.getTokenTimeout();if (tokenActivityTimeout > 0){response.setHeader(tokenName, StpUtil.getTokenValue());response.setHeader("Access-Control-Expose-Headers", tokenName);}if (tokenActivityTimeout < 0 && tokenTimeout > 0){// 首先要让token活跃StpUtil.updateLastActivityToNow();String loginId = (String) StpUtil.getLoginId();// 先退出,否则之前的token还能用StpUtil.logout(loginId);// 重新设置token,这里仅仅是为了安全,否则始终token是一个值StpUtil.login(loginId,new SaLoginModel().setToken(IdUtil.randomUUID()));// 请求头修改token的值,否则在第二个拦截器会报错,因为老的token已经失效了request.setAttribute(tokenName, StpUtil.getTokenValue());// 响应头设置值response.setHeader(tokenName, StpUtil.getTokenValue());response.setHeader("Access-Control-Expose-Headers", tokenName);}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {}
}
package fun.acowbo.config;import cn.dev33.satoken.interceptor.SaInterceptor;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.beans.factory.annotation.Value;
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;import javax.annotation.Resource;
import java.util.Arrays;/*** description: sa-token权限配置类** @author <a href="https://acowbo.fun">acowbo</a>* @since 2024/6/7 13:59*/
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {@Value("${excludePath}")private String excludePath;@Resourceprivate CustomInterceptor customInterceptor;@Overridepublic void addCorsMappings(CorsRegistry registry) {// 允许所有路径registry.addMapping("/**")// 允许所有来源.allowedOrigins("*")// 允许的 HTTP 方法.allowedMethods("GET", "POST", "PUT", "DELETE")// 允许的请求头.allowedHeaders("*")// 允许发送 Cookie.allowCredentials(false)// 预检请求的缓存时间.maxAge(3600);}// 注册拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。registry.addInterceptor(customInterceptor).addPathPatterns("/**").excludePathPatterns(Arrays.asList(excludePath.split(",")));registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())).addPathPatterns("/**").excludePathPatterns(Arrays.asList(excludePath.split(",")));}
}

拦截器知识补充

在 Spring 框架中,如果定义了多个拦截器,它们的执行顺序是根据它们的注册顺序决定的。具体的执行顺序可以通过以下规则来理解:

1. 注册顺序决定执行顺序

在 Spring 中,拦截器是通过 WebMvcConfigurer 接口中的 addInterceptors 方法进行注册的。多个拦截器会按照它们注册的先后顺序进行调用。

  • 拦截器的执行顺序(进入请求时):按照注册顺序依次调用,先注册的拦截器会先执行。
  • 拦截器的执行顺序(响应时):响应返回时,拦截器的执行顺序与请求时相反,最后注册的拦截器会最先执行。

示例

@Configuration
public class WebConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new FirstInterceptor()).addPathPatterns("/**");registry.addInterceptor(new SecondInterceptor()).addPathPatterns("/**");}
}

在这个例子中:

  • 进入请求时FirstInterceptor -> SecondInterceptor
  • 返回响应时SecondInterceptor -> FirstInterceptor

2. 拦截器方法的执行顺序

拦截器的核心方法有以下三个,它们的调用顺序也需要注意:

  • preHandle: 在请求处理之前执行。
  • postHandle: 在请求处理完后(但在视图渲染之前)执行。
  • afterCompletion: 在视图渲染完成后执行。

当有多个拦截器时:

  • preHandle 方法按照拦截器的注册顺序执行。
  • postHandleafterCompletion 方法则按照相反顺序执行。

3. 优先级控制

如果需要更精确地控制拦截器的顺序,除了按注册顺序,还可以借助其他方法:

  • 排序接口:通过实现 Ordered 接口或使用 @Order 注解,可以为拦截器明确指定顺序。
  • 配置拦截器链的顺序:通过配置文件明确指定拦截器的顺序,也可以使用 InterceptorRegistry 的 API 动态调整顺序。

总结

  • 拦截器的执行顺序取决于它们的注册顺序,先注册的拦截器先处理请求,后注册的拦截器先处理响应。
  • 可以使用 @Order 注解或实现 Ordered 接口来精确控制多个拦截器的执行顺序。

相关文章:

springboot+satoken实现刷新token(值变化)

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 &#x1f38f;&#xff1a;你只管努力&#xff0c;剩下的交给时间 &#x1f3e0; &#xff1a;小破站 springbootsatoken实现刷新token satoken是什么&#xff1f;支持什么&#xff1f;为什么需要&…...

63.HDMI显示器驱动设计与验证-彩条实验

&#xff08;1&#xff09;常见的视频传输接口有三种&#xff1a; VGA 接口、 DVI 接口和 HDMI 接口&#xff0c;目前的显示设备都配有这三种视频传输接口。三类视频接口的发展历程为 VGA→DVI→HDMI。其中 VGA 接口出现最早&#xff0c;只能传输模拟图像信号&#xff1b; 随后…...

安卓13设置删除网络和互联网选项 android13隐藏设置删除网络和互联网选项

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改4.1修改方法14.2修改方法25.编译6.彩蛋1.前言 有些客户不想让用户修改默认的网络配置,禁止用户进入里面调整网络相关的配置。 2.问题分析 像这个问题,我们有好几种方法去处理,这种需求一般…...

C++的6种构造函数

在 C 中&#xff0c;构造函数是一种特殊的成员函数&#xff0c;用于初始化类对象。在对象创建时自动调用&#xff0c;构造函数的主要作用是分配资源、初始化数据成员等。根据不同的功能和使用场景&#xff0c;C 提供了多种类型的构造函数&#xff1a; 1. 默认构造函数 (Defaul…...

【FE】NPM——概述

NPM基本使用 下载Node 老生常谈&#xff0c;选择LTS版本官网放这里&#xff1a;https://nodejs.cn/download/ 1.镜像配置&#xff1a;镜像源 镜像配置 依赖仓库&#xff1a;版本查看 //不确定仓库有哪些版本&#xff0c;列出指定包的所有版本 npm view <package-name&…...

Clipboard.js实现复制文本到剪贴板功能

一、Clipboard.js简介 Clipboard.js是一个轻量级的实现复制文本到剪贴板功能的JavaScript插件&#xff0c;该插件可以将输入框&#xff0c;文本域&#xff0c;DOM节点元素中的文本内容复制到剪贴板中。 官网地址&#xff1a;Clipboard.js 浏览器兼容性&#xff1a;兼容Chrome、…...

Harbor安装笔记

下载离线安装包 wget https://github.com/goharbor/harbor/releases/download/v2.11.1/harbor-offline-installer-v2.11.1.tgz 解压 tar -zxvf harbor-offline-installer-v2.11.1.tgz 复制一份配置文件出来&#xff0c;修改配置 cp harbor.yml.tmpl harbor.yml vim harbor…...

HTTP 1.0 2.0 3.0详解

HTTP HTTP全称超文本传输协议&#xff0c;是一种属于应用层的通信协议。它允许将超文本标记语言文档&#xff08;HTML&#xff09;从Web服务器传输到客户端的浏览器。 HTTP报文结构 请求报文结构 请求方法&#xff1a; GET&#xff1a;一般用来请求已被URI识别的资源&#x…...

Python操作TXT文本:从入门到精通

在数字化时代,文本处理成为了许多工作和项目的基础。Python作为一种强大且易学的编程语言,在文本处理方面展现出了无与伦比的优势。本文将通过举例的方式,向读者介绍如何使用Python来操作TXT文本,让您轻松掌握文本处理的精髓。 一、读取TXT文本内容 首先,我们需要学会如…...

开源 AI 智能名片 2+1 链动模式 S2B2C 商城小程序的数据运营策略与价值创造

一、引言 1.1 研究背景 在当今数字化时代&#xff0c;数据运营已成为企业发展的核心驱动力。开源 AI 智能名片 21 链动模式 S2B2C 商城小程序作为一种创新的营销工具&#xff0c;与数据运营紧密相连。该小程序通过集成人工智能、大数据分析等先进技术&#xff0c;能够实时收集…...

ip 地址查看cmd命令

ip 地址查看cmd命令 在不同的操作系统中&#xff0c;查看IP地址的命令可能会有所不同。以下是一些常见操作系统中查看IP地址的命令&#xff1a; Windows: 打开命令提示符&#xff08;CMD&#xff09;&#xff0c;然后输入 ipconfig 命令。 Linux/Unix: 打开终端&#xff0…...

力扣9.26

931. 下降路径最小和 给你一个 n x n 的 方形 整数数组 matrix &#xff0c;请你找出并返回通过matrix 的下降路径 的 最小和 。 下降路径 可以从第一行中的任何元素开始&#xff0c;并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列&#xff08;即…...

HT8731 内置自适应H类升压和防破音功能的10W D类及AB类音频功率放大器

1、特点 防削顶失真功能(防破音,Anti-Clipping Function, ACF) 免滤波器数字调制&#xff0c;直接驱动扬声器 输出功率 10W(VBAT4.2V,RL3Ω,THDN10%, fiN 1kHz) 6W(VBAT3.3~4.2V,RL4Ω,THDN<1%,20-20kHz 全频段) 3W (VBAT3.3~4.2V,RL8Ω, THDN<1%, 20- 20kHz 全频段 VB…...

webpack使用

一、简介 概述 本次使用webpack4进行构建打包 二、webpack 安装webpack、webpack-cli npm install webpack4.2.0 webpack-cli4.2.0 -D 三、loader 加载器概述 raw-loader&#xff1a;加载文件原始内容&#xff08;utf-8&#xff09; file-loader&#xff1a;把文件输出…...

高通Android 12 音量API设置相关代码

// 获取当前音量大小public static int getCurrentVolume(Context context) {AudioManager audioManager (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);return audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); // 使用 STREAM_MUSIC 作为示例…...

Qt开发第一讲

一、Qt项目里面有什么&#xff1f; 对各个文件的解释&#xff1a; Empty.pro文件 QT core gui # 要引入的Qt模块&#xff0c;后面学习到一些内容的时候可能会修改这里 #这个文件相当于Linux里面的makefile文件。makefile其实是一个非常古老的技术了。 #qmake搭配.pr…...

详细指南:如何有效解决Windows系统中msvcp140.dll丢失的解决方法

如果你在使用Windows系统时遇到“msvcp140.dll丢失”的错误提示&#xff0c;通常是因为你的计算机上缺少或损坏了msvcp140.dll文件。msvcp140.dll是Microsoft Visual C Redistributable包的一部分&#xff0c;许多应用程序和游戏需要它来正常运行。以下是几种解决msvcp140.dll丢…...

【RabbitMQ】幂等性、顺序性

幂等性 概述 幂等性是数学和计算机科学中某些运算的性质&#xff0c;他们可以被多次应用&#xff0c;而不会改变初始应用的结果。RabbitMQ的幂等性则是指同一条消息&#xff0c;多次消费&#xff0c;对系统的影响是相同的。 一般消息中间件的消息传输保障分为三个层级&#…...

FFmpeg源码:avio_skip函数分析

AVIOContext结构体和其相关的函数分析&#xff1a; FFmpeg源码&#xff1a;avio_r8、avio_rl16、avio_rl24、avio_rl32、avio_rl64函数分析 FFmpeg源码&#xff1a;read_packet_wrapper、fill_buffer函数分析 FFmpeg源码&#xff1a;avio_read函数分析 FFmpeg源码&#xff…...

Llama 3.1 技术研究报告-6

6 推理 我们研究了两种主要技术&#xff0c;以使 Llama 3 405B 模型的推理⾼效&#xff1a;(1) 流⽔线并⾏和 (2) FP8 量化。我们已经公开发布了我们的 FP8 量化实现。 6.1 流⽔线并⾏ 当使⽤ BF16 数字表⽰模型参数时&#xff0c;Llama 3 405B 不适合在装有 8 个 Nvidia H1…...

更新日志-Python OS

这么久没更新全是因为这段时间的事情很多&#xff0c;只能一点一点的更新代码&#xff0c;不过好在&#xff0c;也是成功更新出来啦&#xff01; 更新日志&#xff08;2024/9/29&#xff09; 代码全文更新&#xff0c;将所有的绝对路径替换为相对路径&#xff0c;这样在各位大…...

Chrome浏览器的C++内存管理技术揭秘

Chrome浏览器作为全球最流行的网络浏览器之一&#xff0c;其高效的内存管理技术功不可没。本文将深入探讨Chrome浏览器在C中的内存管理技术&#xff0c;并介绍如何通过调整网页加载时间、优化视频播放体验和解决谷歌浏览器占用CPU过高的问题来提升浏览器性能。 &#xff08;本…...

Redis --- redis事务和分布式事务锁

redis事务基本实现 Redis 可以通过 MULTI&#xff0c;EXEC&#xff0c;DISCARD 和 WATCH 等命令来实现事务(transaction)功能。 > MULTI OK > SET USER "Guide哥" QUEUED > GET USER QUEUED > EXEC 1) OK 2) "Guide哥"使用 MULTI命令后可以输入…...

SQL,将多对多的关联记录按行输出

数据库的Primary表和Secondary表有相同的结构&#xff0c;其中W、H、D是主键。Primary表&#xff1a;NameWHDPrimary item 1100500300Primary item 2100600300Primary item 3200500300Primary item 4100500300Primary item 5100600300Primary item 6200500300 Secondary表&…...

【SQL】筛选字符串与正则表达式

目录 语法 需求 示例 分析 代码 语法 SELECT column1, column2, ... FROM table_name WHERE condition; WHERE 子句用于指定过滤条件&#xff0c;以限制从数据库表中检索的数据。当你执行一个查询时&#xff0c;WHERE 子句允许你筛选出满足特定条件的记录。如果记录满…...

【Redis入门到精通五】Java如何像使用MySQL一样使用Redis(jedis安装及使用)

目录 Jedis 1.jedis是什么 2.jedis的安装配置 3.jedis的基础命令操作展示 1.set和get操作&#xff1a; 2.exists和del操作&#xff1a; 3.keys和type操作&#xff1a; 4. expire和ttl&#xff1a; Jedis Java 操作 redis 的客⼾端有很多&#xff0c;其中最知名的是 jedi…...

【 微信机器人+ AI 搭建】

摘要&#xff1a; 各种大模型已经出来好久了&#xff0c;各类app也已经玩腻了&#xff0c;接下来&#xff0c;就在考虑&#xff0c;怎么让大模型&#xff0c;利益最大化。 本人没有显著的家世&#xff0c;没有富婆包养&#xff0c;只能自己抽点时间&#xff0c;研究下技术&…...

VGG16网络介绍及代码撰写详解(总结1)

可以从本人以前的文章中可以看出作者以前从事的是嵌入式控制方面相关的工作&#xff0c;是一个机器视觉小白&#xff0c;之所以开始入门机器视觉的学习只要是一个idea&#xff0c;想把机器视觉与控制相融合未来做一点小东西。废话不多说开始正题。 摘要&#xff1a;本文是介绍V…...

多个excel表数据比对操作

多个excel表数据比对操作 本文主要使用两种方法进行比对,分别使用了openpyxl第三方库和pandas第三方库进行数据比对 两种方法优缺点: openpyxy: 优点:主要是处理xlsx的文件,里面方法简单,易懂 缺点:当数据量大的时候,速度很慢,之前我一条一条数据拿出来比较,两百多条…...

golang学习笔记32——哪些是用golang实现的热门框架和工具

推荐学习文档 golang应用级os框架&#xff0c;欢迎stargolang应用级os框架使用案例&#xff0c;欢迎star案例&#xff1a;基于golang开发的一款超有个性的旅游计划app经历golang实战大纲golang优秀开发常用开源库汇总想学习更多golang知识&#xff0c;这里有免费的golang学习笔…...

wordpress弹出层插件/品牌推广运营策划方案

1、题目描述 给定一个无序的数组&#xff0c;找出数组在排序之后&#xff0c;相邻元素之间最大的差值。 如果数组元素个数小于 2&#xff0c;则返回 0。 示例 1: 输入: [3,6,9,1] 输出: 3 解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。…...

做英文网站2014/运营推广计划

2019独角兽企业重金招聘Python工程师标准>>> http://elixir-lang.github.io/getting-started/binaries-strings-and-char-lists.html https://elixirforum.com/t/bitstring-and-binary/2351/4 字符串是怎么保存到计算机中的 iex>?a 97 iex>?吴 21556 a的cod…...

环境设计排版素材网站/百度网站怎样优化排名

转载自&#xff1a;http://www.cocoachina.com/gamedev/misc/2011/0627/2981.html 游戏一直是 App Store 应用里最热门、最赚钱的一大分类&#xff0c;iOS 游戏强调创意和趣味性的特点也让国内开发团队有更多一鸣惊人的机会。针对市场、玩家、开发商、成功案例等游戏开发要素&a…...

用新浪云做网站/百度竞价排名什么意思

小编典典将该文件放入资产中 。对于在Android Studio项目中创建的项目&#xff0c;您需要在主文件夹下创建资产文件夹。将该文件读取为&#xff1a;public String loadJSONFromAsset(Context context) {String json null;try {InputStream is context.getAssets().open("…...

什么创网站/ip切换工具

本文实例讲述了java简单解析xls文件的方法。分享给大家供大家参考&#xff0c;具体如下&#xff1a;读取&#xff1a;import java.io.*;import jxl.*;import jxl.write.*;import jxl.format.*;class Aa{public static void main(String args[]) {try{Workbook workbook null;t…...

i深圳网站建设/营销型网站的公司

p9 svn配置多仓库与权限控制...