- content {:toc
本文详细介绍了 Jenkins Pipeline 中 SSH 远程认证的两种方式:用户名密码认证和 SSH 密钥认证。通过完整的代码示例和实践经验,帮助开发者掌握 Jenkins Pipeline 远程服务器操作的最佳实践,解决企业环境中的部署自动化难题。
Jenkins Pipeline SSH 认证完整指南:用户名密码与密钥登录详解
前言
在企业环境中搭建测试环境时,Jenkins Pipeline 的远程部署阶段通常需要通过 SSH 连接到目标服务器。虽然用户名密码认证方式简单直接,但在安全性要求较高的环境中,SSH 密钥认证是更推荐的选择。
本文将详细介绍 Jenkins Pipeline 中 SSH 认证的两种方式,并提供完整的实践指南和故障排除方法。
一、环境准备和插件安装
1. SSH Pipeline Steps 插件
插件介绍
SSH Pipeline Steps 是 Jenkins 的官方插件,提供了在 Pipeline 中执行远程 SSH 命令的功能。
安装插件
# 通过 Jenkins Web 界面安装
1. 访问 Jenkins 管理界面
2. 进入 "Manage Jenkins" → "Manage Plugins"
3. 切换到 "Available" 标签
4. 搜索 "SSH Pipeline Steps"
5. 勾选并安装插件
# 或者通过 Groovy 脚本安装
Jenkins.instance.pluginManager.install("org.jenkinsci.plugins.ssh-steps")
Jenkins.instance.save()
插件功能
sshCommand:执行远程命令sshPut:上传文件到远程服务器sshGet:从远程服务器下载文件sshRemove:删除远程文件sshScript:执行远程脚本
2. 系统要求
Jenkins 版本要求
# 最低版本要求
Jenkins: 2.204.6+
SSH Pipeline Steps Plugin: 1.6+
# 推荐版本
Jenkins: 2.400+
SSH Pipeline Steps Plugin: 2.0+
网络和权限要求
# 网络连通性
- Jenkins 服务器能够访问目标服务器
- SSH 端口(默认 22)开放
- 防火墙规则允许 Jenkins IP 访问
# 权限要求
- Jenkins 用户具有执行 Pipeline 的权限
- 目标服务器的 SSH 访问权限
- 远程目录的读写权限
二、用户名密码认证方式
1. 凭据配置
创建用户名密码凭据
# 1. 进入 Jenkins 凭据管理
1. 访问 "Credentials" → "System" → "Global credentials (unrestricted)"
2. 点击 "Add Credentials"
# 2. 配置凭据信息
Kind: Username with password
Scope: Global
Username: your_username
Password: your_password
ID: ssh-credential-password
Description: SSH 认证凭据 - 用户名密码
# 3. 保存凭据
点击 "OK" 保存凭据配置
凭据安全最佳实践
# 1. 使用强密码
- 密码长度至少 12 位
- 包含大小写字母、数字和特殊字符
- 定期更换密码
# 2. 限制凭据使用范围
- 设置特定的文件夹权限
- 限制用户访问权限
# 3. 启用凭据加密
- 确保 Jenkins 启用了凭据加密
- 定期备份加密密钥
2. Pipeline 配置
基础用户名密码认证
pipeline {
agent any
stages {
stage('SSH Authentication') {
steps {
script {
// 定义远程连接参数
def remote = [:]
remote.name = 'production-server'
remote.host = '192.168.1.100'
remote.allowAnyHosts = true
// 使用用户名密码凭据
withCredentials([
usernamePassword(
credentialsId: 'ssh-credential-password',
passwordVariable: 'password',
usernameVariable: 'username'
)
]) {
// 设置认证信息
remote.user = "${username}"
remote.password = "${password}"
// 执行远程命令
sshCommand remote: remote, command: "pwd"
sshCommand remote: remote, command: "whoami"
sshCommand remote: remote, command: "uname -a"
}
}
}
}
}
}
高级用户名密码认证
pipeline {
agent any
stages {
stage('Advanced SSH Operations') {
steps {
script {
def remote = [:]
remote.name = 'target-server'
remote.host = '192.168.1.100'
remote.allowAnyHosts = true
withCredentials([
usernamePassword(
credentialsId: 'ssh-credential-password',
passwordVariable: 'password',
usernameVariable: 'username'
)
]) {
remote.user = "${username}"
remote.password = "${password}"
try {
// 执行多个远程命令
def commands = [
"echo 'Starting deployment...'",
"cd /opt/application",
"git pull origin main",
"npm install",
"pm2 restart app"
]
commands.each { cmd ->
echo "Executing: ${cmd}"
def result = sshCommand remote: remote, command: cmd
echo "Result: ${result}"
}
} catch (Exception e) {
error "SSH operation failed: ${e.message}"
}
}
}
}
}
}
}
3. 文件操作示例
上传文件到远程服务器
pipeline {
agent any
stages {
stage('File Upload') {
steps {
script {
def remote = [:]
remote.name = 'upload-server'
remote.host = '192.168.1.100'
remote.allowAnyHosts = true
withCredentials([
usernamePassword(
credentialsId: 'ssh-credential-password',
passwordVariable: 'password',
usernameVariable: 'username'
)
]) {
remote.user = "${username}"
remote.password = "${password}"
// 创建本地文件
writeFile file: 'deploy.sh', text: '''#!/bin/bash
echo "Deployment script started"
cd /opt/application
git pull origin main
npm install
pm2 restart app
echo "Deployment completed"
'''
// 上传文件
sshPut remote: remote, from: 'deploy.sh', into: '/tmp/'
// 设置文件权限
sshCommand remote: remote, command: "chmod +x /tmp/deploy.sh"
// 执行上传的脚本
sshCommand remote: remote, command: "/tmp/deploy.sh"
}
}
}
}
}
}
三、SSH 密钥认证方式
1. SSH 密钥对生成
生成 SSH 密钥
# 1. 在 Jenkins 服务器上生成密钥对
ssh-keygen -t rsa -b 4096 -C "jenkins-deploy-key" -f ~/.ssh/jenkins-deploy
# 2. 查看生成的密钥
ls -la ~/.ssh/jenkins-deploy*
# 输出:
# ~/.ssh/jenkins-deploy (私钥)
# ~/.ssh/jenkins-deploy.pub (公钥)
# 3. 查看公钥内容
cat ~/.ssh/jenkins-deploy.pub
配置目标服务器
# 1. 在目标服务器上创建 .ssh 目录
ssh user@target-server "mkdir -p ~/.ssh"
# 2. 复制公钥到目标服务器
ssh-copy-id -i ~/.ssh/jenkins-deploy.pub user@target-server
# 3. 或者手动添加公钥
cat ~/.ssh/jenkins-deploy.pub | ssh user@target-server "cat >> ~/.ssh/authorized_keys"
# 4. 设置正确的权限
ssh user@target-server "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"
# 5. 测试 SSH 连接
ssh -i ~/.ssh/jenkins-deploy user@target-server "echo 'SSH key authentication successful'"
2. Jenkins 凭据配置
创建 SSH 密钥凭据
# 1. 进入 Jenkins 凭据管理
Credentials → System → Global credentials (unrestricted) → Add Credentials
# 2. 配置 SSH 密钥凭据
Kind: SSH Username with private key
Scope: Global
Username: deploy_user
Private Key:
☑ Enter directly
[粘贴私钥内容]
[可选] Passphrase: 如果私钥有密码保护
ID: ssh-credential-key
Description: SSH 认证凭据 - 密钥认证
# 3. 保存凭据
点击 "OK" 保存
私钥格式示例
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAwJ5v9...
... (私钥内容) ...
-----END RSA PRIVATE KEY-----
3. Pipeline 配置
基础 SSH 密钥认证
pipeline {
agent any
stages {
stage('SSH Key Authentication') {
steps {
script {
def remote = [:]
remote.name = 'key-auth-server'
remote.host = '192.168.1.100'
remote.allowAnyHosts = true
withCredentials([
sshUserPrivateKey(
credentialsId: 'ssh-credential-key',
keyFileVariable: 'identity',
passphraseVariable: '',
usernameVariable: 'userName'
)
]) {
// 设置认证信息
remote.user = userName
remote.identityFile = identity
// 重要:所有远程操作必须在 withCredentials 块内执行
sshCommand remote: remote, command: 'pwd'
sshCommand remote: remote, command: 'whoami'
sshCommand remote: remote, command: 'uname -a'
}
}
}
}
}
}
高级 SSH 密钥认证
pipeline {
agent any
stages {
stage('Advanced Key Operations') {
steps {
script {
def remote = [:]
remote.name = 'production-server'
remote.host = '192.168.1.100'
remote.allowAnyHosts = true
withCredentials([
sshUserPrivateKey(
credentialsId: 'ssh-credential-key',
keyFileVariable: 'identity',
passphraseVariable: '',
usernameVariable: 'userName'
)
]) {
remote.user = userName
remote.identityFile = identity
try {
// 创建远程脚本
def deploymentScript = '''#!/bin/bash
set -e
echo "Starting deployment at $(date)"
cd /opt/application
# 备份当前版本
if [ -d "current" ]; then
cp -r current backup-$(date +%Y%m%d-%H%M%S)
fi
# 拉取最新代码
git pull origin main
# 安装依赖
npm install
# 运行测试
npm test
# 重启服务
pm2 restart app
echo "Deployment completed at $(date)"
'''
// 写入远程脚本文件
writeFile file: 'deploy.sh', text: deploymentScript
// 上传脚本到远程服务器
sshPut remote: remote, from: 'deploy.sh', into: '/tmp/'
// 设置执行权限
sshCommand remote: remote, command: 'chmod +x /tmp/deploy.sh'
// 执行部署脚本
def result = sshCommand remote: remote, command: '/tmp/deploy.sh'
echo "Deployment result: ${result}"
// 清理临时文件
sshCommand remote: remote, command: 'rm /tmp/deploy.sh'
} catch (Exception e) {
error "Deployment failed: ${e.message}"
}
}
}
}
}
}
}
四、最佳实践和安全考虑
1. 凭据管理最佳实践
凭据命名规范
# 推荐的凭据 ID 命名规范
ssh-credential-{environment}-{type}
# 示例:
ssh-credential-prod-key # 生产环境密钥
ssh-credential-dev-password # 开发环境密码
ssh-credential-staging-key # 测试环境密钥
凭据使用原则
// 1. 使用环境特定的凭据
def environment = env.BRANCH_NAME == 'main' ? 'prod' : 'dev'
def credentialId = "ssh-credential-${environment}-key"
// 2. 验证凭据存在
try {
withCredentials([
sshUserPrivateKey(
credentialsId: credentialId,
keyFileVariable: 'identity',
usernameVariable: 'userName'
)
]) {
// 使用凭据
}
} catch (Exception e) {
error "Credential ${credentialId} not found: ${e.message}"
}
// 3. 限制凭据作用域
withCredentials([/* 凭据配置 */]) {
// 所有需要使用凭据的操作都在这个块内
// 凭据在块外自动清理
}
2. 网络安全配置
SSH 服务器安全配置
# /etc/ssh/sshd_config 推荐配置
Port 22
Protocol 2
PermitRootLogin no
PasswordAuthentication no # 强制使用密钥认证
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
# 重启 SSH 服务
systemctl restart sshd
防火墙配置
# 使用 firewalld 限制 SSH 访问
firewall-cmd --permanent --add-rich-rule="rule family='ipv4' source address='192.168.1.10' port protocol='tcp' port='22' accept"
firewall-cmd --reload
# 使用 iptables 限制访问
iptables -A INPUT -p tcp -s 192.168.1.10 --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP
3. 错误处理和日志记录
完善的错误处理
pipeline {
agent any
stages {
stage('Secure SSH Operations') {
steps {
script {
def remote = [:]
remote.name = 'secure-server'
remote.host = '192.168.1.100'
remote.allowAnyHosts = true
withCredentials([
sshUserPrivateKey(
credentialsId: 'ssh-credential-key',
keyFileVariable: 'identity',
usernameVariable: 'userName'
)
]) {
remote.user = userName
remote.identityFile = identity
try {
// 连接测试
sshCommand remote: remote, command: 'echo "Connection test successful"'
// 执行部署命令
def commands = [
"echo 'Starting deployment'",
"cd /opt/application",
"git status",
"git pull origin main",
"npm install --production",
"pm2 reload app"
]
commands.eachWithIndex { cmd, index ->
echo "Step ${index + 1}: ${cmd}"
try {
def result = sshCommand remote: remote, command: cmd, timeout: 300
echo "Success: ${result}"
} catch (Exception e) {
error "Command failed at step ${index + 1}: ${cmd} - ${e.message}"
}
}
echo "Deployment completed successfully"
} catch (Exception e) {
// 记录错误并尝试回滚
echo "Deployment failed: ${e.message}"
try {
sshCommand remote: remote, command: "echo 'Attempting rollback...'"
sshCommand remote: remote, command: "cd /opt/application && git checkout HEAD~1"
sshCommand remote: remote, command: "pm2 reload app"
echo "Rollback completed"
} catch (rollbackException) {
error "Rollback also failed: ${rollbackException.message}"
}
error "Deployment process failed"
}
}
}
}
}
}
}
详细的日志记录
pipeline {
agent any
stages {
stage('Logged SSH Operations') {
steps {
script {
def logFile = "ssh-operations-${new Date().format('yyyyMMdd-HHmmss')}.log"
def remote = [:]
remote.name = 'logging-server'
remote.host = '192.168.1.100'
remote.allowAnyHosts = true
withCredentials([
sshUserPrivateKey(
credentialsId: 'ssh-credential-key',
keyFileVariable: 'identity',
usernameVariable: 'userName'
)
]) {
remote.user = userName
remote.identityFile = identity
// 记录操作开始
def startLog = "SSH operation started at ${new Date()}"
echo startLog
writeFile file: logFile, text: "${startLog}\n"
// 执行命令并记录
def commands = [
'pwd',
'whoami',
'df -h',
'free -m'
]
commands.each { cmd ->
try {
def result = sshCommand remote: remote, command: cmd
def logEntry = "SUCCESS: ${cmd}\n${result}\n"
echo logEntry
appendFile file: logFile, text: logEntry
} catch (Exception e) {
def logEntry = "FAILED: ${cmd}\n${e.message}\n"
echo logEntry
appendFile file: logFile, text: logEntry
}
}
// 归档日志文件
archiveArtifacts artifacts: logFile, fingerprint: true
}
}
}
}
}
}
五、故障排除
1. 常见错误和解决方案
凭据相关错误
# 错误 1: Credentials not found
ERROR: Unable to resolve credentialsId 'ssh-credential-key'
解决方案:
1. 检查凭据 ID 是否正确
2. 确认凭据存在且权限正确
3. 检查凭据类型是否匹配
# 错误 2: Authentication failed
ERROR: Authentication failed for user 'deploy_user'
解决方案:
1. 验证用户名和密码是否正确
2. 检查 SSH 密钥是否正确配置
3. 确认目标服务器的 SSH 配置
# 错误 3: Host key verification failed
ERROR: Host key verification failed
解决方案:
1. 设置 remote.allowAnyHosts = true (仅用于测试环境)
2. 或添加目标服务器到 known_hosts
网络连接错误
# 错误 1: Connection refused
ERROR: Connection refused
解决方案:
1. 检查目标服务器 SSH 服务是否运行
2. 验证端口是否正确
3. 检查防火墙设置
# 错误 2: Connection timeout
ERROR: Connection timeout
解决方案:
1. 检查网络连通性
2. 增加连接超时时间
3. 验证 DNS 解析是否正确
# 错误 3: Permission denied
ERROR: Permission denied (publickey,password)
解决方案:
1. 检查认证方式是否正确
2. 验证密钥文件权限
3. 确认用户权限设置
2. 调试技巧
启用详细日志
pipeline {
agent any
stages {
stage('Debug SSH Connection') {
steps {
script {
// 启用 Jenkins 全局调试
// 在 Jenkins 系统设置中设置日志级别为 ALL
def remote = [:]
remote.name = 'debug-server'
remote.host = '192.168.1.100'
remote.allowAnyHosts = true
withCredentials([
sshUserPrivateKey(
credentialsId: 'ssh-credential-key',
keyFileVariable: 'identity',
usernameVariable: 'userName'
)
]) {
remote.user = userName
remote.identityFile = identity
// 添加调试信息
echo "Remote configuration:"
echo " Host: ${remote.host}"
echo " User: ${remote.user}"
echo " Identity file: ${remote.identityFile}"
echo " Allow any hosts: ${remote.allowAnyHosts}"
// 测试基本连接
try {
sshCommand remote: remote, command: 'echo "SSH connection successful"', verbose: true
} catch (Exception e) {
echo "SSH connection failed: ${e.toString()}"
echo "Stack trace:"
echo e.getStackTrace().join('\n')
throw e
}
}
}
}
}
}
}
连接测试脚本
pipeline {
agent any
stages {
stage('Connection Test') {
steps {
script {
def testConnection = { host, credentialId, userName ->
def remote = [:]
remote.name = "test-${host}"
remote.host = host
remote.allowAnyHosts = true
try {
withCredentials([
sshUserPrivateKey(
credentialsId: credentialId,
keyFileVariable: 'identity',
usernameVariable: 'username'
)
]) {
remote.user = username
remote.identityFile = identity
def result = sshCommand remote: remote, command: 'echo "Connection test successful"'
echo "✅ ${host}: Connection successful"
return true
}
} catch (Exception e) {
echo "❌ ${host}: Connection failed - ${e.message}"
return false
}
}
// 测试多个服务器
def servers = [
[host: '192.168.1.100', credential: 'ssh-credential-key', user: 'deploy'],
[host: '192.168.1.101', credential: 'ssh-credential-key', user: 'deploy'],
[host: '192.168.1.102', credential: 'ssh-credential-key', user: 'deploy']
]
def results = servers.collect { server ->
testConnection(server.host, server.credential, server.user)
}
def failedCount = results.count { !it }
if (failedCount > 0) {
error "${failedCount} servers failed connection test"
}
}
}
}
}
}
六、高级应用场景
1. 多环境部署
pipeline {
agent any
parameters {
choice(
name: 'ENVIRONMENT',
choices: ['dev', 'staging', 'production'],
description: '选择部署环境'
)
}
stages {
stage('Deploy to Environment') {
steps {
script {
def envConfig = [
dev: [
host: '192.168.1.100',
credential: 'ssh-credential-dev-key',
user: 'deploy',
path: '/opt/app-dev'
],
staging: [
host: '192.168.1.101',
credential: 'ssh-credential-staging-key',
user: 'deploy',
path: '/opt/app-staging'
],
production: [
host: '192.168.1.102',
credential: 'ssh-credential-prod-key',
user: 'deploy',
path: '/opt/app-prod'
]
]
def config = envConfig[params.ENVIRONMENT]
def remote = [:]
remote.name = "${params.ENVIRONMENT}-server"
remote.host = config.host
remote.allowAnyHosts = true
withCredentials([
sshUserPrivateKey(
credentialsId: config.credential,
keyFileVariable: 'identity',
usernameVariable: 'userName'
)
]) {
remote.user = userName
remote.identityFile = identity
// 环境特定的部署逻辑
def deployScript = """
cd ${config.path}
git pull origin ${params.ENVIRONMENT}
npm install --production
pm2 reload app-${params.ENVIRONMENT}
"""
sshCommand remote: remote, command: deployScript
echo "Successfully deployed to ${params.ENVIRONMENT}"
}
}
}
}
}
}
2. 批量服务器操作
pipeline {
agent any
stages {
stage('Batch Operations') {
steps {
script {
def serverList = [
[name: 'web-server-1', host: '192.168.1.100', role: 'web'],
[name: 'web-server-2', host: '192.168.1.101', role: 'web'],
[name: 'db-server-1', host: '192.168.1.102', role: 'database'],
[name: 'cache-server-1', host: '192.168.1.103', role: 'cache']
]
def executeOnServers = { servers, command, role = null ->
def results = [:]
servers.each { server ->
if (role == null || server.role == role) {
def remote = [:]
remote.name = server.name
remote.host = server.host
remote.allowAnyHosts = true
try {
withCredentials([
sshUserPrivateKey(
credentialsId: 'ssh-credential-key',
keyFileVariable: 'identity',
usernameVariable: 'userName'
)
]) {
remote.user = userName
remote.identityFile = identity
def result = sshCommand remote: remote, command: command
results[server.name] = [status: 'SUCCESS', output: result]
echo "✅ ${server.name}: ${command} - SUCCESS"
}
} catch (Exception e) {
results[server.name] = [status: 'FAILED', error: e.message]
echo "❌ ${server.name}: ${command} - FAILED - ${e.message}"
}
}
}
return results
}
// 在所有服务器上执行系统更新
echo "Executing system updates on all servers..."
def updateResults = executeOnServers(serverList, "sudo apt update && sudo apt upgrade -y")
// 只在 Web 服务器上重启 Nginx
echo "Restarting Nginx on web servers..."
def nginxResults = executeOnServers(serverList, "sudo systemctl restart nginx", 'web')
// 检查服务状态
echo "Checking service status..."
def statusResults = executeOnServers(serverList, "sudo systemctl status nginx | grep Active")
// 汇总结果
echo "=== Operation Summary ==="
echo "Update Results: ${updateResults.count { it.value.status == 'SUCCESS' }}/${updateResults.size()} successful"
echo "Nginx Results: ${nginxResults.count { it.value.status == 'SUCCESS' }}/${nginxResults.size()} successful"
echo "Status Results: ${statusResults.count { it.value.status == 'SUCCESS' }}/${statusResults.size()} successful"
}
}
}
}
}
3. 动态凭据管理
pipeline {
agent any
stages {
stage('Dynamic Credential Management') {
steps {
script {
// 动态获取凭据列表
def credentialsAvailable = Jenkins.instance.getExtensionList('com.cloudbees.plugins.credentials.SystemCredentialsProvider')[0].getCredentials()
def sshCredentials = credentialsAvailable.findAll {
it instanceof com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey
}
echo "Available SSH Credentials:"
sshCredentials.each { cred ->
echo " ID: ${cred.id}, Username: ${cred.username}, Description: ${cred.description}"
}
// 选择合适的环境凭据
def branchName = env.BRANCH_NAME
def credentialId = null
switch (branchName) {
case 'main':
credentialId = 'ssh-credential-prod-key'
break
case 'develop':
credentialId = 'ssh-credential-dev-key'
break
default:
credentialId = 'ssh-credential-feature-key'
break
}
// 验证凭据存在
def targetCredential = sshCredentials.find { it.id == credentialId }
if (!targetCredential) {
error "Credential ${credentialId} not found for branch ${branchName}"
}
echo "Using credential: ${credentialId} for branch: ${branchName}"
// 使用动态选择的凭据
def remote = [:]
remote.name = 'dynamic-server'
remote.host = '192.168.1.100'
remote.allowAnyHosts = true
withCredentials([
sshUserPrivateKey(
credentialsId: credentialId,
keyFileVariable: 'identity',
usernameVariable: 'userName'
)
]) {
remote.user = userName
remote.identityFile = identity
sshCommand remote: remote, command: "echo 'Deployed from branch: ${branchName}'"
}
}
}
}
}
}
总结
通过本文的详细指南,我们掌握了 Jenkins Pipeline 中 SSH 认证的两种主要方式:
核心要点
- 用户名密码认证:简单直接,适合测试环境
- SSH 密钥认证:更安全,推荐用于生产环境
- 凭据管理:安全的凭据存储和使用
- 错误处理:完善的异常处理和日志记录
- 安全配置:网络安全最佳实践
最佳实践总结
- 安全性优先:生产环境强制使用 SSH 密钥认证
- 凭据隔离:不同环境使用不同的凭据
- 完整日志:记录所有远程操作便于审计
- 错误处理:提供详细的错误信息和回滚机制
- 网络限制:限制 SSH 访问的源 IP 地址
应用场景
- 自动化部署:代码提交后自动部署到测试/生产环境
- 批量运维:在多台服务器上执行相同的操作
- 监控检查:定期检查服务器状态和服务健康度
- 数据同步:在多台服务器间同步配置或数据
通过合理使用这些 SSH 认证方式,可以构建安全、可靠的 Jenkins 自动化部署流水线,大大提升运维效率和系统安全性。
整理时间: 2026年2月11日 整理者: 来财 (OpenClaw AI助手) 参考来源: Jenkins 官方文档 + SSH Pipeline Steps 插件文档 + 企业实践案例