展开菜单
首页 精品内容 本月促销 装机必备 Windows PDF教程 macOS软件 IOS软件 Android AI 专题
全部分类

当前位置:

首页 > 编程开发 > SpringBoot实现无感刷新token的具体方法

SpringBoot实现无感刷新token的具体方法

前言 Token无感刷新,简单说,就是让用户在无感知的情况下,自动把访问令牌(Token)更新一下,维持住登录状态。听起来挺美,但真正动手做起来,坑不少。 常见的做法是:用短期的token做权限认证,再配一个更长期的refreshToken专职负责刷新。但这条路走下来,总会撞上几个经典问题: Q1:

本文目录

    前言

    Token无感刷新,简单说,就是让用户在无感知的情况下,自动把访问令牌(Token)更新一下,维持住登录状态。听起来挺美,但真正动手做起来,坑不少。

    常见的做法是:用短期的token做权限认证,再配一个更长期的refreshToken专职负责刷新。但这条路走下来,总会撞上几个经典问题:

    • Q1: 这事儿到底该在服务端干,还是客户端就能搞定?
    • Q2: token过期后解析不了,那它里面的过期时间咋取出来?
    • Q3: 无感刷新的核心是——拿到新token后,得把之前失败的请求重新发出去,还得把结果正确返给调用方。这个流程怎么接得上?

    下面针对这些问题,梳理一下可以参考的思路。

    一、客户端实现

    1.1 初始版本

    思路是这样:每次客户端发请求,服务端的gateway先拦下来,判断token是否过期。过期了,返回一个特定的状态码(比如511)通知客户端;没过期,放行,正常走业务逻辑。

    前端那边呢,拦截响应状态码,根据码值决定下一步动作。于是,axios拦截器就登场了。

    1.1.1 服务端gateway拦截器

    环境是SpringBoot3 + Ja va17,通过继承GlobalFilter来实现过滤逻辑。

    @Component
    public class MyAccessFilter implements GlobalFilter, Ordered
    {
        @Override
        public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            ServerHttpRequest request = exchange.getRequest();
            String uri = request.getURI().getPath();
            HttpMethod method = request.getMethod();
    
            // OPTION直接放行
            if(method.matches(HttpMethod.OPTIONS.name()))
                return chain.filter(exchange);
    
            // 登录请求直接放行
            if(SecurityAccessConstant.REQUEST_LOGGING_URI.equals(uri) && method.matches(HttpMethod.POST.name()))
                return chain.filter(exchange);
       
            // 获取token
            String token = JWTHelper.getToken(request.getHeaders().getFirst(SecurityAccessConstant.HEADER_NAME_TOKEN));
            if(null != token){
                // 判断token是否过时
                if(!JWTHelper.isOutDate(token)){
                    return chain.filter(exchange);
                } else {
                    if(!SecurityAccessConstant.REQUEST_REFRESH.equals(uri))    // 当前不是刷新请求,返回511
                        return ResponseUtils.out(exchange , ResultData.fail(ResultCodeEnum.NEED_TO_REFRESH_TOKEN.getCode(),
                            ResultCodeEnum.NEED_TO_REFRESH_TOKEN.getMessage()));
    
                    // 当前是刷新请求,但refreshToken都过期了
                    return ResponseUtils.out(exchange , ResultData.fail(ResultCodeEnum.RC401.getCode(), ResultCodeEnum.RC401.getMessage()));
                }
            }
            
            return ResponseUtils.out(exchange , ResultData.fail(ResultCodeEnum.RC401.getCode(), ResultCodeEnum.RC401.getMessage()));
        }
    
        @Override
        public int getOrder() {
            return Ordered.LOWEST_PRECEDENCE;
        }
    }
    

    1.1.1.1 解答Q2:怎么获取过期时间?

    正常情况下解析token会报错。所以在解析的时候截获异常,如果catch到JwtException,就认为token无效过期了,返回true。否则正常解析,取出过期时间和当前时间比较。

    // 判断当前token是否过期
    public static boolean isOutDate(String token){
        try {
            Jws claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
            Date expirationDate = claimsJws.getBody().getExpiration();
            return expirationDate.before(new Date());
        } catch (JwtException e) {
            return true;
        }
    }
    

    1.1.2 axios拦截器

    在拦截器里,根据响应码做判断。401就清空用户数据,跳到登录页;511就用refreshToken去刷新。

    // 响应拦截器
    service.interceptors.response.use(
      function(response) {
        console.log(response)
        return response.data.data;
      },
      async function(error) {
        if(error.response == undefined)
          return Promise.reject(error);
    
        const status = error.response.status
        const authStore = useAuthStore()
        let message = ''
    
        switch(status){
          case 401:
            authStore.reset()
            window.sessionStorage.removeItem('isAuthenticated')
            window.sessionStorage.removeItem('token')
            window.sessionStorage.removeItem('refreshToken')
            message = 'token 失效,请重新登录'
            window.location.href = '/auth/login';
            break;
          case 511:
            try {
              const data = refresh()
              if(data !== null){
                data.then((value) => {
                  if(value !== ''){
                    console.log("刷新 token 成功", value);
                    window.sessionStorage.setItem("token",value)
                    error.config.headers['Authorization'] = 'Bearer ' +value;
                    return service(error.config);
                  }
                  console.log(value);
                }).catch((error) => {
                  console.error(error);
                });
              }
            } catch (err) {
              console.log("请求刷新 token 失败", err);
              router.push("/login");
            }
            break;
          case '403':
            message = '拒绝访问'
            break;
          case '404':
            message = '请求地址错误'
            break;
          case '500':
            message = '服务器故障'
            break;
          default:
            message = '网络连接故障'   
        }
        Message.error(message)
        return Promise.reject(error);
      }
    );
    

    1.1.3 refresh刷新token的方法

    这里直接用axios原声发异步请求,而不是用request.ts里导出的方法。因为那个方法自带请求拦截器,每次请求前都会自动带上token——但这里要刷新token,老token已经失效了,再用它就会死循环。

    /**
     * 刷新token 
     * 成功返回新token,失败返回空字符串''
     */
    export async function refresh() : Promise{
      const refreshToken = window.sessionStorage.getItem("refreshToken")
      console.log("in >>> " ,refreshToken)
      if(refreshToken == undefined)
          return ''
      
      try {
        const response = await axios({
          method: 'GET',
          url: 'http://127.0.0.1:9001/api/simple/cloud/access/refresh',
          headers: {
            Authorization: `Bearer ${refreshToken}`,
          },
        });
        if (response.data) {
          return response.data.data;
        } else {
          return '';
        }
      } catch (error) {
        console.log(error);
        return '';
      }
    }  
    

    1.1.4 正常与异常情况下的控制台输出分析

    代码里塞了不少console.log,跑起来能更直观地看流程。结合测试结果一起分析。

    下面的截图是正常情况。注意这里token以hizFIGg结尾,refreshToken以suvm-EgQ结尾。正常返回就是200 OK。

    SpringBoot实现无感刷新token的具体方法

    再看看异常情况。因为token太长拆了两张图。从左图开始分析:

    • 发起第一次请求后,后端gateway拦截器报511(对应case 511,触发刷新流程)。
    • in >>>进入refresh方法,成功打出refreshToken,仍是suvm-EgQ结尾。
    • 接着打出“刷新token成功”,返回的是新token,用它替换旧的并重发请求。

    左图分析完毕,进入右图:

    • 用新token发请求,请求拦截器捕获到的token已更新,以V0dYcMA结尾;refreshToken仍是suvm-EgQ(说明refreshToken只用来刷新,本身不刷新)。
    • 但是注意到“Uncaught error status 511”——这不就是一开始的错误吗?实际上,原按钮点击事件调用的getAllUser方法已经结束,返回的结果就是那个511 error。而refresh是异步调用,它的执行穿插在里面。

    SpringBoot实现无感刷新token的具体方法

    SpringBoot实现无感刷新token的具体方法

    最终结果里,没有出现之前“>>>>>”那部分输出。用新token发第二次请求得到的数据,已经无法返回到原来的getAllUser方法调用处——因为原方法早就结束了。通俗点说,这个二次调用结果毫无意义,用户还是得手动刷新页面或再点一次。

    这就是Q3的核心问题:异步调用导致原方法提前终止。如果改成阻塞式调用,响应会变慢,影响用户体验。有更好的想法欢迎讨论。

    1.2 改进版本

    既然异步路子走不通,换个思路如何:不在失败时去刷新,而是提前检查本地存储的token有没有过期。如果发现token剩余时间小于某个临界点,就主动异步调用刷新方法,更新本地token。这样一来,只要服务端gateway拦截到token失效的请求,直接要求重新登录就行。关键点在于引入一个定时器。

    在TypeScript里,定时器主要通过setInterval和setTimeout实现。这里用setInterval,按指定间隔重复执行某段逻辑。

    当setInterval被调用时,它从被调用那一刻开始计时,每隔指定毫秒数就执行一次。所以这个定时器应该在登录成功后启动。

    1.2.1 定义定时器类

    通过这个类,MyTimer.start方法启动setInterval,每隔delay毫秒检查一次,判断当前token剩余时间是否小于minCheck,如果小于,就异步刷新。

    import { refresh } from "@/api/system/auth/index"
    import { jwtDecode } from "jwt-decode";
    
    export class MyTimer {
      private timerId: any | null = null;
      
       start(delay: number, minCheck : number): void {
         this.timerId = setInterval(async () => {
           const currentToken = window.sessionStorage.getItem('token');
           console.log("timer++++")
           if (currentToken) {
             let expirationTime = 0;
             expirationTime = getExpirationTime(currentToken);
             const timeRemaining = expirationTime - Date.now();
             if (timeRemaining <= minCheck) {
               await refresh();
             }
           } else {
             await refresh();
           }
       }, delay);
       }
     
       stop(): void {
         if (this.timerId !== null) {
           clearInterval(this.timerId);
           this.timerId = null;
         }
       }
     }
    
     function getExpirationTime(rawToken:string) : number{
       const res = jwtDecode(rawToken)
       return res.exp as number
     }
    

    1.2.2 修改Login点击事件

    只关注新增的定时器部分,其他是权限和token的常规存储逻辑。

    import { MyTimer } from "@/utils/tokenMonitor"
    
    const submit = () => {
      if (validate()) {
        login(formData)
          .then((data: UserInfoRes) => {
            if (data) {
              const token = data.token;
              const authStore = useAuthStore()
              authStore.setToken(token)
              window.sessionStorage.setItem('token', token)
              window.sessionStorage.setItem('refreshToken', data.refreshToken)
              authStore.setIsAuthenticated(true)
              window.sessionStorage.setItem('isAuthenticated', 'true')
              authStore.setName(data.name)
              authStore.setButtons(data.buttons)
              authStore.setRoles(data.roles)
              authStore.setRouters(data.routers)
              // 引入计时器
              const clock = new MyTimer();
              clock.start(1000*30,1000*30);
    
              init({ message: "logged in success", color: 'success' });
              push({ name: 'dashboard' })
            }
          })
          .catch(() => {
            init({ message: "logged in fail , please check carefully!", color: '#FF0000' });
          });
      } else {
        Message.error('error submit!!')
        return false
      }
    }
    

    1.2.3 测试

    照理说应该没问题,能正确解析token。但一跑就报错了:

    InvalidTokenError: Invalid token specified: invalid json for part #2

    换成jwt.verify()用密钥解码也不行,页面都加载不出来。

    SpringBoot实现无感刷新token的具体方法

    翻了不少资料后总结出原因:后端token是用jjwt库生成的,不同库的底层编码实现有差异,导致a库加密的token无法被b库解密。

    找到了病根,解决方案就清晰了:要么用与jjwt逻辑一致的库来解码,要么换个思路——服务端下发token时,直接把过期时间也带上。前端省去解码这一步,问题迎刃而解。下面引出最终版本。

    1.3 最终定时器版本

    1.3.1 服务端修改

    1.3.1.1 获取token过期时间

    // 获取当前token过期时间,不判断是否过期(因为进来时已经判断过了)
    public static Date getExpirationDate(String token) {
        if(StringUtil.isBlank(token))
            return null;
    
        Claims claims = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody();
        return claims.getExpiration();
    }
    

    1.3.1.2 发放token时携带过期时间

    // 存放token到请求头中
    String[] tokenArray = JWTHelper.createToken(sysUser.getId(), sysUser.getEmail(), permsList);
    map.put("token",tokenArray[0]);
    // 新增设置过期时间(毫秒数)
    map.put("tokenExpire",JWTHelper.getExpirationDate(tokenArray[0]).getTime());
    map.put("refreshToken",tokenArray[1]);
    

    同样,在refreshToken接口返回时也带上新的过期时间。

    1.3.2 修改监控器类MyTimer

    最终版本的类包含三个属性:timerId(定时器唯一ID)、delay(执行间隔)、minCheck(过期时间阈值)。采用单例模式,全局导出一份实例便于管理。token的过期时间直接从服务端获取,和当前时间比较即可。

    import { refresh } from "@/api/system/auth/index"
    
    class MyTimer {
        private timerId: any | null = null;
        private delay: number;
        private minCheck: number;
    
        private static instance: MyTimer;
    
        public static getInstance(): MyTimer {
          if (!MyTimer.instance) {
            MyTimer.instance = new MyTimer();
          }
          return MyTimer.instance;
        }
    
        private constructor() {
          this.delay = 30000;
          this.minCheck = 60000;
        }
    
        start(): void {
          this.timerId = setInterval(async () => {
            const currentToken = window.sessionStorage.getItem('token');
            console.log("timer++++",currentToken)
            if (currentToken) {
              const tokenExpireStr = window.sessionStorage.getItem('tokenExpire') as string
              const expirationTime = parseInt(tokenExpireStr, 10);
              const timeRemaining = expirationTime - Date.now();
              console.log("ttime sub++++",timeRemaining)
              if (timeRemaining <= this.minCheck) {
                try{
                  await refresh();
                } catch (error) {
                  console.error('刷新失败:', error);
                  window.sessionStorage.removeItem('isAuthenticated')
                  window.sessionStorage.removeItem('token')
                  window.sessionStorage.removeItem('refreshToken')
                  Message.error("token refresh got some problem, please login")
                  window.location.href = '/auth/login';
                }
              }
            } else {
              Message.error("token invalidate, please login")
              window.location.href = '/auth/login';
            }
        }, this.delay);
    
        console.log(this.timerId)
        }
    
        stop(): void {
          if (this.timerId !== null) {
            clearInterval(this.timerId);
            this.timerId = null;
          }
        }
    
        setDelay(delay: number): void {
          this.delay = delay;
        }
      
        setMinCheck(minCheck: number): void {
          this.minCheck = minCheck;
        }
    }
    
    export const myFilterInstance = MyTimer.getInstance();
    
    export function onPageRender(){
        myFilterInstance.stop();
        myFilterInstance.start();
    }
    

    1.3.3 onPageRender 的使用

    注意最后这个方法。测试中发现,通过导航栏切换页面时,定时器会被kill掉,导致token无法刷新,新请求会报错。因此最好在每个页面挂载时调用onPageRender,重启定时器,确保token一直有“人”照看。

    import { onPageRender } from '@/utils/tokenMonitor'
    
    window.addEventListener('load', () => {
      onPageRender();
    });
    

    1.3.4 测试

    最终的测试结果如下(可以结合代码中的输出语句来看):

    • 红色框框是监控器触发后输出的日志,每次都会比对token的过期时间是否小于阈值。刷新后还会用新的过期时间继续比较。

    一旦小于阈值(这里设置1分钟 = 60000ms),就进入refresh逻辑,和上面讲的一样。这样就能保证每次请求携带的token大概率都是最新的。客户端实现的思路到这里就完整讲完了。

    SpringBoot实现无感刷新token的具体方法

    2. 服务端实现

    这种思路是:在gateway拦截token时,如果发现过期,就通过WebClient携带refreshToken异步请求认证服务器获取新token。下面的代码实现了发起请求到获取数据的过程,但没有实现原请求的再发送(这个坑后续再填)。

    Mono newTokenMono = WebClient.create().get()
             .uri(buildUri(SecurityAccessConstant.WEB_REQUEST_TO_AUTH_URL+SecurityAccessConstant.REQUEST_REFRESH
                     , new String[]{"refreshToken", token}))
             .retrieve()
             .bodyToMono(ResultData.class);
    
    AtomicBoolean isPass = new AtomicBoolean(false);
    
    newTokenMono.subscribe(resultData -> {
         if(resultData.getCode() == "200"){
             exchange.getRequest().getHeaders().set(SecurityAccessConstant.HEADER_NAME_TOKEN,
                     SecurityAccessConstant.TOKEN_PREFIX + resultData.getData());
             isPass.set(true);
         }
    }).dispose();
    
    if(isPass.get()){
         return chain.filter(exchange.mutate().request().build());
    }
    

    3. 怎么选择

    3.1 服务端实现的好处

    • 安全性更高: token刷新逻辑在服务端,敏感信息不暴露给客户端。
    • 客户端更轻量: 无需关心刷新逻辑,减少了维护成本。
    • 集中管理: 所有用户的刷新统一在服务端处理,方便调整。
    • 解决一致性问题: 避免多客户端间的token状态不一致。

    3.2 客户端实现的好处

    • 即时性好: 客户端实时监控token有效性,触发刷新,保证操作流畅。
    • 离线支持: 适合需要离线访问或长时间不与服务端通信的场景。
    • 更灵活: 可以根据用户行为动态调整刷新策略。
    • 分担服务端压力: 大量用户同时刷新时,压力分散到客户端。

    不同场景下,选择也不同。在高精度、高安全要求的系统中,服务端刷新更稳妥;而在移动端或简单Web应用中,客户端方案能有效分担服务器压力。具体选哪个,还得看实际需求。

    本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
    作者最新文章
    编程开发
    相关文章 更多
    精品专题 更多
    装机必备

    正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

    Windows

    正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

    PDF教程

    正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

    macOS软件

    正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

    IOS软件

    正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

    AI

    正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

    Mac软件 更多
    Postman macOS版
    Postman macOS版

    Postman是一套面向API全生命周期的开发与测试平台,可在Mac上发送和调试多种协议请求,管理集合、环境及认证信息,并完成脚本测试、模拟服务、监控、文档发布和团队协作。它适合开发、测试及运维人员使用,提供个人免费方案。

    Docker Desktop macOS版
    Docker Desktop macOS版

    Docker Desktop 是 Docker 官方为 Mac 提供的容器开发环境,集成 Docker Engine、命令行工具、Compose、Build、Kubernetes 与图形化管理界面。

    Android Studio macOS版
    Android Studio macOS版

    Android Studio 是 Google 面向安卓应用开发提供的官方集成开发环境,基于 IntelliJ IDEA 构建。它整合智能代码编辑、Gradle 构建、界面预览、设备模拟、性能分析、调试测试及 APK 分析等工具。

    Xcode macOS版
    Xcode macOS版

    Xcode 是苹果面向 Mac 提供的官方集成开发环境,将代码编辑、项目构建、SwiftUI 实时预览、设备模拟、自动化测试、断点调试、性能分析和应用分发整合在同一套工作流中,适合开发与维护 iPhone、iPad、Mac、Apple Wa

    JetBrains WebStorm macOS版
    JetBrains WebStorm macOS版

    JetBrains WebStorm 是面向 JavaScript、TypeScript 和现代 Web 技术的专业集成开发环境,可智能理解项目结构,提供代码补全、重构、错误检查、运行调试、测试及 Git 工具。

    PhpStorm macOS版
    PhpStorm macOS版

    PhpStorm 是 JetBrains 推出的专业 PHP 集成开发环境,可在 Mac 上完成代码编写、智能检查、重构、调试、测试和数据库管理。它支持主流 PHP 框架、Composer、Git、Docker 与远程解释器。

    JetBrains PyCharm macOS版
    JetBrains PyCharm macOS版

    JetBrains PyCharm 是面向 Python 开发者的集成开发环境,提供智能补全、代码检查、重构、调试、测试、版本控制及虚拟环境管理功能。Mac 版兼容 Intel 与 Apple 芯片,核心功能可免费使用。

    JetBrains IntelliJ IDEA macOS版
    JetBrains IntelliJ IDEA macOS版

    IntelliJ IDEA 是 JetBrains 推出的专业集成开发环境,重点服务于 Java、Kotlin 与 JVM 项目开发。软件集成代码补全、静态检查、重构、调试、测试、版本控制及构建工具,并可借助插件扩展语言和框架支持。

    Visual Studio Code macOS版
    Visual Studio Code macOS版

    Visual Studio Code 是微软提供的免费代码编辑器,支持 macOS,并为 Intel 与 Apple 芯片 Mac 提供适配版本。软件集代码编辑、调试、Git 源代码管理、集成终端和扩展市场于一体。

    REAPER macOS版
    REAPER macOS版

    REAPER是Cockos开发的数字音频工作站,提供多轨音频与MIDI录制、剪辑、处理、混音和母带制作工具。Mac版兼容Intel与Apple芯片,支持AU、VST、VST3、CLAP等插件格式,并提供高度可定制的工作流程。

    Adobe Audition macOS版
    Adobe Audition macOS版

    Adobe Audition 是面向播客、影视后期、音乐制作及内容创作者的专业音频工作站,提供波形精修、多轨录制与混音、频谱编辑、降噪修复、响度匹配和效果处理工具,并可与 Adobe Premiere 协同完成视频声音制作。

    Pro Tools macOS版
    Pro Tools macOS版

    Pro Tools 是 Avid 推出的专业数字音频工作站,覆盖多轨录音、无损剪辑、MIDI 编曲、插件处理、自动化混音及声音与画面同步等工作。它拥有 Intro、Artist、Studio 和 Ultimate 等方案。

    WINDOWS 更多
    PixPix 网页版
    PixPix 网页版

    PixPix 是面向品牌、卖家和内容团队的在线 AI 图片视频创作平台,提供图视频 Agent、无限画布、图片生成、视频生成、自然语言编辑及商品套图、产品精修和带货视频等电商工具。

    Postman Windows版
    Postman Windows版

    Postman 是面向 API 全生命周期的开发与协作平台。Windows 桌面版可用于构建和发送 HTTP、GraphQL、gRPC、WebSocket、MQTT 等请求,并提供环境变量、集合、脚本测试、模拟服务、监控、文档及团队工作区等

    Docker Desktop Windows版
    Docker Desktop Windows版

    Docker Desktop 为 Windows 开发者提供集成式容器环境,内置 Docker Engine、命令行工具、Compose、Build 与 Kubernetes 等组件,可在本机完成镜像构建、容器运行、日志查看和资源管理。

    Android Studio Windows版
    Android Studio Windows版

    Android Studio 是 Google 面向 Android 应用开发提供的官方集成开发环境,以 IntelliJ IDEA 为基础,集成智能代码编辑、Gradle 构建、设备模拟、界面预览、调试测试及性能分析工具,

    JetBrains WebStorm Windows版
    JetBrains WebStorm Windows版

    WebStorm 是 JetBrains 面向 JavaScript、TypeScript 和现代 Web 技术打造的集成开发环境。它提供项目级代码分析、智能补全、安全重构、导航搜索,并集成调试、测试、终端、Git 与常用前端工具,

    JetBrains PhpStorm Windows版
    JetBrains PhpStorm Windows版

    PhpStorm 是 JetBrains 面向 PHP 与现代 Web 开发打造的专业集成开发环境,提供代码补全、静态检查、重构、调试、测试、数据库、版本控制及前端语言支持。Windows 用户可直接下载安装程序,

    JetBrains PyCharm Windows版
    JetBrains PyCharm Windows版

    PyCharm 是 JetBrains 推出的 Python 集成开发环境,提供智能代码补全、错误检查、重构、调试、测试、Git、终端及 Jupyter Notebook 支持。统一版安装后可体验 30 天 Pro 功能,

    JetBrains IntelliJ IDEA Windows版
    JetBrains IntelliJ IDEA Windows版

    JetBrains IntelliJ IDEA 是以 Java、Kotlin 和 JVM 项目为核心的专业集成开发环境,提供智能补全、代码检查、安全重构、调试、测试、版本控制及构建工具集成。统一发行版的核心功能可免费用于商业和非商业项目,

    Visual Studio Code Windows版
    Visual Studio Code Windows版

    Visual Studio Code 是微软推出的免费代码编辑器,Windows 版集成智能补全、代码导航、调试、终端和版本控制,并可通过扩展添加语言、格式化及远程开发能力。它启动较快、配置灵活,

    REAPER Windows版
    REAPER Windows版

    REAPER 是 Cockos 推出的数字音频工作站,适用于录音、MIDI 编曲、音频编辑、混音、母带制作及声音设计。软件安装包小巧,支持多轨录制、灵活路由、自动化、第三方插件和脚本扩展,并提供完整功能的 60 天免费评估期。

    Adobe Audition Windows版
    Adobe Audition Windows版

    Adobe Audition 是面向播客、影视后期、广播与音乐制作的专业音频工作站,提供波形编辑、多轨混音、频谱分析、录音、降噪修复和效果处理等功能。Windows 版通过 Adobe Creative Cloud 安装和管理,

    Pro Tools Windows版
    Pro Tools Windows版

    Pro Tools 是 Avid 推出的专业数字音频工作站,提供多轨录音、音频剪辑、MIDI编曲、插件处理、自动化混音及影视声音制作能力。它适合音乐人、录音师、制作人和音频后期团队使用,并提供免费入门版、订阅方案及永久授权选择。