应用程序安全保障全攻略
1. 配置负载均衡器相关设置
在配置应用程序的过程中,负载均衡器(ELB)的设置至关重要。我们需要进行一些关键的操作来确保应用的正常运行和安全性。
-
启用 HTTPS 监听器
:点击保存后,会返回上一视图,同时能看到端口已打开的确认信息。
-
启用 Cookie 粘性
:由于应用会使用基于会话的 Cookie,而应用在多个实例上运行时,无法保证用户登录后会两次被定向到同一实例,这会导致会话无法正常使用。ELB 的 Stickiness 功能可以将用户会话绑定到特定实例并转发相关 Cookie。具体操作如下:
1. 返回负载均衡器的描述选项卡,找到端口配置设置。
2. 点击端口 443 配置旁边的“编辑”。
3. 在弹出的模态视图中,选择“启用应用程序生成的 Cookie 粘性”。
4. 在“Cookie 名称”字段中输入“connect.sid”,然后点击保存。
以下是操作流程的 mermaid 流程图:
graph LR
A[返回负载均衡器描述选项卡] --> B[找到端口配置设置]
B --> C[点击端口 443 配置编辑]
C --> D[选择启用应用程序生成的 Cookie 粘性]
D --> E[输入 Cookie 名称 connect.sid]
E --> F[点击保存]
2. 修改安全组
为了进一步保障应用的安全性,我们需要修改安全组。虽然 HTTPS 连接在客户端与 CloudFront 以及 CloudFront 与负载均衡器之间进行协商,但由于实例不在 VPC 中,仍有可能直接连接到实例。为了防止恶意用户直接连接到实例,我们可以创建自定义安全组。具体步骤如下:
1. 在 EC2 中,选择左侧导航栏中的“安全组”链接,会看到 OpsWorks 自动生成的安全组列表。
2. 在安全组列表中,找到“AWS - OpsWorks - nodejsApp”。
3. 打开上方的“操作”菜单,点击“复制到新组”。
4. 将新安全组命名为“Photoalbums - nodejs - App”,并给出有用的描述,如“用于 Photoalbums 应用的安全组,限制 HTTP 和 HTTPS 连接”,无需选择 VPC。
5. 更改入站 HTTP 和 HTTPS 连接的安全规则:
- 找到 HTTP 行,将“源”更改为“自定义 IP”,在 IP 字段中输入“amazon - elb/amazon - elb - sg”。
- 对 HTTPS 进行相同的更改。
6. 完成后,点击“创建”。
| 步骤 | 操作 |
|---|---|
| 1 | 在 EC2 选择安全组链接 |
| 2 | 找到 AWS - OpsWorks - nodejsApp |
| 3 | 复制到新组 |
| 4 | 命名并描述新安全组 |
| 5 | 更改 HTTP 和 HTTPS 源 |
| 6 | 点击创建 |
3. 使应用程序栈使用新安全组
创建好自定义安全组后,需要让应用程序栈使用它。具体操作如下:
1. 导航到 OpsWorks 并选择应用程序栈。
2. 由于不能让应用层没有安全组,所以先添加自定义安全组,再移除默认安全组。
3. 从导航菜单中选择“层”,点击应用层旁边的“安全”。
4. 点击右上角的“编辑”按钮。
5. 从下拉列表中选择自定义安全组,然后点击保存。
6. 从导航菜单中进入“栈”,转到“栈设置”并点击“编辑”。
7. 在底部会看到 OpsWorks 安全组的切换开关,将其设置为“否”,然后点击保存。此时,实例将不再使用自动生成的安全组。
以下是使用新安全组的操作流程 mermaid 流程图:
graph LR
A[导航到 OpsWorks 选择应用程序栈] --> B[添加自定义安全组并移除默认安全组]
B --> C[选择层并点击应用层安全]
C --> D[点击编辑按钮]
D --> E[选择自定义安全组并保存]
E --> F[进入栈设置并编辑]
F --> G[设置 OpsWorks 安全组为否并保存]
4. 应用程序安全设置
为了保障应用程序的安全,我们需要实现两个安全模式:存储加密密码和使用安全会话。
-
添加会话和加密模块
:需要向应用程序添加“easycrypto”和“express - session”两个模块。在“package.json”的“dependencies”对象中添加以下内容:
{
...
"debug": "~1.0.4",
"easycrypto": "0.1.1",
"express-session": "^1.7.6",
"jade": "~1.5.0"
}
然后在命令行中,导航到工作目录并输入“npm install”,依赖项将自动安装。
-
添加密码加密
:在“/lib/model/model - user.js”文件中进行密码加密/解密的操作。
- 在文件顶部添加一个名为“encryptionKey”的变量,并设置为任意随机字符串,例如:
var encryptionKey = '80smoviereferencegoeshere';
- 在文件底部添加两个私有方法用于生成哈希密码和解密哈希密码:
function generatePasswordHash(password){
var easycrypto = require('easycrypto').getInstance();
var encrypted = easycrypto.encrypt(password, encryptionKey);
return encrypted;
}
function decryptPasswordHash(passwordHash) {
var easycrypto = require('easycrypto').getInstance();
var decryptedPass = easycrypto.decrypt(passwordHash, encryptionKey);
return decryptedPass;
}
- 替换“createUser”函数:
function createUser(params, callback){
var newUser = {
username: params.username,
password: generatePasswordHash(params.password),
email: params.email
}
var query = 'INSERT INTO users SET ? ';
connection.query(query, newUser, function(err, rows, fields) {
if (err) {
if(err.errno == 1062){
var error = new Error("This username has already been taken.");
callback(error);
} else {
callback(err);
}
} else {
callback(null, {message:'Registration successful!'});
}
});
}
- 替换“loginUser”函数:
function loginUser(params, callback){
connection.query('SELECT username, password, userID FROM users WHERE username=' +
connection.escape(params.username), function(err, rows, fields) {
if(err){
callback(err);
} else if(rows.length > 0){
var decryptedPass = decryptPasswordHash(rows[0].password);
if(decryptedPass == params.password){
var response = {
username: rows[0].username,
userID: rows[0].userID
}
callback(null, response);
} else {
var error = new Error("Invalid login");
callback(error);
}
} else {
var error = new Error("Invalid login");
callback(error);
}
});
}
通过以上步骤,我们可以逐步提升应用程序的安全性,确保用户数据的安全和应用的稳定运行。后续还需要进行安全会话的配置等操作,以进一步完善应用的安全体系。
5. 使用安全会话
使用安全会话的操作会稍微复杂一些,具体取决于应用程序的目标。由于应用的所有路由都包含受限和不受限的 API 端点,因此需要在各个路由级别手动保护应用程序。
-
配置 express - session 中间件
:在“server.js”中,在引入“express”之后立即添加“express - session”中间件,并在其他“app.use”语句之前添加配置:
var express = require('express');
var expressSession = require('express-session');
var path = require('path');
app.use(expressSession({secret: 'ssshhhhh'}));
建议将“secret”的值替换为自己的密钥。
-
设置用户会话标识
:在用户登录时,需要在会话 Cookie 中设置标识用户的值。打开“/routes/users.js”,找到“/login”路由,在“model.loginUser()”的回调中设置会话的“userID”:
router.post('/login', function(req, res) {
if(req.param('username') && req.param('password') ){
var params = {
username: req.param('username').toLowerCase(),
password: req.param('password')
};
model.loginUser(params, function(err, obj){
if(err){
res.status(400).send({error: 'Invalid login'});
} else {
req.session.userID = obj.userID;
res.send(obj);
}
});
} else {
res.status(400).send({error: 'Invalid login'});
}
});
- 简化注销路由 :“/logout”路由只需销毁当前会话:
router.post('/logout', function(req, res) {
if(req.session){
req.session.destroy();
}
res.send({message: 'User logged out successfully'});
});
-
保护关键路由
:需要让关键路由要求会话 Cookie 而不是 POST 参数。以“/routes/photos.js”为例,对“/upload”和“/delete”路由进行修改:
- /photos/upload :
router.post('/upload', function(req, res) {
if(req.session && req.session.userID){
if(req.param('albumID') && req.files.photo){
var params = {
userID : req.session.userID,
albumID : req.param('albumID')
}
if(req.param('caption')){
params.caption = req.param('caption');
}
fs.exists(req.files.photo.path, function(exists) {
if(exists) {
params.filePath = req.files.photo.path;
var timestamp = Date.now();
params.newFilename = params.userID + '/' + params.filePath.replace('tmp/', timestamp);
uploadPhoto(params, function(err, fileObject){
if(err){
res.status(400).send({error: 'Invalid photo data'});
}
params.url = fileObject.url;
delete params.filePath;
delete params.newFilename;
model.createPhoto(params, function(err, obj){
if(err){
res.status(400).send({error: 'Invalid photo data'});
} else {
res.send(obj);
}
});
});
} else {
res.status(400).send({error: 'Invalid photo data'});
}
});
} else {
res.status(400).send({error: 'Invalid photo data'});
}
} else {
res.status(401).send({error: 'You must be logged in to upload photos'});
}
});
- **/photos/delete**:
router.post('/delete', function(req, res) {
if(req.session && req.session.userID){
if(req.param('id')){
var params = {
photoID : req.param('id') ,
userID : req.session.userID
}
model.deletePhoto(params, function(err, obj){
if(err){
res.status(400).send({error: 'Photo not found'});
} else {
res.send(obj);
}
});
} else {
res.status(400).send({error: 'Invalid photo ID'});
}
} else {
res.status(401).send({error: 'Unauthorized to create album'});
}
});
同样,在“/routes/albums.js”中,对“/upload”和“/delete”路由进行类似修改:
router.post('/upload', function(req, res) {
if(req.session && req.session.userID){
if(req.param('title')){
var params = {
userID : req.session.userID,
title : req.param('title')
}
model.createAlbum(params, function(err, obj){
if(err){
res.status(400).send({error: 'Invalid album data'});
} else {
res.send(obj);
}
});
} else {
res.status(400).send({error: 'Invalid album data'});
}
} else {
res.status(401).send({error: 'Unauthorized to create album'});
}
});
router.post('/delete', function(req, res) {
if(req.session && req.session.userID){
if(req.param('albumID')){
var params = {
albumID : req.param('albumID') ,
userID : req.session.userID
}
model.deleteAlbum(params, function(err, obj){
if(err){
res.status(400).send({error: 'Album not found'});
} else {
res.send(obj);
}
});
} else {
res.status(400).send({error: 'Invalid album ID'});
}
} else {
res.status(401).send({error: 'Unauthorized to create album'});
}
});
- 修改模型 :需要确保用户只能删除自己的内容。在“/lib/models/model - photos.js”中,替换“deletePhotoByID()”方法:
function deletePhotoByID(params, callback){
var query = 'UPDATE photos SET published=0 WHERE photoID=' + connection.escape
(params.photoID) + ' AND userID=' + params.userID;
connection.query(query, function(err, rows, fields){
if(rows.length > 0){
callback(null, rows);
} else {
if(rows.changedRows > 0){
callback(null, {message: 'Photo deleted successfully'});
} else {
var deleteError = new Error('Unable to delete photo');
callback(deleteError);
}
}
});
}
在“/lib/models/model - albums.js”中,替换“deleteAlbum()”方法:
function deleteAlbum(params, callback){
var query = 'UPDATE albums SET published=0 WHERE albumID=' + connection.escape
(params.albumID) + ' AND userID=' + params.userID;
connection.query(query, function(err, rows, fields){
if(err){
callback(err);
} else {
if(rows.changedRows > 0){
callback(null, {message: 'Album deleted successfully'});
} else {
var deleteError = new Error('Unable to delete album');
callback(deleteError);
}
}
});
}
6. 总结与测试
完成以上所有代码更改后,将更改提交到代码仓库,并部署到 OpsWorks 实例,等待部署过程完成。部署完成后即可开始测试。由于之前的用户数据使用的是明文密码,现在应用会尝试解密,会导致密码不匹配,所以测试的最简单方法是注册一个新用户,登录并创建一个相册。可以通过向域名或负载均衡器本身发送 HTTPS POST 请求来成功完成这些操作,而通过 HTTP 尝试登录域名将被拒绝。
通过一系列的配置和代码修改,我们从负载均衡器的设置、安全组的修改,到应用程序内部的密码加密和安全会话的使用,全面地提升了应用程序的安全性。整个过程涵盖了多个关键步骤,以下是整体的操作步骤总结表格:
| 操作类别 | 具体操作 |
| ---- | ---- |
| 负载均衡器设置 | 启用 HTTPS 监听器、启用 Cookie 粘性 |
| 安全组配置 | 创建自定义安全组、修改安全规则、应用新安全组 |
| 应用程序安全 | 添加会话和加密模块、密码加密、使用安全会话 |
以下是整体操作流程的 mermaid 流程图:
graph LR
A[负载均衡器设置] --> B[安全组配置]
B --> C[应用程序安全设置]
C --> D[提交代码更改]
D --> E[部署到 OpsWorks 实例]
E --> F[测试应用程序]
通过遵循这些步骤,开发者可以构建一个更加安全、可靠的应用程序,确保用户数据的安全和应用的稳定运行。同时,随着技术的不断发展,需要持续关注新的安全问题和解决方案,以保持应用程序的安全性。
超级会员免费看
960

被折叠的 条评论
为什么被折叠?



