2024-11-17 00:16:46
近期阿里云双十一活动 RDS 99/年,非常有性价比,于是我就买了一个香港区的 MySQL RDS 用做我的 Memos 数据服务。这就不可避免的碰到了数据库迁移的问题,即需要将 SQLite 数据库迁移到 MySQL,这种生产环境的数据库迁移一般都非常谨慎,需要严格 SQL 开发和验证,需要花费不少的时间。由于 SQLite 文件有备份,刚刚购买的 MySQL 资源充足,心血来潮之下想玩一波大的 —— 试试 AI 来帮我完成生产数据库的迁移工作。
我完成这些事情是 prompt tune 的思路,主要包括:
这里主要是确认当前生产数据创建了哪些库表,有两种方法:
show create table ${table name}
CREATE DATABASE IF NOT EXISTS memos-migrate-test;
version: '3'
services:
db:
image: mysql:latest
ports:
- 3306:3306
restart: always
environment:
MYSQL_ROOT_PASSWORD: your_root_password
volumes:
- ./data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
docker pull mysql
docker compose up -d
I have a notes service and the data is stored on sqlite database, now i want to migrate all the data to mysql, please help me to write a script in python. Here is the schema definition of the sqlite database
<<<LATEST.sql>>>
注意:替换 LATEST.sql 的内容。
当前具备 code 分析能力的 LLM 均可以用来做迁移工具的 prompt tune,由于 Claude 在代码分析和生成等领域有非常不错的口碑,所以我这次使用了 Claude 3.5 Sonnet 作为调优模型。
Here's a Python script that can help you migrate data from SQLite to MySQL based on the schema you provided:
\`\`\`python
import sqlite3
import mysql.connector
import json
# SQLite connection
sqlite_conn = sqlite3.connect('path_to_your_sqlite_database.db')
sqlite_cursor = sqlite_conn.cursor()
# MySQL connection
mysql_conn = mysql.connector.connect(
host="your_mysql_host",
user="your_mysql_user",
password="your_mysql_password",
database="your_mysql_database"
)
mysql_cursor = mysql_conn.cursor()
# List of tables to migrate
tables = [
"migration_history", "system_setting", "user", "user_setting", "memo",
"memo_organizer", "memo_relation", "resource", "activity", "idp",
"inbox", "webhook", "reaction"
]
for table in tables:
print(f"Migrating table: {table}")
# Fetch data from SQLite
sqlite_cursor.execute(f"SELECT * FROM {table}")
rows = sqlite_cursor.fetchall()
if not rows:
print(f"No data in table {table}")
continue
# Get column names
columns = [description[0] for description in sqlite_cursor.description]
# Prepare MySQL insert query
placeholders = ', '.join(['%s'] * len(columns))
columns_string = ', '.join(columns)
insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"
# Insert data into MySQL
for row in rows:
# Convert any JSON fields
processed_row = []
for value, column in zip(row, columns):
if column in ['tags', 'payload']:
try:
processed_value = json.dumps(json.loads(value))
except:
processed_value = value
else:
processed_value = value
processed_row.append(processed_value)
try:
mysql_cursor.execute(insert_query, processed_row)
except mysql.connector.Error as err:
print(f"Error inserting into {table}: {err}")
continue
mysql_conn.commit()
print(f"Migrated {len(rows)} rows for table {table}")
# Close connections
sqlite_conn.close()
mysql_conn.close()
print("Migration completed")
\`\`\`
Before running this script, make sure to:
1. Install required packages: `pip install mysql-connector-python`
2. Replace `'path_to_your_sqlite_database.db'` with the actual path to your SQLite database file.
3. Update the MySQL connection details (host, user, password, database) with your actual MySQL server information.
This script does the following:
1. Connects to both SQLite and MySQL databases.
2. Iterates through each table in the given list.
3. Fetches all data from the SQLite table.
4. Prepares an insert query for MySQL.
5. Inserts the data into the corresponding MySQL table, handling JSON fields if necessary.
6. Commits the changes and moves to the next table.
Note that this script assumes that you have already created the tables in MySQL with the same schema as in SQLite. If you haven't, you'll need to create the tables in MySQL first using the CREATE TABLE statements you provided.
Also, be aware that this script does not handle auto-increment fields specially. If you have auto-increment fields and want to preserve the exact IDs from SQLite, you may need to modify the script to handle this (e.g., by temporarily disabling auto-increment in MySQL during the migration).
prompt tuning 就是在测试环境中执行 Claude 生成的工具,并将测试的报错内容反馈给 Claude 让它继续进行优化调整,并重新生成迁移工具。
测试初始化 prompt 步骤中的迁移工具,发现有如下报错:
LATEST.sql
)进行测试发现有时间戳相关报错:
migrate error: Incorrect datetime value: '1698320364' for column 'created_ts'
I apologize for the oversight. The error you're encountering is due to a difference in how SQLite and MySQL handle datetime values. SQLite often stores timestamps as Unix timestamps (seconds since epoch), while MySQL expects a formatted datetime string.
To resolve this issue, we need to convert the Unix timestamp to a datetime format that MySQL can understand. Here's an updated version of the script that handles this conversion:
\`\`\`python
import sqlite3
import mysql.connector
import json
from datetime import datetime
# SQLite connection
sqlite_conn = sqlite3.connect('path_to_your_sqlite_database.db')
sqlite_cursor = sqlite_conn.cursor()
# MySQL connection
mysql_conn = mysql.connector.connect(
host="your_mysql_host",
user="your_mysql_user",
password="your_mysql_password",
database="your_mysql_database"
)
mysql_cursor = mysql_conn.cursor()
# List of tables to migrate
tables = [
"migration_history", "system_setting", "user", "user_setting", "memo",
"memo_organizer", "memo_relation", "resource", "activity", "idp",
"inbox", "webhook", "reaction"
]
# List of timestamp columns
timestamp_columns = ['created_ts', 'updated_ts']
def convert_timestamp(value):
try:
return datetime.fromtimestamp(int(value)).strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
return value
for table in tables:
print(f"Migrating table: {table}")
# Fetch data from SQLite
sqlite_cursor.execute(f"SELECT * FROM {table}")
rows = sqlite_cursor.fetchall()
if not rows:
print(f"No data in table {table}")
continue
# Get column names
columns = [description[0] for description in sqlite_cursor.description]
# Prepare MySQL insert query
placeholders = ', '.join(['%s'] * len(columns))
columns_string = ', '.join(columns)
insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"
# Insert data into MySQL
for row in rows:
# Convert any JSON fields and timestamps
processed_row = []
for value, column in zip(row, columns):
if column in ['tags', 'payload']:
try:
processed_value = json.dumps(json.loads(value))
except:
processed_value = value
elif column in timestamp_columns:
processed_value = convert_timestamp(value)
else:
processed_value = value
processed_row.append(processed_value)
try:
mysql_cursor.execute(insert_query, processed_row)
except mysql.connector.Error as err:
print(f"Error inserting into {table}: {err}")
continue
mysql_conn.commit()
print(f"Migrated {len(rows)} rows for table {table}")
# Close connections
sqlite_conn.close()
mysql_conn.close()
print("Migration completed")
\`\`\`
The main changes in this updated script are:
1. We've added a list of `timestamp_columns` that need to be converted.
2. We've added a `convert_timestamp` function that converts Unix timestamps to MySQL datetime format.
3. In the data processing loop, we now check if a column is in the `timestamp_columns` list and convert it if necessary.
This script should handle the timestamp conversion correctly. If you encounter any other issues or if some tables have different datetime formats, you may need to adjust the `convert_timestamp` function or add more specific handling for certain tables or columns.
Also, make sure that your MySQL tables are set up to accept NULL values for timestamp columns if applicable, as some of your SQLite data might contain NULL values for these fields.
再次进行测试发现有保留关键字导致语法相关的报错:
Error inserting into user_setting: 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'key, value) VALUES (1, 'USER_SETTING_ACCESS_TOKENS', '{\"accessTokens\":[{\"acce' at line 1
I apologize for the oversight. The error is occurring because 'blob' and 'type' are also reserved keywords in MySQL. We need to escape these column names as well. Here's an updated version of the script that handles this issue:
\`\`\`python
import sqlite3
import mysql.connector
import json
from datetime import datetime
# SQLite connection
sqlite_conn = sqlite3.connect('path_to_your_sqlite_database.db')
sqlite_cursor = sqlite_conn.cursor()
# MySQL connection
mysql_conn = mysql.connector.connect(
host="your_mysql_host",
user="your_mysql_user",
password="your_mysql_password",
database="your_mysql_database"
)
mysql_cursor = mysql_conn.cursor()
# List of tables to migrate
tables = [
"migration_history", "system_setting", "user", "user_setting", "memo",
"memo_organizer", "memo_relation", "resource", "activity", "idp",
"inbox", "webhook", "reaction"
]
# List of timestamp columns
timestamp_columns = ['created_ts', 'updated_ts']
# List of reserved keywords that need to be escaped
reserved_keywords = ['key', 'value', 'blob', 'type']
def convert_timestamp(value):
try:
return datetime.fromtimestamp(int(value)).strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
return value
def escape_column_names(columns):
return [f"`{column}`" if column.lower() in reserved_keywords else column for column in columns]
for table in tables:
print(f"Migrating table: {table}")
# Fetch data from SQLite
sqlite_cursor.execute(f"SELECT * FROM {table}")
rows = sqlite_cursor.fetchall()
if not rows:
print(f"No data in table {table}")
continue
# Get column names
columns = [description[0] for description in sqlite_cursor.description]
# Escape column names if necessary
escaped_columns = escape_column_names(columns)
# Prepare MySQL insert query
placeholders = ', '.join(['%s'] * len(columns))
columns_string = ', '.join(escaped_columns)
insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"
# Insert data into MySQL
for row in rows:
# Convert any JSON fields and timestamps
processed_row = []
for value, column in zip(row, columns):
if column in ['tags', 'payload']:
try:
processed_value = json.dumps(json.loads(value))
except:
processed_value = value
elif column in timestamp_columns:
processed_value = convert_timestamp(value)
else:
processed_value = value
processed_row.append(processed_value)
try:
mysql_cursor.execute(insert_query, processed_row)
except mysql.connector.Error as err:
print(f"Error inserting into {table}: {err}")
print(f"Problematic row: {processed_row}")
continue
mysql_conn.commit()
print(f"Migrated {len(rows)} rows for table {table}")
# Close connections
sqlite_conn.close()
mysql_conn.close()
print("Migration completed")
\`\`\`
The main changes in this updated script are:
1. We've added 'blob' and 'type' to the list of reserved keywords that need to be escaped.
2. The `escape_column_names` function now checks against this expanded list of reserved keywords.
3. I've added a line to print the problematic row when an error occurs, which can help with debugging if you encounter any other issues.
This change should resolve the SQL syntax error you encountered with the `resource` table. The script now properly escapes the 'blob' and 'type' column names, which should allow the insertion into the `resource` table to proceed without errors.
If you encounter any other issues, please let me know, and I'll be happy to help further. Also, remember that if your `blob` column contains large binary data, you might need to
调优完成之后,就可以向正式的 MySQL 数据库实例进行数据迁移了,主要注意点的:
LATEST.sql
进行初始化库表import sqlite3
import mysql.connector
import json
from datetime import datetime
# SQLite connection
sqlite_conn = sqlite3.connect('~/.memos/memos_prod.db')
sqlite_cursor = sqlite_conn.cursor()
# MySQL connection
mysql_conn = mysql.connector.connect(
host="xxxxxx.rds.aliyuncs.com",
user="xxxxxxxxxxxxxxxxx",
password="xxxxxxxxxxxxx",
database="xxxxxxxxxxxxx"
)
mysql_cursor = mysql_conn.cursor()
# List of tables to migrate
tables = [
"migration_history", "system_setting", "user", "user_setting", "memo",
"memo_organizer", "memo_relation", "resource", "activity", "idp",
"inbox", "webhook", "reaction"
]
# List of timestamp columns
timestamp_columns = ['created_ts', 'updated_ts']
# List of reserved keywords that need to be escaped
reserved_keywords = ['key', 'value', 'blob', 'type']
def convert_timestamp(value):
try:
return datetime.fromtimestamp(int(value)).strftime('%Y-%m-%d %H:%M:%S')
except ValueError:
return value
def escape_column_names(columns):
return [f"`{column}`" if column.lower() in reserved_keywords else column for column in columns]
for table in tables:
print(f"Migrating table: {table}")
# Fetch data from SQLite
sqlite_cursor.execute(f"SELECT * FROM {table}")
rows = sqlite_cursor.fetchall()
if not rows:
print(f"No data in table {table}")
continue
# Get column names
columns = [description[0] for description in sqlite_cursor.description]
# Escape column names if necessary
escaped_columns = escape_column_names(columns)
# Prepare MySQL insert query
placeholders = ', '.join(['%s'] * len(columns))
columns_string = ', '.join(escaped_columns)
insert_query = f"INSERT INTO {table} ({columns_string}) VALUES ({placeholders})"
# Insert data into MySQL
for row in rows:
# Convert any JSON fields and timestamps
processed_row = []
for value, column in zip(row, columns):
if column in ['tags', 'payload']:
try:
processed_value = json.dumps(json.loads(value))
except:
processed_value = value
elif column in timestamp_columns:
processed_value = convert_timestamp(value)
else:
processed_value = value
processed_row.append(processed_value)
try:
mysql_cursor.execute(insert_query, processed_row)
except mysql.connector.Error as err:
print(f"Error inserting into {table}: {err}")
print(f"Problematic row: {processed_row}")
continue
mysql_conn.commit()
print(f"Migrated {len(rows)} rows for table {table}")
# Close connections
sqlite_conn.close()
mysql_conn.close()
print("Migration completed")
2024-11-10 00:17:32
双十一了,好物清单、什么值得买之类的内容铺天盖地,到底我们在消费的时候受到哪些东西影响,我觉得需要研究一下消费决策模型。根据恩格尔(Engel)等提出的 EBK 模型可以看出消费时原来是这么样复杂,哪怕买瓶水、买个包子都无形中被这么多因素影响着。为了自己的钱包,我觉得很有必要建立属于自己的消费决策模型。
根据 EKB 模型,消费决策是由五步构成的:
当人们认为理想与实际之间有差距时,便产生了问题,问题产生后则整个系统(内外)便开始运作,目标也化成了具体的行动。
例如:工作了半天感觉肚子饿了,继续饿着就没力气干活了,看来需要找点吃的了。
当人们发现了问题的存在,便会去搜寻相关的信息,信息搜寻又可分为内部搜寻与外部搜寻两种,所谓内部搜寻是指在其现有资料或过去的购买经验中去寻找。当内部搜寻无法满足其需要时,便会转由外部寻找,所谓的外部搜寻诸如大众传播媒体、行销人员及亲友等。
例如:
收集到信息之后就需要判断一下哪个更好,哪个跟适合。
例如:
当评估了各种可能的方案后,便会选择一个最能解决原始问题的方案并采取消费行为。
例如:
当消费并使用了某产品后,可能发生满意或购买认知失调两种结果,并存储于记忆当中。
例如:
商家为了刺激消费,常常会巧妙地运用心理学知识。这些心理技巧潜移默化地影响着我们的消费决策。下面就来详细介绍一些常见的商家常用的心理学知识:
理解消费决策的心理过程并据此做出明智的消费选择是至关重要的。下面提供了一个优化后、更具可操作性的消费决策分析和思考方法,整合了心理学原理和实践指导:
最后分享一个我自己正在尝试使用的简单消费决策逻辑:
使用说明:
2024-11-03 22:27:45
真诚的人从不公开表达自己的情感,真正的情感总是蒙了一层面纱。只有虚伪的人才用情感作秀,为了给大家看他们有多敏感。
我本性不善言辞,却忙于人际周旋;我本喜欢独处,却被生活推着奔波前行。
而所做的一切,都是为了得到那解万千惆怅的碎银几两。
原来,这个世界不允许我们内向。
大家只在意结果,没人关心你独撑的过程。
时常心累,却只能一笑而过。
有些伤痛,何可言、何能言、何处言;
有些经历,不可说、不能说、不必说。
沉默并非无话可说,只是难以启齿,一言难尽。
人们总是把幸福解读为:“有”
有房,有车,有钱,有权,
但幸福其实是“无”,
无忧,无虑,无病,无灾,
“有”多半是给别人看的,“无”才是你自己的。
你所有的压力,都是因为你太想要了,你所有的痛苦,都是因为你太较真了。有些事不能尽你心意,就在提醒你该转弯了。
如果事事都是如意,那就不叫生活了,所以睡前原谅一切,醒来不问过往,珍惜所有的不期而遇,看淡所有的不辞而别。
人生,一站有一站的风景,一岁有一岁的味道,你的年龄应该成为你生命的勋章而不是你伤感的理由。
生活嘛,慢慢来,你又不差。
人生至福,就是确信有人爱你,有人为你的现状而爱你,说得更准确些,有人不问你如何就爱你。
耳朵是从不犯错误的,惹祸的都是嘴。
2024-10-26 23:21:38
《喜剧之王单口季》总决赛上付航的表演,后劲有点大,忍不住想说点什么……
总决赛上杨天真说会写一个 repo 给付航,于是顺手搜索了一下付航相关的信息,当我设置了 site:douban.com
之后,我震惊地发现清一色的都是男女对立的点评,此类评论大多认为大家叫好、有共鸣是因为付航段子是男性视角的爽文 —— 贤妻扶我凌云志,我还贤妻万两金。因为我没有国内的社交媒体账号,豆瓣、微博上这样的「口诛笔伐」我不会经常看见也挺好的,只是希望付航会继续勇敢地做自己。
19年开始关注脱口秀(standup comedy),第一次看懂文本结构是杨波的 one-linar,第一次看懂脱口秀内核是付航的 passion。所有人都认为脱口秀是冒犯的艺术,所以花样百出的冒犯别人、冒犯世界,付航让我明白好笑的脱口秀不一定需要冒犯别人。
现今的付航早已不是微末,并不需要我的任何非专业解读和夸赞,他给我带来了真切的快乐就很好了。马东曾经在十三邀里面谈起自己的父亲,他是这样说的:被误会是表达者的命运,我继承了我父亲的性格,面对误会选择沉默。
付航最后一段结尾的原文是:「十多年啦!所有人都说我是废物,但是只有你说我是天才。你教会了我人生之中最重要的一件事:不管你的出生、学历、家庭背景,只要你敢勇敢地做自己,就总有人会爱你」。如果付航感谢的人是自己的好哥们、好朋友、家人,他的表达也是成立,只是他很幸运正好是爱自己的女生,我相信这个世界上一定有另外一个“付航”正在因为被他的家人、朋友爱着而勇敢的生活做自己,所以这并不是爽文。
「你必须成为佼佼者才能拥有美好的爱情,体己的朋友,成功的人生,爱你的家人」,诸如此类的社会共识好像也并不完全正确。
总决赛作品里的付航与电影《喜剧之王》里的周星驰在某种程度上是重合的,他们都在一遍遍地强调自己,他们都是小人物,被人轻视,但他们都保留着那份坚韧,最终因梦想而不平凡。但是付航跟周星驰的底我觉得还是有点不一样的,周星驰是小人物经历心酸然后变强赢得结果和尊重。付航是小人物经历心酸也获得了快乐幸福,领悟到人类社会存在的不平等的荒谬性,遇到了一个看到和接纳平凡又不平凡的爱自己的人。
We are all in the gutter, but some of us are looking at the stars. -- Oscar Wilde
经历这些心酸、不公、歧视、谩骂,需要付出多少的努力才能换来跟世界、跟自己的妥协与和解,需要吞下多少苦累才能说出那句“嘿嘿,爷爷在此”和“你在我的剧本里杀青了”。一年一度喜剧大赛中,面对老师好(刘旸教主团队)的越努力越心酸的喜剧表达时,舒淇红着眼睛跟李诞说“我没有很努力”,其实我很共情,就像麦子的那句话说的“我用十八年的努力换来跟你一起喝咖啡的资格”(我奋斗了18年才和你坐在一起喝咖啡-麦子-故事文摘),我身边大多都是“三清”(本、硕、博都是清华)的人,一个小小 C9 硕士能跟他们一起工作、竞争或多或少都需要一些不足为人道的心酸和努力,有关于资源的、也有关于天赋的。
李诞的解释是对的,努力是因为我想努力,不能有悲壮感,不能去苛求公平 —— 努力就有回报!道理是没有错的,我努力是因为我想努力,羞于努力是不可取的。可是匆匆行走的人们何时看到过脚底板下的尘埃,大家幸酸的不是努力而不得的悲壮感,而是我的每份努力都伴随一种沉重的责任感 —— 这个世界上公平是相对的,不公平是绝对的,对不公平已经存在可以视而不见的永远不可能是在努力的那一个。
付航最后有一段对自己的心酸历程的解释:
很多人看到我的东西,说我底色悲凉
真没有
为什么有一些朋友说其实我的底色悲凉,只不过因为我讲的都是我小时候经历的这些普通人都会经历到的事情,如果这样都算悲凉的话,那大家其实都悲凉,我希望传递的是 Passion
朋友们其实不是你不够好,就是因为他不喜欢你,总有人会爱你
我觉得这是付航打破社会和自己对自己的一次次审视的压抑之后练就的钝感力,他用耍宝搞怪去消解敏感者的纠结和窘迫,钝感力背后的幸酸和对世界的善意,让我看到了付航的温柔,如果对一个人温柔,那么对世界温柔那不就是慈悲吗!
如果你觉得大喊大叫、上串下跳的表演浮夸、肤浅,如果你觉得农夫、猴子、马的故事一点也不高级,如果你觉得最后的那句话老套煽情,如果没有看懂他的这些,其实挺好,这说明你一切顺遂,没有经历那份「始于微末」的不足为外人道。
2024-10-20 16:26:21
我很少谈论自己所在行业,"Security Is Our Top Priority" is BS 这篇文章与我对安全领域的理解不谋而合,作为一个计算机安全行业的从业者,我想简单分享一下自己对于计算机安全的思考:
最后附上博客原文翻译:
几年前,我被邀请去做一个关于软件安全的会议演讲。其实,我并没有真正被邀请,而是我的公司购买了一个包含演讲席位的赞助包,我回复了一封内部邮件,自愿参与了这个活动 🤣 无论如何,在准备我的演讲过程中,我意识到了几个关于安全的重要观点,这些观点自此一直萦绕在我的心头:
1. **安全是无止境的**。你总是可以投入更多努力来提高安全性。质量和安全、员工满意度等也是如此。
2. 安全的需求与**便捷的用户体验**需求**相冲突**。增强一个方面往往会损害另一个方面。
现在,有些组织宣称“安全是我们的首要任务”。真的吗?你想要将一个没有上限的事情作为你的首要任务?我的意思是,安全是一件好事,但这听起来是不是有点太简单了?实际上,像这样的空洞营销声明可能会让我有点生气。在这篇文章中,我将帮助你理解如何解读这样的声明,以及在现实生活中如何处理安全问题。我将涵盖:
* 一个哲学性的介绍
* “安全是我们的首要任务”实际上意味着什么?
* 你应该在多大程度上重视安全?
* 公司应该怎么说替代这句话?
## 安全性与用户体验平衡的哲学思考
安全的无限性以及它与用户体验(UX)之间的平衡让我想起了某件事。我不得不停下来思考了一会儿,然后我想起来了。大约在我19或20岁的时候,我发现了GK Chesterton。我非常喜欢他,因为他让我意识到实际上有非常了不起的基督教思想家,而在我那种多少算是福音派的背景中,这样的人并不多。不幸的是,尽管有Chesterton的影响,但与20岁时的我希望的相反,我个人的信仰并没有坚持下去。不过,我还是从他的著作中学到了很多至今仍感激的东西。这里有一段与我们讨论的话题相关的引用:
> “现代社会并不邪恶;在某些方面,现代社会太过美好了。它充满了狂野而被浪费的美德。当一个宗教体系被粉碎(正如基督教在宗教改革中被粉碎那样),被释放的不仅仅是恶习。恶习确实被释放了,它们四处游荡并造成破坏。但美德也被释放了;美德游荡得更加疯狂,美德造成的破坏更加可怕。现代社会充满了旧基督教美德的疯狂。美德之所以疯狂,是因为它们彼此孤立,独自游荡。因此,有些科学家只关心真理;而他们的真理是无情的。因此,有些人文主义者只关心怜悯;而他们的怜悯(很遗憾地说)往往是不真实的。”——GK Chesterton,《正统》,1908年
Chesterton的意思是,好的事物(美德)如果被过分追求或者脱离了其他好的事物,就可能变成坏事。这听起来似乎是显而易见的,确实如此。但是一旦你掌握了这个模式,就会发现它一直在发生。在这篇博文中,我们谈论的是那些忽视了UX的安全狂热者,但在我们的政治世界中也是如此。有些人将多样性、公平性和包容性(DEI)推行到极致,以至于将白人男性妖魔化。有些人将家庭价值观推崇到极致,让其他人感到格格不入。家庭价值观和包容性都是好东西,但如果它们被孤立对待,就可能走向极端,并在群体之间造成分裂。
因此,我们需要在所有美好的事物之间找到一个健康的平衡,不让任何一个走向极端而忽视其他。这并不容易!如果你是一个安全极端主义者,即使你还是被黑客攻击了,至少可以说你已经竭尽全力来提高安全性。另一方面,如果你在安全性和UX之间取得了平衡,并为了更好的UX而做出了一些降低安全性的决定,然后遭到了黑客攻击。你如何为自己辩护?说“现实是复杂的,我们在这种情况下决定优先考虑UX”并不是一个听起来很好的回答。捍卫非极端立场需要勇气,因为极端主义者总是有更有力的口号。
## “安全是我们的首要任务”实际上意味着什么?
安全真的是无限的吗?我认为是的。实际上,银行最安全的运营方式可能是关闭他们的在线业务,购买一个大保险库,并在门外部署一支小型军队。即便如此,安全检查的强度和军队的规模也可以无限增加。然而,大多数人还是更愿意通过银行应用程序的面部识别来转账,并认为这种安全程度已经足够好。
公司真的会这么说吗?是的。比如微软的“[将安全置于一切之上](https://blogs.microsoft.com/blog/2024/05/03/prioritizing-security-above-all-else/)”,AWS的“[在AWS,云安全是最高优先级](https://docs.aws.amazon.com/whitepapers/latest/aws-overview/security-and-compliance.html)”,Meta的“[保护您的数据是我们的最高优先级](https://about.meta.com/actions/protecting-privacy-and-security/)”,还有很多其他例子。
这听起来不错,但在实践中,这是否意味着只要有人提出以牺牲UX、消费者价格等为代价来提高安全性的想法,你就会实施?因为这似乎是它的意思。当然,答案是否定的,所以我对这些声明并不太认真,尽管我理解这给客户带来了一种温暖而模糊的感觉。我更希望组织能够坦诚和清晰,但我只能梦想在一个这样的世界里。
实际上,这些公司所表达的可能是:
> “我们有一个平衡的优先级框架,我们对问题进行分类并给予每个问题加权得分。我们给UX问题分配20%,安全25%,技术债务10%,新功能20%,等等。正如你所见,风险是首要优先级,因为25%高于20%。”
或者他们可能意味着:
> “安全(达到我们行业中标准水平的安全性)是我们的首要任务。一旦我们实施了足够的安全措施,我们就会专注于其他问题。”
如果你有完全诚实的安全声明的例子,比如“我们关心安全,因为我们需要你信任我们,因此它是我们的首要任务之一”,我会非常感兴趣。请在评论中分享!
## 安全性应该做到什么程度?
当我在2018年做那次演讲时,我还不清楚到底应该在何处划线。自那以后,我开始使用ISO 14971和ISO 27001这样的风险框架进行工作,现在我已经有了一些工具来帮助我处理这个问题。
一切始于一个评分机制:弄清楚我们需要保护的是什么,存在哪些风险,以及这些风险发生的可能性和严重程度如何。你将可能性和严重性相乘,为每个风险得出一个风险分数,然后将这些分数映射到低、中、高三个等级。
- 如果风险分数是低的,你可以安全地接受这个风险。
- 如果是中等,你应该考虑风险控制,除非有正当理由不这么做。
- 如果是高风险,你必须实施风险控制。
然后,一旦风险控制措施到位,验证它们是否真正减轻了风险,剩余风险是否现在变低或不存在。这样你就完成了。
听起来简单,但低、中、高的界限我们应该怎么划分呢?我总是把一些决策者聚在一起,给他们提供可能出错的情况的例子,然后请他们提出一个评分系统。接着询问他们愿意接受哪些类型的风险,同时考虑到如果应用了风险控制,产品或流程会是什么样子。
这个简单的评分系统包括风险矩阵、风险偏好/风险接受政策和风险清单。这是一个简单的工具,可以帮助你做出决策。
## 我们应该怎么说?
几十年来,全球都在以成熟的方式处理安全、质量和安全问题。让我们推广那些实际这么做的公司,而不是说这是他们的首要任务。这是一个没有意义的声明。
在我理想的世界里,公司会说:“我们维护着最先进的安全体系,因为没有客户的信任,我们作为公司就没有存在的权利。因此,这是我们工作中最重要的事情之一,我们投入了大量的精力。”这可能对大多数人来说听起来没有那么好,但这样说的公司肯定会给我留下深刻的印象。