Streamlit建立權限控管機制

前言

權限控管顧名思義則是讓有登記的人可以進來雲端網頁中操作介面,這對於一些公司內部才能操作的內容有很大的幫助。

權限控管基本功能:

  • 註冊
  • 登入
  • 忘記密碼
  • 重置密碼

發送重置密碼時會隨意產生一組token,內容會包在電子郵件中寄送,這也算是一種多因素認證的機制。

本篇文章前端介面採用Python的Streamlit套件實作,後端邏輯則採用GAS實作,資料庫則採用GoogleSpreadSheet儲存。

正常的帳號密碼實作流程不建議採用明碼儲存,這裡只是一個實作的經驗,看之後密碼的部分要加鹽還是做哈希加密都可以再追加進去。

GAS端

先新增一個SpreadSheet並且將工作表設定如下:

Users

email username password
... ... ...

ResetTokens

email token timestamp
... ... ...

timestamp在GAS中的目的是為了要取得24小時內的token才能有效更改新密碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
function doPost(e) {
var action = e.parameter.action;
var email = e.parameter.email;
var username = e.parameter.username;
var password = e.parameter.password;
var newPassword = e.parameter.newPassword;
var token=e.parameter.token;

try {
if (action == "register") {
return registerUser(email, username, password);
} else if (action == "login") {
return loginUser(username, password);
} else if (action == "forgotPassword") {
return forgotPassword(email);
} else if (action == "updatePassword") {
return updatePassword(username,token, newPassword);
} else {
return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Invalid action" })).setMimeType(ContentService.MimeType.JSON);
}
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({ status: "error", message: error.message })).setMimeType(ContentService.MimeType.JSON);
}
}

function registerUser(email, username, password) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Users');
var data = sheet.getDataRange().getValues();

for (var i = 0; i < data.length; i++) {
if (data[i][0] == email) {
return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Email already registered" })).setMimeType(ContentService.MimeType.JSON);
}
}

sheet.appendRow([email, username, password]);
return ContentService.createTextOutput(JSON.stringify({ status: "success", message: "User registered successfully" })).setMimeType(ContentService.MimeType.JSON);
}

function loginUser(username, password) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Users');
var data = sheet.getDataRange().getValues();

for (var i = 0; i < data.length; i++) {
if (data[i][1] == username && data[i][2] == password) {
return ContentService.createTextOutput(JSON.stringify({ status: "success", message: "Login successful" })).setMimeType(ContentService.MimeType.JSON);
}
}

return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Invalid username or password" })).setMimeType(ContentService.MimeType.JSON);
}

function updatePassword(email, token, newPassword) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('ResetTokens');
var data = sheet.getDataRange().getValues();
var now = new Date().getTime();
var tokenValid = false;

for (var i = 0; i < data.length; i++) {
if (data[i][0] == email && data[i][1] == token) {
var timestamp = data[i][2];
if (now - timestamp <= 24 * 60 * 60 * 1000) { // Token valid for 24 hours
tokenValid = true;
sheet.getRange(i+1, 3).setValue(0);
break;
}
}
}

if (tokenValid) {
var userSheet =SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Users');
var userData = userSheet.getDataRange().getValues();

for (var j = 0; j < userData.length; j++) {
if (userData[j][0] == email) {
userSheet.getRange(j + 1, 3).setValue(newPassword);
return ContentService.createTextOutput(JSON.stringify({ status: "success", message: "Password updated successfully" })).setMimeType(ContentService.MimeType.JSON);
}
}
}

return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Invalid token or email" })).setMimeType(ContentService.MimeType.JSON);
}
function forgotPassword(email) {
var sheet =SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Users');
var data = sheet.getDataRange().getValues();

for (var i = 0; i < data.length; i++) {
if (data[i][0] == email) {
// Generate a random token for password reset
var token = generateToken();

// Store the token and timestamp in a separate sheet (or update the existing sheet structure)
storeResetToken(email, token);

// Send the reset email
sendResetEmail(email, token);

return ContentService.createTextOutput(JSON.stringify({ status: "success", message: "Password reset email sent" })).setMimeType(ContentService.MimeType.JSON);
}
}

return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Email not found" })).setMimeType(ContentService.MimeType.JSON);
}

function generateToken() {
var charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var token = '';
for (var i = 0; i < 20; i++) {
var randomIndex = Math.floor(Math.random() * charset.length);
token += charset[randomIndex];
}
return token;
}

function storeResetToken(email, token) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('ResetTokens');
// if (!sheet) {
// sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet('ResetTokens');
// sheet.appendRow(['Email', 'Token', 'Timestamp']);
// }
var timestamp = new Date().getTime();
sheet.appendRow([email, token, timestamp]);
}

function sendResetEmail(email, resetUrl) {
var subject = 'Password Reset Request';
var body = 'You requested a password reset. Click the link below to reset your password:\n\n' + resetUrl + '\n\nIf you did not request a password reset, please ignore this email.';
GmailApp.sendEmail(email, subject, body);
}

建立完成之後發布成為WebApp,會得到一串URL,此為Python中需要引入的BASE_URL

Python端

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import streamlit as st
import requests

BASE_URL = 'YOUR_GAS_WEB_APP_URL'

def register_user(email, username, password):
response = requests.post(BASE_URL, data={
'action': 'register',
'email': email,
'username': username,
'password': password
})
return response.json()

def login_user(username, password):
response = requests.post(BASE_URL, data={
'action': 'login',
'username': username,
'password': password
})
return response.json()

def forgot_password(email):
response = requests.post(BASE_URL, data={
'action': 'forgotPassword',
'email': email
})
return response.json()

def update_password(username,token, new_password):
response = requests.post(BASE_URL, data={
'action': 'updatePassword',
'username': username,
'token':token,
'newPassword': new_password
})
return response.json()

def main():
st.title("User Management System")

menu = ["Home", "Login", "Register", "Forgot Password", "Update Password"]
choice = st.sidebar.selectbox("Menu", menu)

if choice == "Home":
st.subheader("Home")

elif choice == "Login":
st.subheader("Login")
username = st.text_input("Username")
password = st.text_input("Password", type='password')
if st.button("Login"):
result = login_user(username, password)
st.write(result)

elif choice == "Register":
st.subheader("Register")
email = st.text_input("Email")
username = st.text_input("Username")
password = st.text_input("Password", type='password')
if st.button("Register"):
result = register_user(email, username, password)
st.write(result)

elif choice == "Forgot Password":
st.subheader("Forgot Password")
email = st.text_input("Email")
if st.button("Submit"):
result = forgot_password(email)
st.write(result)

elif choice == "Update Password":
st.subheader("Update Password")
username = st.text_input("Username")
token=st.text_input("Token")
new_password = st.text_input("New Password", type='password')
if st.button("Update"):
result = update_password(username, token,new_password)
st.write(result)

if __name__ == '__main__':
main()

將這些內容用Streamlit run app.py 即可看到使用介面

image

後續要利用的時候就把登入成功的內容放置於session_state中先存起來,再顯示主要操作的介面就可以了!

哈希加密算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function hashPassword(password) {
var rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, password);
var hash = rawHash.map(function(byte) {
var v = (byte < 0 ? byte + 256 : byte).toString(16);
return v.length == 1 ? '0' + v : v;
}).join('');
return hash;
}

function registerUser(email, username, password) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Users');
var data = sheet.getDataRange().getValues();

for (var i = 0; i < data.length; i++) {
if (data[i][0] == email) {
return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Email already registered" })).setMimeType(ContentService.MimeType.JSON);
}
}

var hashedPassword = hashPassword(password);

sheet.appendRow([email, username, hashedPassword]);
return ContentService.createTextOutput(JSON.stringify({ status: "success", message: "User registered successfully" })).setMimeType(ContentService.MimeType.JSON);
}

function loginUser(username, password) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Users');
var data = sheet.getDataRange().getValues();

var hashedPassword = hashPassword(password);

for (var i = 0; i < data.length; i++) {
if (data[i][1] == username && data[i][2] == hashedPassword) {
return ContentService.createTextOutput(JSON.stringify({ status: "success", message: "Login successful" })).setMimeType(ContentService.MimeType.JSON);
}
}

return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Invalid username or password" })).setMimeType(ContentService.MimeType.JSON);
}

function updatePassword(email, token, newPassword) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('ResetTokens');
var data = sheet.getDataRange().getValues();
var now = new Date().getTime();
var tokenValid = false;

for (var i = 0; i < data.length; i++) {
if (data[i][0] == email && data[i][1] == token) {
var timestamp = data[i][2];
if (now - timestamp <= 24 * 60 * 60 * 1000) { // Token valid for 24 hours
tokenValid = true;
sheet.getRange(i + 1, 3).setValue(0);
break;
}
}
}

if (tokenValid) {
var userSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Users');
var userData = userSheet.getDataRange().getValues();

var hashedPassword = hashPassword(newPassword);

for (var j = 0; j < userData.length; j++) {
if (userData[j][0] == email) {
userSheet.getRange(j + 1, 3).setValue(hashedPassword);
return ContentService.createTextOutput(JSON.stringify({ status: "success", message: "Password updated successfully" })).setMimeType(ContentService.MimeType.JSON);
}
}
}

return ContentService.createTextOutput(JSON.stringify({ status: "error", message: "Invalid token or email" })).setMimeType(ContentService.MimeType.JSON);
}