如何设计一个安全的对外接口?

news2024/12/23 19:08:14

对外接口安全措施的作用主要体现在两个方面,一方面是如何保证数据在传输过程中的安全性,另一方面是数据已经到达服务器端,服务器端如何识别数据。

1. 数据加密

数据在传输过程中是很容易被抓包的,如果直接传输,数据可以被任何人获取,所以必须对数据加密。加密方式有对称加密和非对称加密:

  • 对称加密:对称密钥在加密和解密的过程中使用的密钥是相同的,常见的对称加密算法有 DES、AES。优点是计算速度快,缺点是在数据传送前,发送方和接收方必须商定好密钥,并完好保存。如果一方的密钥被泄露,那么加密信息也就不安全了;
  • 非对称加密:服务端会生成一对密钥,私钥存放在服务器端,公钥可以发布给任何人使用。与对称加密相比,这种方式更安全,但速度慢太多了。

现在主流的做法是使用 HTTPS 协议,在 HTTP 和 TCP 之间添加一层加密层 (SSL 层),这一层负责数据的加密和解密。HTTPS 的实现方式结合了对称加密与非对称加密的优点,在安全和性能方面都比较突出。

示例

javascript

const CryptoJS = require("crypto-js");
// 密钥,8 字节
const key = CryptoJS.enc.Utf8.parse('12345678');
// 偏移量,8 字节
const iv = CryptoJS.enc.Utf8.parse('12345678');

// DES 加密
function encryptDES(data) {
  const encrypted = CryptoJS.DES.encrypt(data, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });
  return encrypted.toString();
}

// DES 解密
function decryptDES(encryptedData) {
  const decrypted = CryptoJS.DES.decrypt(encryptedData, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  });
  return decrypted.toString(CryptoJS.enc.Utf8);
}

// 测试
const originalData = 'Hello, world!';
const encryptedData = encryptDES(originalData); //YcunRrQmVq9nAmF4fyALkw==
console.log('加密后的数据:', encryptedData);
const decryptedData = decryptDES(encryptedData);
console.log('解密后的数据:', decryptedData);
复制代码

java

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import org.springframework.stereotype.Component;

public class DesUtil {
    
    // 密钥,长度为8个字符
    private static final String KEY = "12345678";
    
    // 偏移量,长度为8个字符
    private static final String IV = "12345678";
    
    // 加密算法
    private static final String ALGORITHM = "DES";
    
    // 加密模式
    private static final String TRANSFORMATION = "DES/CBC/PKCS5Padding";
    
    // 加密
    public static String encrypt(String input) throws Exception {
        // 创建密钥对象
        SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
        // 创建偏移量对象
        IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
        // 创建加密器对象
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        // 初始化加密器
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
        // 加密
        byte[] encrypted = cipher.doFinal(input.getBytes());
        // 将加密后的字节数组转换为Base64字符串
        return Base64.encodeBase64String(encrypted);
    }
    
    // 解密
    public static String decrypt(String input) throws Exception {
        // 创建密钥对象
        SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
        // 创建偏移量对象
        IvParameterSpec ivSpec = new IvParameterSpec(IV.getBytes());
        // 创建解密器对象
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        // 初始化解密器
        cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
        // 将Base64字符串解码为字节数组
        byte[] decrypted = cipher.doFinal(Base64.decodeBase64(input));
        // 将解密后的字节数组转换为字符串
        return new String(decrypted);
    }
  
    // 测试
    public static void main(String[] args) throws Exception {
        String originalData = "Hello, world!";
        String encryptedData = encrypt(originalData);
        System.out.println(encryptedData); // YcunRrQmVq9nAmF4fyALkw==
    }  
}
复制代码

2. 数据加签

数据加签就是由发送者产生一段无法伪造的数字串,来保证数据在传输过程中不被篡改。数据如果已经通过 HTTPS 加密了,其加密部分只是在外网,而加签可以防止内网中数据被篡改。

数据签名使用较多的是 MD5 算法,将需要提交的数据通过某种方式组合,然后通过 MD5 生成一段加密字符串,这段加密字符串就是数据包的签名。而其中的用户密钥,客户端和服务端都保留一份,会更加安全。

示例

java

定义一个工具类来实现带密钥的MD5加签和验签:

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Util {
    public static String md5(String data, String key) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update((data + key).getBytes());
            byte[] digest = md.digest();
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b & 0xff));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5加签失败", e);
        }
    }

    public static boolean verify(String data, String sign, String key) {
        return md5(data, key).equals(sign);
    }
}


复制代码

在业务代码中使用该工具类进行MD5加签和验签:

@RestController
public class DemoController {
    private static final String KEY = "123456";

    @GetMapping("/demo")
    public String demo(String data, String sign) {
        // 对数据进行MD5加签
        String signData = MD5Util.md5(data, KEY);
        if (signData.equals(sign)) {
            // 验签通过
            return "验签通过";
        } else {
            // 验签不通过
            return "验签不通过";
        }
    }
}
复制代码

javascript

定义一个工具类来实现带密钥的 MD5 加签和验签:

const CryptoJS = require("crypto-js");

const MD5Util = {
  md5(data, key) {
    // 使用 crypto-js 库计算 MD5 值
    const hash = CryptoJS.MD5(data + key); 
    return hash.toString();
  },

  verify(data, sign, key) {
    // 计算数据的签名
    const dataSign = this.md5(data, key); 
    // 返回验签结果
    return dataSign === sign; 
  }
};
复制代码

在业务代码中使用该工具类进行 MD5 加签和验签:

const KEY = '123456';

function demo(data, sign) {
  // 对数据进行 MD5 加签
  const signData = MD5Util.md5(data, KEY);
  if (signData === sign) {
    // 验签通过
    return '验签通过';
  } else {
    // 验签不通过
    return '验签不通过';
  }
}

复制代码

3. 时间戳机制

数据经过如上的加密、加签处理后,就算被抓包也不能看到真实的数据。但是有的不法者不关心真实数据,而是直接拿到抓取的数据包进行恶意请求。这时可以使用时间戳机制,在每次请求中加入当前的时间,服务器端会拿到当前时间与消息中的时间相减,看看是否在一个固定的时间范围内,比如 5 分钟,这样恶意请求的数据包是无法更改时间的,所以 5 分钟后就视为非法请求了。

java

定义一个拦截器:

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

public class TimeStampInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取请求头中的时间戳
        String timeStampStr = request.getHeader("timeStamp"); 
        if (timeStampStr != null) {
            // 将时间戳转换为 long 类型
            long timeStamp = Long.parseLong(timeStampStr); 
            // 获取当前时间戳
            long now = System.currentTimeMillis(); 
            // 验证时间戳是否有效
            if (TimeUnit.MILLISECONDS.toMinutes(now - timeStamp) < 5) { 
                return true;
            }
        }
        response.getWriter().print("时间戳无效!");
        response.getWriter().flush();
        response.getWriter().close();
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    }
}
复制代码

将拦截器注册到springboot中:

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

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
      	// 添加需要拦截的接口路径
        registry.addInterceptor(new TimeStampInterceptor())
                .addPathPatterns("/api/**"); 
    }
}
复制代码

4. AppID 机制

大部分网站都需要用户名和密码才能登录,这其实也是一种安全机制。相应的对外接口也需要这么一种机制,使用接口的用户需要在后台开通 AppID,提供给用户相关的密钥。在调用的接口中需要提供 AppID+ 密钥,服务器端会进行相关的验证。生成唯一的 AppID 即可,根据实际情况看是否需要全局唯一,同时密钥使用字母、数字等特殊字符随机生成。

java

定义一个拦截器:

import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class AppIdSecretInterceptor implements HandlerInterceptor {

    @Value("${app.id}")
    private String appId;

    @Value("${app.secret}")
    private String appSecret;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      	// 获取请求头中的 AppID
        String appIdParam = request.getHeader("appId"); 
      	// 获取请求头中的密钥
        String secretParam = request.getHeader("secret"); 
      	// 验证 AppID 和密钥是否正确
        if (appId.equals(appIdParam) && DigestUtils.md5Hex(appIdParam + appSecret).equals(secretParam)) { 
            return true;
        }
        response.getWriter().print("AppID 或密钥错误!");
        response.getWriter().flush();
        response.getWriter().close();
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    }

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

复制代码

将拦截器注册到springboot中:

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

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
      	// 添加需要拦截的接口路径
        registry.addInterceptor(new AppIdSecretInterceptor())
                .addPathPatterns("/api/**"); 
    }
}
复制代码

5. 限流机制

本来就是真实的用户,并且开通了 AppID,但出现了频繁调用接口的情况,这时需要给相关 AppID 限流处理,常用的限流算法包括:令牌桶限流、漏桶限流、计数器限流。

漏桶算法的原理是按照固定常量速率流出请求,流入请求速率任意,当请求数超过桶的容量时,新的请求等待或者拒绝服务,漏桶算法可以强制限制数据的传输速度。

令牌桶算法的原理是令牌桶算法 和漏桶算法 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。

计数器算法比较简单粗暴,主要用来限制总并发数,比如数据库连接池、线程池、秒杀的并发数。计数器限流只要一定时间内的总请求数超过设定的阀值,就会进行限流。

java

import com.google.common.util.concurrent.RateLimiter;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RateLimitInterceptor implements HandlerInterceptor {
		// 创建一个令牌桶,每秒放行 10 个请求
    private static final RateLimiter RATE_LIMITER = RateLimiter.create(10); 

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 尝试获取一个令牌
        if (RATE_LIMITER.tryAcquire()) { 
            return true;
        }
        response.getWriter().print("请求过于频繁,请稍后重试!");
        response.getWriter().flush();
        response.getWriter().close();
        return false;
    }
}

复制代码

以上代码中,我们定义了一个拦截器 RateLimitInterceptor,并创建了一个 RATE_LIMITER 对象作为令牌桶,每秒放行 10 个请求。在 preHandle 方法中,我们使用 RATE_LIMITER.tryAcquire() 方法尝试获取一个令牌,如果获取成功,则返回 true,否则拒绝调用接口并返回 false。

拦截器注册到 Spring Boot:

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

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加需要拦截的接口路径
        registry.addInterceptor(new RateLimitInterceptor())
                .addPathPatterns("/api/**"); 
    }
}
复制代码

6. 黑名单机制

如果一个 AppID 进行过很多非法操作,或者专门有一个中黑系统,经过分析之后可以直接将此 AppID 列入黑名单,所有请求直接返回错误码。如何查看黑名单列表呢?可以给用户设置一个状态比如:初始化状态、正常状态、中黑状态、关闭状态等等。或者直接通过分布式配置中心,保存黑名单列表,每次检查用户是否在列表中即可。

java

创建一个黑名单服务,用于管理被禁用的 IP:

import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.Set;

@Service
public class BlacklistService {
		// 存储被禁用的 IP
    private Set<String> blackList = new HashSet<>(); 

    public void add(String ip) {
        blackList.add(ip);
    }

    public boolean contains(String ip) {
        return blackList.contains(ip);
    }
}
复制代码

定义一个拦截器:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class BlacklistInterceptor implements HandlerInterceptor {

    @Autowired
    private BlacklistService blacklistService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String ip = request.getRemoteAddr();
        if (blacklistService.contains(ip)) {
            response.getWriter().print("您的 IP 已被禁用!");
            response.getWriter().flush();
            response.getWriter().close();
            return false;
        }
        return true;
    }
}
复制代码

拦截器注册到 Spring Boot:

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

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加需要拦截的接口路径
        registry.addInterceptor(new BlacklistInterceptor())
                .addPathPatterns("/api/**"); 
    }
}
复制代码

7. 数据合法性校验

这是每个系统都会有的处理机制,只有在数据合法的情况下才会进行数据处理。每个系统都有自己的验证规则,当然也可能有一些常规性的规则,比如身份证号码长度和组成、电话号码长度和组成等等。

合法性校验包括常规性校验以及业务校验,前者包括签名校验、必填校验、长度校验、类型校验、格式校验等,后者根据实际业务而定,比如订单金额不能小于 0 等等。

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

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

相关文章

elasticsearch 核心概念

1.近实时&#xff08;Near Real Time,NRT&#xff09; elasticsearch 是一个近实时的搜索和分析平台&#xff0c;这意味着从索引文档到可搜索文档都会有一段微小的延迟&#xff08;通常是1s以内&#xff09;。这种延迟主要是因为 elasticsearch 需要进行数据刷新和索引更新。 …

远程代码执行渗透与防御

远程代码执行渗透与防御1.简介2.PHP RCE常见函数3.靶场练习4.防御姿势1.简介 远程代码执行漏洞又叫命令注入漏洞 命令注入是一种攻击&#xff0c;其目标是通过易受攻击的应用程序在主机操作系统上执行任意命令。 当应用程序将不安全的用户提供的数据&#xff08;表单、cookie…

jQuery 基础入门速成上篇

jQuery 是目前使用最广泛的 javascript 函数库。提到 jQuery 你可能知道这句英文 &#xff1a; ———— Write Less&#xff0c;Do More ( 写的少&#xff0c;做的多 ) 引入jQuery jQuery是一个函数库&#xff0c;一个 js 文件&#xff0c;页面可以使用 script标签 引入使用&a…

JVM 类加载器

文章目录1 类加载器1.1 类加载器介绍1.2 类加载器的加载规则1.2 类加载器类型总结2 双亲委派模型2.1 双亲委派模型介绍2.2 双亲委派模型的执行流程2.3 双亲委派模型的好处回顾一下类加载过程&#xff1a;加载->连接->初始化。 其中连接又分为&#xff1a;验证->准备-&…

指针太难?手把手教你理解指针(传参、函数指针)

目录 前言 一、数组和指针的参数 1.一维数组传参 2.二维数组传参 3.一级指针传参 4.二级指针传参 二、函数指针 1.函数的地址 2.函数指针的形式 3.函数指针的使用 三、加深理解&#xff0c;两段有趣的代码 前言 之前的一篇文章讲到了指针的概念、指针和数组的关系&am…

【机器学习】随机森林预测泰坦尼克号生还概率

目录 前言&#xff1a; 【一】数据清洗及可视化 介绍 知识点 环境准备 数据特征介绍 检查数据 相关系数 缺失值 偏态分布 数值化和标准化 离群点 实验总结一 【二】分类模型训练及评价 介绍 环境准备 模型评估 模型选择 性能度量 实验总结二 【三】随机森…

机器学习入门(全连接神经网络-1)

机器学习入门(全连接神经网络-1) 目录 机器学习入门(全连接神经网络-1)一、神经元简介1.概念2.例子二、常见的激活函数从神经元开始进行讲述,从零开始搭建全连接神经网络。 一、神经元简介 1.概念 神经元是神经网络的基本组成单位。 神经元接受输入,对它们进行一些数学运…

自主机器人运动规划|地图相关概念总结

自主机器人运动规划|地图相关概念总结地图表示占用栅格地图八叉树地图&#xff08;Octo-map&#xff09;Voxel hashing(哈希表地图)点云地图TSDF mapESDF map地图表示 地图分成两个模块&#xff1a; 地图装在数据的数据结构地图信息融合方法 占用栅格地图 使用最为广范的是 …

用PyTorch训练模型识别captcha库生成的验证码

目录 制作训练数据集 用Dataloader加载自定义的Dataset 训练模型 识别验证码 总结与提高 源码下载 在本节&#xff0c;我们将使用深度学习框架PyTorch来训练模型去识别一种难度稍大一点的数字字母混合验证码&#xff08;我们可以使用第三方库captcha生成这种验证码&#…

4.2 换元积分法

思维导图&#xff1a; 学习目标&#xff1a; 学习换元积分法时&#xff0c;可以遵循以下几个步骤&#xff1a; 理解换元积分法的基本思想&#xff1a;将一个复杂的积分变成一个简单的积分&#xff0c;通过引入一个新的变量来实现。 掌握换元积分法的基本公式&#xff1a;如果…

reviewSpringBoot

1.springboot简介说明 Springboot简化新Spring应用的初始搭建以及开发过程 SpringBoot是基于Spring的框架&#xff0c;该框架使用了特定的方式来进行配置&#xff0c;从而使开发人员不再需要定义样板化的配置。 SpringBoot集成了绝大部分目前流行的开发框架&#xff0c;就像…

FISCO-BCOS链节点黑名单对共识的影响

目录一、前言二、测试过程三、PBFT一、前言 目的&#xff1a;在fisco-bcos&#xff08;v2.8.0&#xff09;环境下&#xff0c;测试黑名单对节点共识的影响。 部署搭建四节点的链&#xff0c;并部署console&#xff1a; https://fisco-bcos-documentation.readthedocs.io/zh_…

3 个自定义防抖 Hooks 的实现原理

前言— 本文通过实现 useDebounceFn、useDebounce、useDebounceEffect 3 种自定义防抖 Hooks&#xff0c;来介绍在日常开发过程中自定义 Hooks 的思路及实现&#xff0c;帮助大家完成通用 Hooks 来提高开发效率。 防抖— 防抖的概念已经司空见惯了&#xff0c;这里稍作简单介…

00后也太卷了吧!进厂起薪18K,原来面试时候都说了这些......

都说00后躺平了&#xff0c;但是有一说一&#xff0c;该牛的还是牛。 这不&#xff0c;前段时间公司来了个00后&#xff0c;工作都没两年&#xff0c;跳槽起薪18K。本来还以为是个年少有为的技术大牛呢&#xff0c;结果相处一个月下来发现技术也就那样。 问起他是如何做到和老…

Java Web学习路线

⭐作者介绍&#xff1a;大二本科网络工程专业在读&#xff0c;持续学习Java&#xff0c;努力输出优质文章 ⭐作者主页&#xff1a;逐梦苍穹 ⭐所属专栏&#xff1a;Java Web ⭐如果觉得文章写的不错&#xff0c;欢迎点个关注一键三连&#x1f609;有写的不好的地方也欢迎指正&a…

(四)栈—中缀表达式转后缀表达式

一、基本介绍 二、应用实例 将中缀表达式"1((23)x4)-5" 转换为 后缀表达式"1 2 3 4 x 5 -" 思路&#xff1a; 1.初始化两个栈&#xff1a;运算符栈s1和存储中间结果的栈s2&#xff1b; 2.从左至右扫描中缀表达式&#x…

【c++初阶】第九篇:vector(常用接口的使用 + 模拟实现)

文章目录vector介绍vector的使用vector的定义vector iterator(迭代器) 的使用begin和endrbegin和rendvector 空间增长问题size和capacityreserve和resize&#xff08;重点&#xff09;测试vector的默认扩容机制emptyvector的增删查改push_back和pop_backinsert和erasefindswapo…

GPT-4创造者:第二次改变AI浪潮的方向

OneFlow编译 翻译&#xff5c;贾川、杨婷、徐佳渝 编辑&#xff5c;王金许 一朝成名天下知。ChatGPT/GPT-4相关的新闻接二连三刷屏朋友圈&#xff0c;如今&#xff0c;这些模型背后的公司OpenAI的知名度不亚于任何科技巨头。 不过&#xff0c;就在ChatGPT问世前&#xff0c;Ope…

Verilog Tutorial(10)如何实现可复用的设计?

写在前面在自己准备写verilog教程之前&#xff0c;参考了许多资料----FPGA Tutorial网站的这套verilog教程即是其一。这套教程写得不错&#xff0c;只是没有中文&#xff0c;在下只好斗胆翻译过来&#xff08;加了自己的理解&#xff09;分享给大家。这是网站原文&#xff1a;h…

【敬伟ps教程】平移、缩放、移动、选区

文章目录平移抓手工具旋转抓手缩放工具移动工具详解选区选区工具详解平移 抓手工具 当打开一张大图时&#xff0c;可以通过修改底部的百分比或使用抓手工具&#xff08;H或在任何时候按住空格键来使用抓手工具&#xff09;来查看更多细节 使用抓手工具时滚动所有打开的文档&…