電子公文傳輸系統-進展二

“我美式”组發表於2024-05-26

上週任務完成情況

  • 改進密碼儲存方案,解決使用者密碼儲存未加密問題
  • 重新商討金鑰儲存方案,解決使用者公私鑰對存在未加密問題
  • 修改css檔案,美化前端頁面
  • 完善後端資料庫

功能實現修改

SM3雜湊演算法和加鹽技術來儲存密碼

sm3_hash_with_salt函式
def sm3_hash_with_salt(password, salt):
    # 使用SM3演算法生成雜湊值
    hash_hex = sm3_hash(func.bytes_to_list(bytes(password, encoding='utf-8')))
    # 使用提供的鹽替換提取部分
    new_hash_hex = hash_hex[:4] + salt + hash_hex[19:]
    return new_hash_hex
  • 變數:
    • password: 使用者輸入的密碼。
    • salt: 用於雜湊計算的鹽值。
    • hash_hex: 透過SM3演算法生成的密碼雜湊值(十六進位制)。
    • new_hash_hex: 帶鹽的雜湊值,前4個字元和第19個字元後的部分保持不變,中間部分替換為鹽值。
  • 功能: 生成帶鹽的SM3雜湊值。增加密碼儲存的安全性。
  • 步驟:
  1. 使用SM3演算法將密碼轉換為雜湊值。
  2. 將生成的雜湊值前4個字元和第19個字元後面的部分保持不變,將中間的部分替換為鹽值。
  3. 返回帶鹽的雜湊值。
generate_random_string函式
def generate_random_string(length=15):
    # 生成一個隨機字串
    characters = string.ascii_letters + string.digits + string.punctuation
    random_string = ''.join(random.choice(characters) for i in range(length))
    return random_string
  • 變數:
    • length: 生成的隨機字串的長度(預設15)。
    • characters: 可供選擇的字符集,包括字母、數字和標點符號。
    • random_string: 生成的隨機字串。
  • 功能: 生成一個指定長度的隨機字串。用作鹽值或其他需要隨機字串的地方。
  • 步驟:
  1. 定義可用字符集,包括字母、數字和標點符號。
  2. 隨機選擇指定長度的字元組成字串。
  3. 返回生成的隨機字串。
login函式
def login(request):
    if request.method == 'POST':
        id_in = request.POST['id_in']
        password_in = request.POST['password_in']

        # 查詢資料庫,檢查使用者是否存在
        try:
            user_profile = UserProfile.objects.get(id=id_in)
        except UserProfile.DoesNotExist:
            messages.error(request, '學號或密碼錯誤,請重新輸入。')
            return redirect('login')

        # 獲取儲存的鹽值
        salt = user_profile.salt

        # 計算帶鹽的SM3雜湊值
        salted_hash_password_in = sm3_hash_with_salt(password_in, salt)
        print(salted_hash_password_in)

        # 比較雜湊值
        if salted_hash_password_in == user_profile.password_up:
            # 登入成功,將使用者資訊儲存到session中
            request.session['user_id'] = user_profile.id
            request.session['username'] = user_profile.username_up

            LogData = Log.objects.create(
                username = user_profile.username_up,
                documentname = "無",
                operation = f'使用者{user_profile.username_up}於{timezone.now()}登入了系統。'
            )
            LogData.save()

            # 可以在這裡新增其他處理,例如重定向到成功頁面或顯示成功訊息
            # 登入成功,新增訊息
            time.sleep(3)
            return redirect('index')
        else:
            messages.error(request, '學號或密碼錯誤,請重新輸入。')
            return redirect('login')

    return render(request, 'login.html')  # 替換為你的模板路徑
  • 變數:
    • request: 包含請求資訊的物件。
    • id_in: 使用者輸入的學號。
    • password_in: 使用者輸入的密碼。
    • user_profile: 從資料庫中獲取的使用者資訊。
    • salt: 儲存在資料庫中的鹽值。
    • salted_hash_password_in: 使用者輸入密碼的帶鹽雜湊值。
    • LogData: 用於記錄日誌的物件。
  • 功能: 處理使用者登入請求。驗證使用者身份,並在成功登入後記錄日誌。
  • 步驟:
  1. 從POST請求中獲取使用者輸入的學號和密碼。
  2. 查詢資料庫以檢查使用者是否存在。如果不存在,返回錯誤訊息。
  3. 獲取儲存的鹽值。
  4. 使用sm3_hash_with_salt函式計算使用者輸入密碼的帶鹽雜湊值。
  5. 將計算出的雜湊值與資料庫中儲存的雜湊值進行比較。如果匹配,則登入成功,記錄登入日誌,並將使用者資訊儲存到session中。
  6. 如果雜湊值不匹配,返回錯誤訊息。
register函式
def register(request):
    if request.method == 'POST':
        id = request.POST['id']
        username_up = request.POST['username_up']
        email = request.POST['email']
        password_up = request.POST['password_up']

        # 檢查id的唯一性
        if UserProfile.objects.filter(id=id).exists():
            messages.error(request, '學號已存在,請使用不同的學號。')
            return redirect('register')

        # 生成隨機鹽值
        salt = generate_random_string(15)

        # 計算帶鹽的SM3雜湊值
        salted_hash_password = sm3_hash_with_salt(password_up, salt)
        print(salted_hash_password)

        priKey = PrivateKey()
        pubKey = priKey.publicKey()

        new_user = UserProfile.objects.create(
            id=id,
            username_up=username_up,
            email=email,
            password_up=salted_hash_password,  # 儲存帶鹽的SM3雜湊值
            salt=salt,  # 儲存鹽值
            public_key=pubKey.toString(compressed=False),  # 儲存公鑰
            private_key=priKey.toString(), # 儲存私鑰
            avatar='avatars/default_avatar.png'
        )
        new_user.save()

        LogData = Log.objects.create(
            username = new_user.username_up,
            documentname = "無",
            operation = f'使用者{new_user.username_up}於{timezone.now()}註冊了賬號。'
        )
        LogData.save()

        # 新增成功訊息
        messages.success(request, '註冊成功,請登入。')
        time.sleep(3)
        return redirect('login')

    return render(request, 'register.html')  # 替換為你的模板路徑
  • 變數:
    • request: 包含請求資訊的物件。
    • id: 使用者輸入的學號。
    • username_up: 使用者輸入的使用者名稱。
    • email: 使用者輸入的郵箱。
    • password_up: 使用者輸入的密碼。
    • salt: 生成的隨機鹽值。
    • salted_hash_password: 使用者輸入密碼的帶鹽雜湊值。
    • priKey: 生成的私鑰物件。
    • pubKey: 生成的公鑰物件。
    • new_user: 新建立的使用者物件。
    • LogData: 用於記錄日誌的物件。
  • 功能: 處理使用者註冊請求。生成帶鹽的雜湊密碼和公私鑰,建立新使用者記錄,並記錄註冊日誌。
  • 步驟:
  1. 從POST請求中獲取使用者輸入的學號、使用者名稱、郵箱和密碼。
  2. 檢查學號的唯一性。如果學號已存在,返回錯誤訊息。
  3. 生成隨機鹽值。
  4. 使用sm3_hash_with_salt函式計算使用者輸入密碼的帶鹽雜湊值。
  5. 生成使用者的公鑰和私鑰。
  6. 建立新的使用者記錄,儲存學號、使用者名稱、郵箱、帶鹽的雜湊密碼、鹽值、公鑰和私鑰。
  7. 記錄註冊日誌。
  8. 返回成功訊息,並重定向到登入頁面。
create_user 函式
from django.shortcuts import render, redirect
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.contrib import messages
from .models import UserProfile, Log
from .forms import UserForm
from .utils import sm3_hash_with_salt, generate_random_string
import time
from some_crypto_library import PrivateKey  # 假設使用某個庫生成金鑰對

def create_user(request):
    current_user = request.session['username']
    if request.method == 'POST':
        form = UserForm(request.POST)
        if form.is_valid():
            # 獲取表單資料並儲存到資料庫
            id = form.cleaned_data['id']
            username = form.cleaned_data['username_up']
            email = form.cleaned_data['email']
            password = form.cleaned_data['password_up']

            # 生成隨機鹽值
            salt = generate_random_string(15)
            # 計算帶鹽的SM3雜湊值
            salted_hash_password = sm3_hash_with_salt(password, salt)

            priKey = PrivateKey()
            pubKey = priKey.publicKey()

            # 儲存到資料庫中
            UserProfile.objects.create(
                id=id,
                username_up=username,
                email=email,
                password_up=salted_hash_password,  # 儲存帶鹽的SM3雜湊值
                salt=salt,  # 儲存鹽值
                public_key=pubKey.toString(compressed=False),  # 儲存公鑰
                private_key=priKey.toString(),  # 儲存私鑰
                avatar='avatars/default_avatar.png'
            )

            LogData = Log.objects.create(
                username=current_user,
                documentname="無",
                operation=f'使用者{current_user}於{timezone.now()}建立了新使用者{username}。'
            )
            LogData.save()

            # 重定向到 index 頁面,使用 HttpResponseRedirect 物件
            return HttpResponseRedirect(reverse('index'))

    else:
        form = UserForm()

    return render(request, 'adduser.html', {'form': form})

在上述程式碼中,主要的改動如下:

  • 新增了對 generate_random_string 和 sm3_hash_with_salt 的呼叫,以生成隨機鹽值並計算帶鹽的SM3雜湊值。
  • 將計算後的帶鹽雜湊值儲存在資料庫中。
  • 將鹽值一起儲存在資料庫中,以便在使用者登入時進行雜湊驗證。
change_userinfo 函式
from django.shortcuts import render, redirect
from django.contrib import messages
from .models import UserProfile, Log
from .utils import sm3_hash_with_salt, generate_random_string
import time

def change_userinfo(request, user_id):
    current_user = request.session['username']
    try:
        user_profile = UserProfile.objects.get(id=user_id)

        # 如果是 POST 請求,即提交表單
        if request.method == 'POST':
            # 獲取表單中的資料
            username = request.POST.get('username_up')
            email = request.POST.get('email')
            password = request.POST.get('password_up')

            # 如果密碼不為空,則更新密碼
            if password:
                # 生成新的隨機鹽值
                salt = generate_random_string(15)
                # 計算帶鹽的SM3雜湊值
                salted_hash_password = sm3_hash_with_salt(password, salt)
                # 更新使用者密碼和鹽值
                user_profile.password_up = salted_hash_password
                user_profile.salt = salt

            # 更新使用者名稱和郵箱
            user_profile.username_up = username
            user_profile.email = email

            LogData = Log.objects.create(
                username=current_user,
                documentname="無",
                operation=f'使用者{current_user}於{timezone.now()}修改了{username}的使用者資訊。'
            )
            LogData.save()

            # 儲存更新後的資訊到資料庫
            user_profile.save()

            messages.success(request, '使用者資訊更新成功。')
            return redirect('index')  # 更新成功後重定向到首頁
        else:
            # 如果是 GET 請求,即使用者訪問修改頁面
            return render(request, 'change_userinfo.html', {'user_profile': user_profile})

    except UserProfile.DoesNotExist:
        messages.error(request, '使用者不存在。')
        return redirect('index')

在上述程式碼中,主要的改動如下:

  • 在更新密碼時,首先生成新的隨機鹽值,並計算帶鹽的SM3雜湊值。
  • 將新的帶鹽雜湊值和鹽值更新到使用者資訊中。
  • 僅在密碼不為空時才更新密碼和鹽值,這樣可以避免不必要的密碼更新。

本週計劃

  • 實現認證密碼“HASH+鹽”,防止彩虹表攻擊
  • 按計劃修改和實現金鑰管理方案
  • 最佳化程式碼效能
  • 繼續修改css檔案,美化前端頁面
  • 總體除錯程式碼,確保要求功能均能成功實現

成品展示

使用者管理模組

  • 使用者註冊、登入、許可權管理等。

資料庫中的使用者模型

class UserProfile(models.Model):
    id = models.CharField(max_length=8, primary_key=True)
    username_up = models.CharField(max_length=16)
    email = models.EmailField()
    password_up = models.CharField(max_length=16)
    access_level = models.IntegerField(default=0)  # Access level: 0 - Regular user, 1 - Admin
    public_key = models.CharField(max_length=128, blank=True, null=True)
    private_key = models.CharField(max_length=128, blank=True, null=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)  # 新增使用者頭像欄位

    def __str__(self):
        return self.username_up

id:使用者的唯一識別符號,字元長度為8,是主鍵。
username_up:使用者的使用者名稱,字元長度為16。
email:使用者的電子郵件地址。
password_up:使用者的密碼,字元長度為16。
access_level:使用者的訪問級別,整數型別,預設值為0(0 - 普通使用者,1 - 管理員)。
public_key:使用者的公鑰,字元長度為128,可以為空。
private_key:使用者的私鑰,字元長度為128,可以為空。
avatar:使用者的頭像圖片,上傳路徑為avatars/,可以為空。
str:定義使用者物件的字串表示形式,這裡返回使用者名稱。

使用者管理介面

register.html
{% load static %}
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <link href="{% static 'images/icon.ico' %}" rel="icon" type="icon">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>註冊賬戶</title>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
    <section>
        <div class="login">
            <h2>註冊介面</h2>
            <div class="innner">
                 <!-- 註冊 -->
                <div class="container__form container--signup">
                   <form action="{% url 'register' %}" class="form" id="form1" onsubmit="return validateForm_up()" name="signup" method="post">
                        {% csrf_token %}
                        <input type="text" placeholder="學號(8位)" name="id" class="input" />
                        <input type="text" placeholder="使用者名稱(6-16位字母、數字或下劃線)" name="username_up" class="input" />
                        <input type="email" placeholder="郵箱" name="email" class="input" />
                        <input type="password" placeholder="密碼(6-16位字母、數字或下劃線)" name="password_up" class="input" />
                        <input type="submit" value="註冊" id="btn">
                        {% if messages %}
                        <ul class="messages">
                            {% for message in messages %}
                                <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
                            {% endfor %}
                        </ul>
                        {% endif %}
                    </form>
                </div>
                <div class="group">
                    <a>已經有賬戶了?點選右邊登入</a>
                    <a href="{% url 'login' %}">登入</a>
                </div>
            </div>
        </div>
    </section>

    <script src="{% static 'js/verify_up.js' %}"></script>
</body>
</html>

使用者可以輸入他們的學號、使用者名稱、郵箱和密碼進行註冊。如果已經有賬戶,可以點選連結跳轉到登入頁面。表單提交前會進行客戶端驗證,並且包含CSRF保護以確保安全。

login.html
{% load static %}
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <link href="{% static 'images/icon.ico' %}" rel="icon" type="icon">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>公文傳輸系統登入介面</title>
    <link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
    <section>
        <div class="login">
            <h2>登入介面</h2>
            <div class="innner">
                <!-- 登入 -->
                <div class="container__form container--sign">
                    <form action="{% url 'login' %}" class="form" id="form1" onsubmit="return validateForm_in()" name="signin" method="post">
                    {% csrf_token %}
                    <input type="text" placeholder="學號(8位)或使用者名稱" name="id_in" class="input" />
                    <input type="password" placeholder="密碼(6-16位字母、數字或下劃線)" name="password_in" class="input" />
                    <input type="submit" value="登入" id="btn">
                    {% if messages %}
                    <ul class="messages">
                        {% for message in messages %}
                            <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
                        {% endfor %}
                    </ul>
                    {% endif %}
                    </form>
                </div>
                <div class="group">
                    <a href="#">忘記密碼?</a>
                    <a href="{% url 'register' %}">還沒有賬號?趕緊註冊</a>
                </div>
            </div>
        </div>

    </section>

    <script src="{% static 'js/verify_in.js' %}"></script>
</body>
</html>

使用者可以輸入他們的學號或使用者名稱及密碼進行登入。如果忘記密碼,可以點選相應連結進行找回。如果還沒有賬號,可以點選連結跳轉到註冊頁面。表單提交前會進行客戶端驗證,並且包含CSRF保護以確保安全。

adduser.html
<!-- adduser.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>新增使用者</title>
    <link rel="stylesheet" href="{% static 'css/adduserpage.css' %}">
    <!-- 此處包含您的樣式表或其他資源 -->
</head>
<body>
    <h1>新增使用者</h1>
    <form method="post" action="{% url 'create_user' %}">
        {% csrf_token %}
        {{ form.as_p }}
        <button type="submit">確認</button>
        <a href="{% url 'index' %}">取消</a>
    </form>
    <script src="{% static 'js/verify_up.js' %}"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            document.querySelector('form').addEventListener('submit', function(event) {
                event.preventDefault(); // 阻止表單預設提交行為

                // 提交表單後,等待 3 秒後跳轉到 index 頁面
                setTimeout(function() {
                    document.querySelector('form').submit(); // 提交表單
                }, 3000); // 等待 3 秒
            });
        });
    </script>
</body>
</html>

提供了一個用於新增使用者的表單,幷包含表單提交前的CSRF保護和JavaScript指令碼,以便在表單提交後等待3秒再跳轉到主頁。

change_userinfo.html
{% load static %}
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>修改使用者資訊</title>
    <link rel="stylesheet" href="{% static 'css/change_userinfo.css' %}">
    <!-- 此處包含您的樣式表或其他資源 -->
</head>
<body>
    <h1>修改使用者資訊</h1>
    <form method="post" action="{% url 'change_userinfo' user_profile.id %}">
        {% csrf_token %}
        <label for="id">學號:</label>
        <input type="text" id="id" name="id" value="{{ user_profile.id }}" disabled><br><br>

        <label for="username">使用者名稱:</label>
        <input type="text" id="username" name="username_up" value="{{ user_profile.username_up }}"><br><br>

        <label for="email">郵箱:</label>
        <input type="email" id="email" name="email" value="{{ user_profile.email }}"><br><br>

        <label for="password">密碼:</label>
        <input type="password" id="password" name="password_up" value="{{ user_profile.password_up }}"><br><br>

        <!-- 如果還有其他欄位需要修改,繼續新增 -->

        <button type="submit">提交</button>
        <a href="{% url 'index' %}">取消</a>
    </form>
    <script src="{% static 'js/verify_up.js' %}"></script>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            document.querySelector('form').addEventListener('submit', function(event) {
                event.preventDefault(); // 阻止表單預設提交行為

                // 提交表單後,等待 3 秒後跳轉到 index 頁面
                setTimeout(function() {
                    document.querySelector('form').submit(); // 提交表單
                }, 3000); // 等待 3 秒
            });
        });
    </script>
</body>
</html>

提供了一個用於修改使用者資訊的表單,幷包含表單提交前的CSRF保護和JavaScript指令碼,以便在表單提交後等待3秒再跳轉到主頁。

manage_permission.html
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>使用者許可權管理</title>
    <link href="../static/images/icon.ico" rel="icon" type="icon">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="{% static 'css/manage.css' %}">
</head>
<body>
    <h1>使用者許可權管理</h1>
    <p>使用者 ID: {{ user.id }}</p>
    <p>使用者名稱: {{ user.username_up }}</p>
    <p>郵箱: {{ user.email }}</p>
    <p>密碼: {{ user.password_up }}</p>

    <form method="post" action="{% url 'manage_permission' user.id %}">
        {% csrf_token %}
        <label for="accessLevel">訪問許可權:</label>
        <select id="accessLevel" name="access_level">
            <option value="0" {% if user.access_level == 0 %} selected {% endif %}>0</option>
            <option value="1" {% if user.access_level == 1 %} selected {% endif %}>1</option>
            <option value="2" {% if user.access_level == 2 %} selected {% endif %}>2</option>
            <option value="3" {% if user.access_level == 3 %} selected {% endif %}>3</option>
        </select>

        <button type="submit">儲存許可權</button>
        <a href="{% url 'index' %}">取消</a>

        <!-- 提示使用者許可權含義 -->
        <p>
            0: 普通使用者<br>
            1: 管理員<br>
            2: 不能傳送公文的普通使用者<br>
            3: 黑名單使用者,無法進行任何操作
        </p>
    </form>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            document.querySelector('form').addEventListener('submit', function(event) {
                event.preventDefault(); // 阻止表單預設提交行為

                // 提交表單後,等待 3 秒後跳轉到 index 頁面
                setTimeout(function() {
                    document.querySelector('form').submit(); // 提交表單
                }, 3000); // 等待 3 秒
            });
        });
    </script>
</body>
</html>

提供了一個用於管理使用者許可權的表單,管理員可以根據需要修改使用者的訪問許可權,幷包含表單提交前的CSRF保護和JavaScript指令碼,以便在表單提交後等待3秒再跳轉到主頁。

upload_avatar.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>上傳使用者頭像</title>
</head>
<body>
    <form id="avatarUploadForm" action="/upload_avatar/" method="post" enctype="multipart/form-data">
        {% csrf_token %}
        <input type="file" name="avatarFile" accept="image/*">
        <input type="submit" value="上傳頭像">
    </form>
</body>
</html>

用於使用者上傳頭像圖片,表單透過POST方法提交到 /upload_avatar/ URL,並使用 multipart/form-data 編碼型別,以便上傳檔案。

使用者管理邏輯

login邏輯
def login(request):
    if request.method == 'POST':
        id_in = request.POST['id_in']
        password_in = request.POST['password_in']

        # 查詢資料庫,檢查使用者是否存在
        try:
            user_profile = UserProfile.objects.get(id=id_in)
        except UserProfile.DoesNotExist:
            messages.error(request, '學號或密碼錯誤,請重新輸入。')
            return redirect('login')

        # 獲取儲存的鹽值
        salt = user_profile.salt

        # 計算帶鹽的SM3雜湊值
        salted_hash_password_in = sm3_hash_with_salt(password_in, salt)
        print(salted_hash_password_in)

        # 比較雜湊值
        if salted_hash_password_in == user_profile.password_up:
            # 登入成功,將使用者資訊儲存到session中
            request.session['user_id'] = user_profile.id
            request.session['username'] = user_profile.username_up

            LogData = Log.objects.create(
                username = user_profile.username_up,
                documentname = "無",
                operation = f'使用者{user_profile.username_up}於{timezone.now()}登入了系統。'
            )
            LogData.save()

            # 可以在這裡新增其他處理,例如重定向到成功頁面或顯示成功訊息
            # 登入成功,新增訊息
            time.sleep(3)
            return redirect('index')
        else:
            messages.error(request, '學號或密碼錯誤,請重新輸入。')
            return redirect('login')

    return render(request, 'login.html')  # 替換為你的模板路徑

當請求方法為 POST 時,表示使用者提交了登入表單。
獲取使用者輸入的學號和密碼。
透過學號查詢資料庫,檢查使用者是否存在。如果使用者不存在,則返回錯誤訊息並重定向到登入頁面。
如果使用者存在,則獲取資料庫中儲存的鹽值。
使用 SM3 雜湊演算法計算帶鹽的密碼雜湊值。
將計算得到的雜湊值與資料庫中儲存的密碼雜湊值進行比較,如果相同則表示密碼正確,登入成功。
登入成功後,將使用者資訊儲存到 session 中,並記錄使用者的登入操作到日誌中。
最後,重定向到主頁,並在這裡可以新增其他處理,比如重定向到成功頁面或顯示成功訊息。

register邏輯
def register(request):
    if request.method == 'POST':
        id = request.POST['id']
        username_up = request.POST['username_up']
        email = request.POST['email']
        password_up = request.POST['password_up']

        # 檢查id的唯一性
        if UserProfile.objects.filter(id=id).exists():
            messages.error(request, '學號已存在,請使用不同的學號。')
            return redirect('register')

        # 生成隨機鹽值
        salt = generate_random_string(15)

        # 計算帶鹽的SM3雜湊值
        salted_hash_password = sm3_hash_with_salt(password_up, salt)
        print(salted_hash_password)

        priKey = PrivateKey()
        pubKey = priKey.publicKey()

        new_user = UserProfile.objects.create(
            id=id,
            username_up=username_up,
            email=email,
            password_up=salted_hash_password,  # 儲存帶鹽的SM3雜湊值
            salt=salt,  # 儲存鹽值
            public_key=pubKey.toString(compressed=False),  # 儲存公鑰
            private_key=priKey.toString(), # 儲存私鑰
            avatar='avatars/default_avatar.png'
        )
        new_user.save()

        LogData = Log.objects.create(
            username = new_user.username_up,
            documentname = "無",
            operation = f'使用者{new_user.username_up}於{timezone.now()}註冊了賬號。'
        )
        LogData.save()

        # 新增成功訊息
        messages.success(request, '註冊成功,請登入。')
        time.sleep(3)
        return redirect('login')

    return render(request, 'register.html')  # 替換為你的模板路徑

當請求方法為 POST 時,表示使用者提交了登錄檔單。
獲取使用者輸入的學號、使用者名稱、郵箱和密碼。
檢查學號的唯一性,如果該學號已存在,則返回錯誤訊息並重定向到註冊頁面。
生成一個隨機的鹽值,用於加密密碼。
使用 SM3 雜湊演算法計算帶鹽的密碼雜湊值。
使用 ECC 演算法生成公鑰和私鑰。
建立一個新的使用者物件,將學號、使用者名稱、郵箱、密碼雜湊、鹽值、公鑰、私鑰和預設頭像路徑等資訊儲存到資料庫中。
記錄使用者的註冊操作到日誌中。
新增成功訊息,並在3秒後重定向到登入頁面。

sm3_hash_with_salt邏輯
def sm3_hash_with_salt(password, salt):
    # 使用SM3演算法生成雜湊值
    hash_hex = sm3_hash(func.bytes_to_list(bytes(password, encoding='utf-8')))
    # 使用提供的鹽替換提取部分
    new_hash_hex = hash_hex[:4] + salt + hash_hex[19:]
    return new_hash_hex

接收兩個引數 password 和 salt,password 是要進行雜湊的密碼,salt 是要新增到雜湊中的鹽值。
將密碼轉換為 UTF-8 編碼的位元組序列,並透過 sm3_hash 函式生成密碼的 SM3 雜湊值,得到一個長度為 64 的十六進位制雜湊字串 hash_hex。
將 hash_hex 中的一部分提取出來,並在其中間插入鹽值,然後返回這個新的雜湊字串。具體來說,將 hash_hex 的前 4 位與鹽值和後面的部分(從第 20 位開始)連線起來,這樣就生成了帶鹽的密碼雜湊值,長度仍然為 64。

generate_random_string邏輯
def generate_random_string(length=15):
    # 生成一個隨機字串
    characters = string.ascii_letters + string.digits + string.punctuation
    random_string = ''.join(random.choice(characters) for i in range(length))
    return random_string

接收一個引數 length,表示要生成的隨機字串的長度,預設為 15。
定義了一個包含大寫字母、小寫字母、數字和標點符號的字符集 characters。
使用列表推導式和 random.choice 函式從 characters 中隨機選擇字元,並將它們連線成一個長度為 length 的隨機字串 random_string。
返回生成的隨機字串。

index邏輯
def index(request):
    # 檢查使用者是否登入
    if 'user_id' in request.session:
        user_id = request.session['user_id']
        username = request.session['username']
        try:
            # 根據使用者名稱查詢使用者的訪問許可權
            user_profile = UserProfile.objects.get(username_up=username)
            access_level = user_profile.access_level

            # 獲取使用者頭像的 URL
            user_avatar_url = user_profile.avatar.url if user_profile.avatar else '/avatars/default_avatar.png'
            print(user_avatar_url)
            # 調整圖片大小
            # 假設使用者頭像在media資料夾下的avatars資料夾內
            if user_avatar_url and 'avatars' in user_avatar_url:
                image_path = join(settings.BASE_DIR, 'web', user_avatar_url.lstrip('/').replace('/', '\\'))  # 移除url開頭的斜槓並替換斜槓為反斜槓
                print(settings.MEDIA_ROOT)
                print(user_avatar_url[1:])
                print(image_path)
                image = Image.open(image_path)
                resized_image = image.resize((60, 60))  # 設定新的寬度和高度
                resized_image.save(image_path)  # 覆蓋原始圖片檔案

            # 構建完整的 URL
            user_avatar_full_url = request.build_absolute_uri(user_avatar_url)

            return render(request, 'index.html',
                          {'user_id': user_id, 'username': username, 'access_level': access_level,
                           'user_avatar_url': user_avatar_full_url})
        except UserProfile.DoesNotExist:
            # 處理未找到使用者的情況
            # 可以引發 Http404 異常或者進行其他適當的處理
            pass

    else:
        # 使用者未登入,可以重定向到登入頁面或其他處理
        return redirect('login')

首先檢查使用者是否已經登入,透過檢查 session 中是否存在 user_id 鍵來判斷。
如果使用者已登入,從 session 中獲取使用者的 user_id 和 username。
根據使用者名稱查詢使用者的訪問許可權,並獲取使用者頭像的 URL。如果使用者沒有上傳頭像,則使用預設頭像的 URL。
對使用者頭像進行調整大小的操作。這部分程式碼假設使用者頭像儲存在 media 資料夾下的 avatars 資料夾內,根據 URL 的路徑構建圖片的絕對路徑,然後使用 PIL 庫開啟圖片並調整大小,最後儲存覆蓋原始圖片檔案。
構建完整的使用者頭像 URL,使用 request.build_absolute_uri 方法。
將使用者的 user_id、username、訪問許可權 access_level 和頭像 URL user_avatar_full_url 傳遞給模板,渲染主頁。
如果查詢使用者資訊時發生 UserProfile.DoesNotExist 異常,說明未找到對應的使用者,可以引發 Http404 異常或者進行其他適當的處理。
如果使用者未登入,重定向到登入頁面。

get_users邏輯
def get_users(request):
    # 從資料庫中獲取使用者資訊
    users = UserProfile.objects.all().values()  # 假設 User 有合適的欄位來表示使用者資訊

    # 將查詢到的資料轉換為列表,並以 JSON 格式返回給前端
    return JsonResponse(list(users), safe=False)

使用 UserProfile.objects.all() 查詢資料庫中的所有使用者資訊。
使用 .values() 方法將查詢結果轉換為一個包含每個使用者資訊的字典的 QuerySet。
使用 JsonResponse 將 QuerySet 轉換為 JSON 格式,並返回給前端。由於 JsonResponse 預設不允許直接序列化 QuerySet,需要將 safe 引數設定為 False,表示可以序列化任意型別的資料結構,包括 QuerySet。

create_user邏輯
def create_user(request):
    current_user = request.session['username']
    if request.method == 'POST':
        form = UserForm(request.POST)
        if form.is_valid():
            # 獲取表單資料並儲存到資料庫
            id = form.cleaned_data['id']
            username = form.cleaned_data['username_up']
            email = form.cleaned_data['email']
            password = form.cleaned_data['password_up']

            # 生成隨機鹽值
            salt = generate_random_string(15)
            # 計算帶鹽的SM3雜湊值
            salted_hash_password = sm3_hash_with_salt(password, salt)

            priKey = PrivateKey()
            pubKey = priKey.publicKey()

            # 儲存到資料庫中
            UserProfile.objects.create(
                id=id,
                username_up=username,
                email=email,
                password_up=salted_hash_password,  # 儲存帶鹽的SM3雜湊值
                salt=salt,  # 儲存鹽值
                public_key=pubKey.toString(compressed=False),  # 儲存公鑰
                private_key=priKey.toString(),  # 儲存私鑰
                avatar='avatars/default_avatar.png'
            )

            LogData = Log.objects.create(
                username=current_user,
                documentname="無",
                operation=f'使用者{current_user}於{timezone.now()}建立了新使用者{username}。'
            )
            LogData.save()

            # 重定向到 index 頁面,使用 HttpResponseRedirect 物件
            return HttpResponseRedirect(reverse('index'))

    else:
        form = UserForm()

    return render(request, 'adduser.html', {'form': form})

獲取當前登入使用者的使用者名稱。
如果請求方法為 POST,表示使用者提交了建立新使用者的表單。
使用表單 UserForm 驗證提交的資料是否有效。如果表單資料有效,繼續以下步驟:
獲取表單中的使用者 ID、使用者名稱、郵箱和密碼。
生成一個隨機鹽值。
計算帶鹽的 SM3 雜湊值,並將其作為加密後的密碼儲存。
生成使用者的公鑰和私鑰。
將使用者的 ID、使用者名稱、郵箱、加密後的密碼、鹽值、公鑰、私鑰和頭像路徑儲存到資料庫中。
建立一條日誌記錄,記錄當前使用者建立新使用者的操作。
重定向到主頁。
如果請求方法不是 POST,即使用者訪問建立使用者頁面,渲染包含使用者表單的模板。

delete_user邏輯
def delete_user(request, user_id):
    current_user = request.session['username']
    if request.method == 'DELETE':
        user = get_object_or_404(UserProfile, id=user_id)

        LogData = Log.objects.create(
            username=current_user,
            documentname="無",
            operation=f'使用者{current_user}於{timezone.now()}刪除了使用者{user.username_up}。'
        )
        LogData.save()

        user.delete()
        return JsonResponse({'message': 'User deleted successfully'}, status=200)
    else:
        return JsonResponse({'message': 'Invalid request method'}, status=400)

獲取當前登入使用者的使用者名稱。
當請求方法為 DELETE 時,表示使用者提交了刪除使用者的請求。
透過使用者的 ID 從資料庫中獲取要刪除的使用者物件。
記錄使用者刪除操作到日誌中。
刪除資料庫中的使用者記錄。
返回一個成功的 JSON 響應。
如果請求方法不是 DELETE,則返回一個錯誤的 JSON 響應。

change_userinfo邏輯
def change_userinfo(request, user_id):
    current_user = request.session['username']
    try:
        user_profile = UserProfile.objects.get(id=user_id)

        # 如果是 POST 請求,即提交表單
        if request.method == 'POST':
            # 獲取表單中的資料
            username = request.POST.get('username_up')
            email = request.POST.get('email')
            password = request.POST.get('password_up')

            # 如果密碼不為空,則更新密碼
            if password:
                # 生成新的隨機鹽值
                salt = generate_random_string(15)
                # 計算帶鹽的SM3雜湊值
                salted_hash_password = sm3_hash_with_salt(password, salt)
                # 更新使用者密碼和鹽值
                user_profile.password_up = salted_hash_password
                user_profile.salt = salt

            # 更新使用者名稱和郵箱
            user_profile.username_up = username
            user_profile.email = email

            LogData = Log.objects.create(
                username=current_user,
                documentname="無",
                operation=f'使用者{current_user}於{timezone.now()}修改了{username}的使用者資訊。'
            )
            LogData.save()

            # 儲存更新後的資訊到資料庫
            user_profile.save()

            messages.success(request, '使用者資訊更新成功。')
            return redirect('index')  # 更新成功後重定向到首頁
        else:
            # 如果是 GET 請求,即使用者訪問修改頁面
            return render(request, 'change_userinfo.html', {'user_profile': user_profile})

    except UserProfile.DoesNotExist:
        messages.error(request, '使用者不存在。')
        return redirect('index')

獲取當前登入使用者的使用者名稱。
使用使用者 ID 查詢使用者資料。如果使用者存在,繼續以下步驟:
如果請求方法為 POST,表示使用者提交了修改使用者資訊的表單:
從表單中獲取使用者名稱、郵箱和密碼。
如果密碼不為空,則執行以下操作:
生成新的隨機鹽值。
計算帶鹽的 SM3 雜湊值。
更新使用者的密碼和鹽值。
更新使用者名稱和郵箱。
建立一條日誌記錄,記錄當前使用者修改使用者資訊的操作。
儲存更新後的使用者資訊到資料庫。
新增一條成功訊息,提示使用者資訊更新成功。
重定向到主頁。

manage_permission邏輯
def manage_permission(request, user_id):
    current_user = request.session['username']
    # 根據 user_id 獲取特定使用者的資訊或許可權
    user = UserProfile.objects.get(id=user_id)

    if request.method == 'POST':
        access_level = request.POST['access_level']
        user.access_level = access_level

        LogData = Log.objects.create(
            username=current_user,
            documentname="無",
            operation=f'使用者{current_user}於{timezone.now()}修改了{user.username_up}的訪問許可權。'
        )
        LogData.save()

        user.save()  # 將更改後的訪問許可權儲存到資料庫

        # 重定向到 index 頁面
        return redirect('index')

    # 渲染 manage_permission.html 頁面,並傳遞使用者資訊或許可權
    return render(request, 'manage_permission.html', {'user': user})

獲取當前登入使用者的使用者名稱。
根據使用者的 ID 從資料庫中獲取要管理許可權的使用者物件。
如果是 POST 請求,即使用者提交了修改使用者許可權的表單,獲取表單中的訪問級別資訊,並更新使用者的訪問級別到資料庫中。
記錄使用者修改許可權操作到日誌中。
將更改後的訪問許可權儲存到資料庫中。
重定向到主頁。
如果是 GET 請求,即使用者訪問管理許可權頁面,渲染包含使用者許可權資訊的模板,將使用者資訊傳遞給模板以便顯示。

logout邏輯
def logout(request):
    current_user = request.session['username']
    # 清除會話資訊
    if 'user_id' in request.session:

        LogData = Log.objects.create(
            username=current_user,
            documentname="無",
            operation=f'使用者{current_user}於{timezone.now()}退出了系統。'
        )

        LogData.save()

        del request.session['user_id']
        del request.session['username']

    # 重定向到主頁或登入頁
    return redirect('index')  # 'index' 應該是你的主頁 URL 名稱或路徑

從會話中獲取當前使用者的使用者名稱。
檢查會話中是否存在使用者ID('user_id'),如果存在,則執行以下操作:
建立一條日誌記錄,記錄使用者的退出操作。
刪除會話中的使用者ID和使用者名稱資訊。
將使用者重定向到主頁或登入頁。

upload_avatar_page邏輯
def upload_avatar_page(request):
    # 傳遞表單給模板,以便在頁面上顯示上傳表單
    form = AvatarUploadForm()
    return render(request, 'upload_avatar.html', {'form': form})

建立一個 AvatarUploadForm 的例項物件 form,用於處理頭像上傳的表單資料。
使用 render 函式將請求、模板檔名和一個包含表單物件的字典傳遞給模板,以便在頁面上顯示上傳頭像的表單。

upload_avatar邏輯
def upload_avatar(request):
    current_user = request.session.get('username')  # 使用 get 方法以避免鍵不存在時引發 KeyError
    if request.method == 'POST':
        form = AvatarUploadForm(request.POST, request.FILES)
        if form.is_valid():
            try:
                user_profile = UserProfile.objects.get(username_up=current_user)
                avatar_image = form.cleaned_data['avatarFile']
                image = Image.open(avatar_image)
                # 將影像轉換為PNG格式並覆蓋原始影像檔案
                image = image.convert("RGB")
                image.save(user_profile.avatar.path, 'JPEG')
                user_profile.save()
                return redirect('index')  # 重定向到首頁或其他頁面
            except UserProfile.DoesNotExist:
                # 處理使用者不存在的情況
                pass
    else:
        form = AvatarUploadForm()

    return render(request, 'upload_avatar.html', {'form': form})

使用 request.session.get('username') 獲取當前使用者的使用者名稱,避免在鍵不存在時引發 KeyError。
檢查請求方法是否為 POST,如果是,則使用 AvatarUploadForm 處理請求中的表單資料和檔案。
如果表單資料有效 (form.is_valid()),則嘗試獲取當前使用者的使用者資料物件 (UserProfile),然後從表單資料中獲取上傳的頭像檔案 (avatarFile)。
使用 PIL 庫開啟上傳的影像檔案,並將其轉換為 JPEG 格式,然後儲存到使用者資料物件的頭像路徑 (avatar.path) 中,並儲存使用者資料物件。
如果使用者不存在 (UserProfile.DoesNotExist),則忽略異常。
如果請求方法不是 POST,則建立一個空的 AvatarUploadForm 物件,並將其傳遞給模板以顯示上傳頭像的表單。
最後,使用 render 函式將請求、模板檔名和包含表單物件的字典傳遞給模板,以便在頁面上顯示上傳頭像的表單。

公文管理模組

  • 公文的建立、編輯、刪除等。

資料庫中的公文模型

class Document(models.Model):
    document_id = models.AutoField(primary_key=True)
    document_owner = models.CharField(max_length=255)
    document_name = models.CharField(max_length=255)
    issuing_office = models.CharField(max_length=255)
    issue_date = models.DateTimeField()
    security_level = models.CharField(max_length=50)
    cc_office = models.CharField(max_length=255)
    file_type = models.CharField(max_length=50)
    modifier = models.CharField(max_length=50)
    modified_date = models.DateTimeField()
    file_address = models.CharField(max_length=255)
    is_sent = models.IntegerField(default=0)
    is_pass = models.IntegerField(default=0)

    def __str__(self):
        return self.document_name

document_id:文件的唯一識別符號,自增整數,是主鍵。
document_owner:文件的擁有者,字元長度為255。
document_name:文件的名稱,字元長度為255。
issuing_office:發文機構,字元長度為255。
issue_date:發文日期,日期時間型別。
security_level:文件的安全級別,字元長度為50。
cc_office:抄送機構,字元長度為255。
file_type:檔案型別,字元長度為50。
modifier:修改者,字元長度為50。
modified_date:修改日期,日期時間型別。
file_address:檔案地址,字元長度為255。
is_sent:是否已傳送,整數型別,預設值為0。
is_pass:是否透過,整數型別,預設值為0。
str:定義文件物件的字串表示形式,這裡返回文件名稱。

公文管理介面

index.html
{% load static %}
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="csrf-token" content="{{ csrf_token }}">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>公文傳輸系統</title>
    <link rel="stylesheet" href="{% static 'css/style-syspage.css' %}">
    <link rel="stylesheet" href="{% static 'css/style-newnavi.css' %}">
    <link rel="stylesheet" href="{% static 'css/sidebarContainer.css' %}">
    <link rel="stylesheet" href="{% static 'css/homepage.css' %}">
    <link rel="stylesheet" href="{% static 'css/textmanagerpage.css' %}">
    <link rel="stylesheet" href="{% static 'css/textpage.css' %}">
    <link rel="stylesheet" href="{% static 'css/usermanagerpage.css' %}">
    <script src="https://cdn.jsdelivr.net/npm/vue@3.0.11/dist/vue.global.prod.js"></script>
    <!-- 在<head>標籤中引入Quill.js的CSS檔案 -->
    <link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">

    <!-- 在<body>標籤中引入Quill.js的JavaScript檔案 -->
    <script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
</head>
<body>
    <div>
        <div class="navbar-top">
            <div class="menu-content">
            <div class="menu-icon">&#9776;</div>
            <input type="text" class="search-box" placeholder="搜尋...">
            <div class="user-info">
                <span class="help-button">幫助文件</span>
                <span class="user-name">您好 {{ username }}!</span>
                <input type="hidden" id="loggedInUserId" value="{{ user_id }}">
            </div>
        </div>
        </div>
        <div>
            <div class="navbar-left">
                <a href="/upload_avatar_page/">
                    <div class="Avatar">
                        <img src="{{ user_avatar_url }}" alt="User Avatar" class="user-avatar">
                    </div>
                </a>
                <div class="sys">
                    公文傳輸系統
                </div>
                <div class="nav-links">
                <!-- 在左側導航欄的每個功能鍵上新增@click事件處理程式 -->
                <a href="#" class="tab-button" @click="handleTabClick('首頁')" data-page="homePage">首頁</a>
                <a href="#" class="tab-button" @click="handleTabClick('公文管理')" data-page="documentManagementPage">公文管理</a>
                <a href="#" class="tab-button" @click="handleTabClick('公文稽核')" data-page="documentReviewPage">公文稽核</a>
                <a href="#" class="tab-button" @click="handleTabClick('公文草擬')" data-page="documentDraftPage">公文草擬</a>
                <a href="#" class="tab-button" @click="handleTabClick('使用者管理')" data-page="userManagementPage">使用者管理</a>
                <a href="#" class="tab-button" @click="handleTabClick('日誌管理')" data-page="logManagementPage">日誌管理</a>
                </div>
                <a href="#" class="quit" id="logoutButton">
                    退出系統
                </a>
            </div>
            <div id="sidebarContainer"></div>
        </div>
    </div>

    <script src="{% static 'js/main.js' %}"></script>
    <script>
        window.accessLevel = {{ access_level|default_if_none:0 }};
        const logoutButton = document.getElementById('logoutButton');

        logoutButton.addEventListener('click', (event) => {
            event.preventDefault();

            fetch('/logout/', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': getCookie('csrftoken') // 替換為獲取CSRF令牌的函式,這是一個示例
                },
                // ...其他請求資料
            })
            .then(response => {
                if (response.redirected) {
                    // 延遲3秒後執行重定向
                    setTimeout(() => {
                        window.location.href = response.url; // 重定向到指定頁面
                    }, 3000); // 3000毫秒 = 3秒
                }
            })
            .catch(error => {
                console.error('退出時出錯:', error);
            });
        });

        function getCookie(name) {
            const cookieValue = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
            return cookieValue ? cookieValue.pop() : '';
        }
    </script>
</body>
</html>

實現了一個具有基本功能的公文傳輸系統的前端介面,提供了使用者友好的介面和互動體驗。

公文管理邏輯

delete_document邏輯
@csrf_exempt
def delete_document(request, documentname):
    current_user = request.session['username']
    if request.method == 'DELETE':
        print(f'要刪除的公文:{documentname}')
        # 從資料庫中查詢要刪除的文件
        document = get_object_or_404(Document, document_name=documentname)

        file_enc_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'docx', (documentname + '.enc'))
        file_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'docxs', (documentname + '.docx'))
        key_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'key', (documentname + 'key.dat'))
        key_enc_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'key', (documentname + 'encryptkey.dat'))
        keyhash_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'secure', (documentname + 'hash_decrypted.dat'))
        keyorig_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'secure', (documentname + 'hash_original.dat'))
        keysign_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'sign', (documentname + 'signkey.dat'))
        if os.path.exists(file_enc_path):
            os.remove(file_enc_path)
            print(f"File {file_enc_path} deleted successfully")
        else:
            print(f"File {file_enc_path} does not exist")
        if os.path.exists(file_path):
            os.remove(file_path)
            print(f"File {file_path} deleted successfully")
        else:
            print(f"File {file_path} does not exist")
        if os.path.exists(key_path):
            os.remove(key_path)
            print(f"File {key_path} deleted successfully")
        else:
            print(f"File {key_path} does not exist")
        if os.path.exists(key_enc_path):
            os.remove(key_enc_path)
            print(f"File {key_enc_path} deleted successfully")
        else:
            print(f"File {key_enc_path} does not exist")
        if os.path.exists(keyhash_path):
            os.remove(keyhash_path)
            print(f"File {keyhash_path} deleted successfully")
        else:
            print(f"File {keyhash_path} does not exist")
        if os.path.exists(keyorig_path):
            os.remove(keyorig_path)
            print(f"File {keyorig_path} deleted successfully")
        else:
            print(f"File {keyorig_path} does not exist")
        if os.path.exists(keysign_path):
            os.remove(keysign_path)
            print(f"File {keysign_path} deleted successfully")
        else:
            print(f"File {keysign_path} does not exist")

        LogData = Log.objects.create(
            username=current_user,
            documentname=document.document_name,
            operation=f'使用者{current_user}於{timezone.now()}刪除了公文:{document.document_name}。'
        )
        LogData.save()

        # 刪除文件
        document.delete()

        # 返回成功的 JSON 響應
        return JsonResponse({'message': 'Document deleted successfully'})

    # 如果請求方法不是 DELETE,則返回錯誤響應
    return JsonResponse({'error': 'Invalid request method'}, status=400)

使用 @csrf_exempt 裝飾器來取消 CSRF 保護,這意味著該檢視可以接受不帶 CSRF 令牌的請求。
獲取當前登入使用者的使用者名稱。
當請求方法為 DELETE 時,表示使用者提交了刪除文件的請求。
從資料庫中查詢要刪除的文件,如果文件不存在,則返回 404 錯誤。
構建文件、金鑰和簽名檔案的路徑。
對每個檔案路徑進行檢查,如果檔案存在,則刪除該檔案,並在控制檯列印相應的訊息。
記錄文件刪除操作到日誌中。
刪除資料庫中的文件記錄。
返回一個成功的 JSON 響應。
如果請求方法不是 DELETE,則返回一個錯誤的 JSON 響應。

decrypt_document邏輯
def decrypt_document(request):
    current_user = request.session['username']
    if request.method == 'POST':
        # 獲取傳遞的檔案地址或其他必要資訊
        data = json.loads(request.body)
        file_title = data.get('file_title')
        file_name = data.get('file_name')

        print(file_title)
        print(file_name)

        doc = Document.objects.get(document_name=file_title)
        file_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'docx', file_name)
        print(file_path)
        doc_owner = doc.issuing_office
        cc_office = doc.cc_office
        sender_keyowner = UserProfile.objects.get(id=doc_owner)
        sender_publickey = sender_keyowner.public_key
        sender_privatekey = sender_keyowner.private_key
        key_owner = UserProfile.objects.get(username_up=cc_office)
        publickey = key_owner.public_key
        privatekey = key_owner.private_key

        key_sign_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'sign', (file_title + 'signkey.dat'))
        key_encrypt_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'key', (file_title + 'encryptkey.dat'))
        verify_signature(public_key=sender_publickey,
                         private_key=sender_privatekey,
                         input_file=key_encrypt_path,
                         signature_file=key_sign_path)

        key_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'key', (file_title + 'key.dat'))

        decrypt_data(public_key=publickey,
                     private_key=privatekey,
                     input_file=key_encrypt_path,
                     output_file=key_path)

        key = get_or_generate_key(file_title)
        print(f'對稱金鑰的型別為:{type(key)}')
        # 在這裡執行檔案解密的操作
        decrypted_file_address = decrypt_and_hash_file(file_path, key, file_title)

        LogData = Log.objects.create(
            username=current_user,
            documentname=doc.document_name,
            operation=f'使用者{current_user}於{timezone.now()}下載了公文:{doc.document_name}。'
        )
        LogData.save()

        # 返回解密結果(成功或失敗)
        return JsonResponse({'message': '檔案解密成功', 'decrypted_file_address': decrypted_file_address})  # 或者其他成功資訊
    else:
        return JsonResponse({'message': '無效的請求'}, status=400)

獲取當前登入使用者的使用者名稱:從會話中獲取當前登入使用者的使用者名稱 (current_user),用於記錄操作日誌。
處理 POST 請求:如果請求方法為 POST,則表示使用者提交了解密檔案的請求。解析請求體中的 JSON 資料,提取出檔案標題 (file_title) 和檔名 (file_name)。
獲取文件資訊:根據檔案標題查詢資料庫,找到對應的文件物件,並獲取相關資訊,如文件的釋出部門、抄送部門等。
獲取金鑰資訊:根據文件的釋出部門和抄送部門,獲取對應使用者的公鑰和私鑰。
驗證簽名:根據傳送者的公鑰和私鑰,驗證金鑰檔案的簽名是否有效。
解密金鑰:使用接收者的私鑰解密金鑰檔案,獲取對稱金鑰。
解密檔案:使用對稱金鑰解密檔案,並返回解密後的檔案地址。
記錄操作日誌:建立一條日誌記錄,記錄當前使用者下載了哪個公文,記錄操作的型別和時間。
返回 JSON 響應:如果檔案解密成功,返回狀態碼 200 和訊息 "檔案解密成功",同時返回解密後的檔案地址。如果請求方法不是 POST,則返回狀態碼 400 和訊息 "無效的請求"。

get_or_generate_key邏輯
def get_or_generate_key(document_name):
    # 嘗試從外部輸入獲取金鑰
    key_file_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'key', (document_name + 'key.dat'))
    print(key_file_path)
    if os.path.exists(key_file_path):
        with open(key_file_path, 'rb') as key_file:
            key = key_file.read()
            print(key)
            print(len(key))
            if len(key) != 16:
                print("金鑰長度必須為16位元組")
                return None
    else:
        # 生成隨機的16位元組金鑰
        key = secrets.token_bytes(16)
        with open(key_file_path, 'wb') as key_file:
            key_file.write(key)

    return key

構建金鑰檔案路徑:根據文件名稱構建金鑰檔案的路徑。
檢查金鑰檔案是否存在:檢查金鑰檔案是否存在於指定路徑中。
如果存在,則嘗試從檔案中讀取金鑰內容。
如果金鑰長度不是16位元組,則列印訊息並返回None。
如果不存在,則生成一個16位元組的隨機金鑰,並將其寫入檔案中。
返回金鑰:返回讀取或生成的金鑰。

encrypt_and_hash_file邏輯
def encrypt_and_hash_file(input_file_path, key, document_name):
    # 讀取檔案內容
    with open(input_file_path, 'rb') as file:
        plaintext = file.read()

    # 計算檔案的雜湊值
    hash_value = sm3_hash(list(plaintext))  # Convert bytes to list
    hash_file_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'secure', (document_name + 'hash_original.dat'))
    with open(hash_file_path, 'w') as hash_file:
        hash_file.write(hash_value)

    print(f'原檔案的雜湊值已儲存到:{hash_file_path}')

    # 初始化SM4加密器
    crypt_sm4 = CryptSM4()

    # 設定金鑰
    crypt_sm4.set_key(key, SM4_ENCRYPT)

    # 加密檔案內容
    ciphertext = crypt_sm4.crypt_ecb(plaintext)

    # 建立加密後的檔案
    encrypted_file_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'docx', (document_name + '.enc'))
    with open(encrypted_file_path, 'wb') as file:
        file.write(ciphertext)

    print(f'檔案加密成功:{encrypted_file_path}')

讀取檔案內容:使用二進位制模式開啟檔案,讀取檔案內容為 plaintext。
計算檔案雜湊值:使用 SM3 雜湊演算法計算檔案內容的雜湊值,並將其寫入檔案中。
初始化 SM4 加密器:建立一個 SM4 加密器物件。
設定金鑰:使用給定的金鑰設定 SM4 加密器的金鑰。
加密檔案內容:使用 ECB 模式對檔案內容進行 SM4 加密。
建立加密後的檔案:將加密後的內容寫入新的檔案中。
列印日誌:列印加密後的檔案路徑,以及原檔案的雜湊值儲存路徑。

decrypt_and_hash_file邏輯
def decrypt_and_hash_file(encrypted_file_path, key, document_name):
    # 初始化SM4解密器
    crypt_sm4 = CryptSM4()

    # 設定金鑰
    crypt_sm4.set_key(key, SM4_DECRYPT)

    # 讀取加密檔案內容
    with open(encrypted_file_path, 'rb') as file:
        ciphertext = file.read()

    # 解密檔案內容
    plaintext = crypt_sm4.crypt_ecb(ciphertext)

    # 建立解密後的檔案
    decrypted_file_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'docxs', (document_name + '.docx'))
    with open(decrypted_file_path, 'wb') as file:
        file.write(plaintext)

    print(f'檔案解密成功:{decrypted_file_path}')

    # 計算解密檔案的雜湊值
    hash_value = sm3_hash(list(plaintext))  # Convert bytes to list

    # 將雜湊值儲存到hash_decrypted.txt檔案
    hash_decrypted_file_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'secure', (document_name + 'hash_decrypted.dat'))
    with open(hash_decrypted_file_path, 'w') as hash_file:
        hash_file.write(hash_value)

    print(f'解密檔案的雜湊值已儲存到:{hash_decrypted_file_path}')

    # 比較原始雜湊和解密後的雜湊
    hash_original_file = os.path.join(settings.BASE_DIR, 'web', 'static', 'secure',
                                      (document_name + 'hash_original.dat'))
    hash_decrypted_file = os.path.join(settings.BASE_DIR, 'web', 'static', 'secure',
                                       (document_name + 'hash_decrypted.dat'))

    with open(hash_original_file, 'rb') as original, open(hash_decrypted_file, 'rb') as decrypted:
        original_hash = original.read()
        decrypted_hash = decrypted.read()

        if original_hash == decrypted_hash:
            print("加密和解密後的檔案內容一致。")
        else:
            print("加密和解密後的檔案內容不一致。")

    decrypted_file_path_str = f'/static/docxs/{document_name}'+'.docx'

    return decrypted_file_path_str

初始化 SM4 解密器:建立一個 SM4 解密器物件。
設定金鑰:使用給定的金鑰設定 SM4 解密器的金鑰。
讀取加密檔案內容:使用二進位制模式開啟加密檔案,讀取檔案內容為 ciphertext。
解密檔案內容:使用 ECB 模式對檔案內容進行 SM4 解密,得到解密後的內容 plaintext。
建立解密後的檔案:將解密後的內容寫入新的檔案中。
計算解密檔案的雜湊值:使用 SM3 雜湊演算法計算解密後檔案內容的雜湊值,並將其寫入檔案中。
比較原始雜湊和解密後的雜湊:讀取原始檔案和解密後檔案的雜湊值,比較它們是否相同,以驗證解密的正確性。
返回解密後檔案的路徑:返回解密後檔案的路徑字串,用於後續操作。

CurveFp邏輯
class CurveFp:
    def __init__(self, A, B, P, N, Gx, Gy, name):
        self.A = A
        self.B = B
        self.P = P
        self.N = N
        self.Gx = Gx
        self.Gy = Gy
        self.name = name


sm2p256v1 = CurveFp(
    name="sm2p256v1",
    A=0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC,
    B=0x28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93,
    P=0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF,
    N=0xFFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123,
    Gx=0x32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7,
    Gy=0xBC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0
)

這段程式碼定義了一個名為 CurveFp 的類,用於表示橢圓曲線的引數。具體解釋如下:
CurveFp 類有七個屬性:A、B、P、N、Gx、Gy 和 name,分別表示橢圓曲線的引數。
A、B、P、N 分別表示橢圓曲線方程中的引數,Gx 和 Gy 表示基點的座標,name 表示該橢圓曲線的名稱。
sm2p256v1 是一個例項化的 CurveFp 物件,表示了一個特定的橢圓曲線引數集,通常用於密碼學中的橢圓曲線加密演算法。

multiply邏輯
def multiply(a, n, N, A, P):
    return fromJacobian(jacobianMultiply(toJacobian(a), n, N, A, P), P)

實現了橢圓曲線上的點乘法運算。具體解釋如下:
multiply 函式接受五個引數:a 表示要進行乘法的點,n 表示乘法的倍數,N 表示橢圓曲線的引數 N,A 和 P 分別表示橢圓曲線的引數 A 和 P。
首先,呼叫 toJacobian(a) 將點 a 轉換為雅各比座標系下的表示。
然後,呼叫 jacobianMultiply 函式對轉換後的點進行倍乘運算,得到倍乘後的結果。
最後,呼叫 fromJacobian 函式將結果從雅各比座標系轉換回橢圓曲線上的點表示,並返回最終結果。

add邏輯
def add(a, b, A, P):
    return fromJacobian(jacobianAdd(toJacobian(a), toJacobian(b), A, P), P)

實現了橢圓曲線上的點加法運算。具體解釋如下:
add 函式接受四個引數:a 和 b 分別表示要進行加法運算的兩個點,A 和 P 分別表示橢圓曲線的引數 A 和 P。
首先,呼叫 toJacobian(a) 和 toJacobian(b) 將點 a 和 b 分別轉換為雅各比座標系下的表示。
然後,呼叫 jacobianAdd 函式對轉換後的兩個點進行加法運算,得到加法後的結果。
最後,呼叫 fromJacobian 函式將結果從雅各比座標系轉換回橢圓曲線上的點表示,並返回最終結果。

inv邏輯
def inv(a, n):
    if a == 0:
        return 0
    lm, hm = 1, 0
    low, high = a % n, n
    while low > 1:
        r = high // low
        nm, new = hm - lm * r, high - low * r
        lm, low, hm, high = nm, new, lm, low
    return lm % n

實現了求整數 a 在模 n 意義下的乘法逆元。具體解釋如下:
首先檢查 a 是否為 0,如果是則直接返回 0,因為 0 在模任何數意義下都不存在乘法逆元。
接著初始化兩組變數 lm、hm 和 low、high,分別代表上一次迭代的 m 和 m-1 的係數,以及 n 與 a 對 n 取餘的結果。
進入迴圈,當 low 大於 1 時,執行以下操作:
計算除法商 r,用於得到新一輪的 m 和 m-1 的係數。
更新 nm 和 new,分別代表新的 m 和 m-1 的係數。
更新 lm、low、hm、high,將 new 賦給 low,將 low 賦給 high,將 nm 賦給 hm,將 lm 賦給 low。
最後返回 lm % n,即 a 在模 n 意義下的乘法逆元。

toJacobian邏輯
def toJacobian(Xp_Yp):
    Xp, Yp = Xp_Yp
    return (Xp, Yp, 1)

接受一個二元組 Xp_Yp 作為引數,表示一個點的座標 (Xp, Yp)。
將輸入的點座標轉換為雅可比座標形式,即將 (Xp, Yp) 轉換為 (Xp, Yp, 1)。
返回轉換後的雅可比座標形式的點。

fromJacobian邏輯
def fromJacobian(Xp_Yp_Zp, P):
    Xp, Yp, Zp = Xp_Yp_Zp
    z = inv(Zp, P)
    return ((Xp * z ** 2) % P, (Yp * z ** 3) % P)

計算 Zp 的逆元 z,這裡使用了之前定義的 inv 函式,若 Zp 為 0,則返回 0。
計算仿射座標 (X, Y) 中的 X:X = (Xp * z^2) % P,其中 P 是橢圓曲線的模數。
計算仿射座標 (X, Y) 中的 Y:Y = (Yp * z^3) % P。

jacobianDouble邏輯
def jacobianDouble(Xp_Yp_Zp, A, P):
    Xp, Yp, Zp = Xp_Yp_Zp
    if not Yp:
        return (0, 0, 0)
    ysq = (Yp ** 2) % P
    S = (4 * Xp * ysq) % P
    M = (3 * Xp ** 2 + A * Zp ** 4) % P
    nx = (M ** 2 - 2 * S) % P
    ny = (M * (S - nx) - 8 * ysq ** 2) % P
    nz = (2 * Yp * Zp) % P
    return (nx, ny, nz)

獲取輸入點的座標 (Xp, Yp, Zp)。
如果輸入點的 Y 座標為 0,則返回 (0, 0, 0),表示無窮遠點。
計算 Y 座標的平方:ysq = (Yp ** 2) % P。
計算 S:S = (4 * Xp * ysq) % P。
計算 M:M = (3 * Xp ** 2 + A * Zp ** 4) % P。
計算新點的 X 座標:nx = (M ** 2 - 2 * S) % P。
計算新點的 Y 座標:ny = (M * (S - nx) - 8 * ysq ** 2) % P。
計算新點的 Z 座標:nz = (2 * Yp * Zp) % P。
返回新點的雅可比座標 (nx, ny, nz)。

jacobianAdd邏輯
def jacobianAdd(Xp_Yp_Zp, Xq_Yq_Zq, A, P):
    Xp, Yp, Zp = Xp_Yp_Zp
    Xq, Yq, Zq = Xq_Yq_Zq
    if not Yp:
        return (Xq, Yq, Zq)
    if not Yq:
        return (Xp, Yp, Zp)
    U1 = (Xp * Zq ** 2) % P
    U2 = (Xq * Zp ** 2) % P
    S1 = (Yp * Zq ** 3) % P
    S2 = (Yq * Zp ** 3) % P
    if U1 == U2:
        if S1 != S2:
            return (0, 0, 1)
        return jacobianDouble((Xp, Yp, Zp), A, P)
    H = U2 - U1
    R = S2 - S1
    H2 = (H * H) % P
    H3 = (H * H2) % P
    U1H2 = (U1 * H2) % P
    nx = (R ** 2 - H3 - 2 * U1H2) % P
    ny = (R * (U1H2 - nx) - S1 * H3) % P
    nz = (H * Zp * Zq) % P
    return (nx, ny, nz)

定義了 jacobianAdd 函式,用於計算橢圓曲線上點的雅可比座標的加法。
如果其中一個點的 Y 座標為 0,則直接返回另一個點的座標。
否則,根據雅可比座標的加法規則計算兩點之和的座標。
如果兩個點的 X 座標相等但 Y 座標不等,則返回無窮遠點的座標。
否則,根據加法公式計算兩點之和的新座標。

jacobianMultiply邏輯
def jacobianMultiply(Xp_Yp_Zp, n, N, A, P):
    Xp, Yp, Zp = Xp_Yp_Zp
    if Yp == 0 or n == 0:
        return (0, 0, 1)
    if n == 1:
        return (Xp, Yp, Zp)
    if n < 0 or n >= N:
        return jacobianMultiply((Xp, Yp, Zp), n % N, N, A, P)
    if (n % 2) == 0:
        return jacobianDouble(jacobianMultiply((Xp, Yp, Zp), n // 2, N, A, P), A, P)
    if (n % 2) == 1:
        return jacobianAdd(jacobianDouble(jacobianMultiply((Xp, Yp, Zp), n // 2, N, A, P), A, P), (Xp, Yp, Zp), A, P)

如果點的 Y 座標為 0 或 n 為 0,則返回無窮遠點的座標。
如果 n 為 1,則返回點的座標。
如果 n 小於 0 或大於等於 N(曲線的階),則將 n 取模為 N,並遞迴呼叫 jacobianMultiply。
如果 n 是偶數,則將點加倍,即呼叫 jacobianDouble。
如果 n 是奇數,則將點加倍後與原點相加,即先呼叫 jacobianDouble,然後呼叫 jacobianAdd。

PrivateKey邏輯
class PrivateKey:
    def __init__(self, curve=sm2p256v1, secret=None):
        self.curve = curve
        self.secret = secret or SystemRandom().randrange(1, curve.N)

    def publicKey(self):
        curve = self.curve
        xPublicKey, yPublicKey = multiply((curve.Gx, curve.Gy), self.secret, A=curve.A, P=curve.P, N=curve.N)
        return PublicKey(xPublicKey, yPublicKey, curve)

    def toString(self):
        return "{}".format(str(hex(self.secret))[2:].zfill(64))

init 方法用於初始化私鑰物件。它接受兩個引數:curve 表示橢圓曲線物件,預設為 sm2p256v1;secret 表示私鑰的值,預設為在曲線的階 N 內隨機選擇一個值作為私鑰。
publicKey 方法用於計算並返回對應的公鑰物件。它利用私鑰物件的 secret 值和橢圓曲線引數,在曲線上執行點乘運算,得到公鑰的座標 (xPublicKey, yPublicKey),並將其傳遞給 PublicKey 建構函式建立公鑰物件。
toString 方法用於將私鑰物件的 secret 值轉換為字串表示,並返回。在這裡,hex(self.secret) 將私鑰值轉換為十六進位制表示,然後透過字串格式化操作 str.format 將其填充為長度為 64 的字串(不足 64 位的在左側用 0 填充)。

PublicKey邏輯
class PublicKey:
    def __init__(self, x, y, curve):
        self.x = x
        self.y = y
        self.curve = curve

    def toString(self, compressed=True):
        return {
            True: str(hex(self.x))[2:],
            False: "{}{}".format(str(hex(self.x))[2:].zfill(64), str(hex(self.y))[2:].zfill(64))
        }.get(compressed)

init 方法用於初始化公鑰物件。它接受三個引數:x 和 y 分別表示公鑰在橢圓曲線上的 x 座標和 y 座標,curve 表示橢圓曲線物件。
toString 方法用於將公鑰物件轉換為字串表示。它接受一個可選引數 compressed,預設為 True,表示是否壓縮公鑰。根據 compressed 的值,使用字典的 get 方法返回相應的結果:
當 compressed 為 True 時,返回公鑰的 x 座標的十六進位制表示(去除開頭的 '0x')。
當 compressed 為 False 時,返回將公鑰的 x 和 y 座標的十六進位制表示連線起來,並使用 '0' 填充至 64 位長度的字串。

encrypt_data邏輯
def encrypt_data(public_key, private_key, input_file, output_file):
    sm2_crypt = sm2.CryptSM2(public_key=public_key, private_key=private_key)

    with open(input_file, 'rb') as f:
        data = f.read()
    print(public_key)
    print(private_key)
    print(type(public_key))
    print(type(private_key))
    encrypted_data = sm2_crypt.encrypt(data)

    with open(output_file, 'wb') as f:
        f.write(encrypted_data)

接受四個引數:public_key(公鑰)、private_key(私鑰)、input_file(輸入檔案路徑)和 output_file(輸出檔案路徑)。
建立 SM2 加密器 sm2_crypt。
開啟輸入檔案並讀取資料。
使用公鑰和私鑰對資料進行加密。
將加密後的資料寫入輸出檔案。

decrypt_data邏輯
def decrypt_data(public_key, private_key, input_file, output_file):
    sm2_crypt = sm2.CryptSM2(public_key=public_key, private_key=private_key)

    with open(input_file, 'rb') as f:
        encrypted_data = f.read()

    decrypted_data = sm2_crypt.decrypt(encrypted_data)

    with open(output_file, 'wb') as f:
        f.write(decrypted_data)

接受四個引數:public_key(公鑰)、private_key(私鑰)、input_file(輸入檔案路徑)和 output_file(輸出檔案路徑)。
建立 SM2 解密器 sm2_crypt。
開啟輸入檔案並讀取加密後的資料。
使用公鑰和私鑰對加密後的資料進行解密。
將解密後的資料寫入輸出檔案。

sign_data邏輯
def sign_data(public_key, private_key, input_file, signature_file):
    sm2_crypt = sm2.CryptSM2(public_key=public_key, private_key=private_key)

    with open(input_file, 'rb') as f:
        data = f.read()

    random_hex_str = func.random_hex(sm2_crypt.para_len)
    signature = sm2_crypt.sign(data, random_hex_str)

    with open(signature_file, 'wb') as f:
        f.write(binascii.unhexlify(signature))

接受四個引數:public_key(公鑰)、private_key(私鑰)、input_file(輸入檔案路徑)和 signature_file(簽名檔案路徑)。
建立 SM2 簽名器 sm2_crypt。
開啟輸入檔案並讀取資料。
生成隨機的十六進位制字串 random_hex_str,長度為簽名器 sm2_crypt 的引數長度。
使用私鑰對資料進行簽名,得到簽名結果 signature。
將簽名結果寫入輸出檔案,使用 binascii.unhexlify 將十六進位制字串轉換為位元組流。

verify_signature邏輯
def verify_signature(public_key, private_key, input_file, signature_file):
    sm2_crypt = sm2.CryptSM2(public_key=public_key, private_key=private_key)

    with open(input_file, 'rb') as f:
        data = f.read()

    with open(signature_file, 'rb') as f:
        signature = f.read()

    if sm2_crypt.verify(binascii.hexlify(signature).decode(), data):
        print("Signature verification successful")
    else:
        print("Signature verification failed")

建立一個 SM2 加密器 sm2_crypt,使用提供的公鑰和私鑰初始化。
開啟輸入檔案 input_file 並讀取其中的資料。
開啟簽名檔案 signature_file 並讀取簽名資料。
呼叫 SM2 加密器的 verify 方法驗證簽名。如果驗證成功,則列印"Signature verification successful",否則列印"Signature verification failed"。

審批流程模組

  • 公文的審批流程設計,包括多級審批、並行審批等。

審批流程邏輯

get_documents_in_docjudge邏輯
def get_documents_in_docjudge(request):
    current_user = request.session['username']
    # 從資料庫中獲取所有公文

    documents = Document.objects.filter(cc_office=current_user, is_sent=1)
    # 將公文資料轉換為 JSON 格式
    data = [
        {
            'current_user': current_user,
            'id': doc.document_id,
            'title': doc.document_name,
            'securityLevel': doc.security_level,
            'owner': doc.document_owner,
            'office': doc.issuing_office,
            'sendTime': doc.issue_date.strftime('%Y-%m-%d'),  # 將日期格式化為字串
            'is_sent': doc.is_sent,
            'cc_office': doc.cc_office,
            'file_address': doc.file_address,
            'is_pass': doc.is_pass,
            # 可以新增其他欄位
        }
        for doc in documents
    ]

    # 傳送 JSON 格式的資料給前端
    return JsonResponse(data, safe=False)

獲取當前登入使用者的使用者名稱:
從會話中獲取當前登入使用者的使用者名稱。
查詢資料庫中需要當前使用者稽核的公文:
查詢抄送辦公室為當前使用者且已傳送的公文。
將公文資料轉換為 JSON 格式:
將公文物件轉換為字典列表,包括公文 ID、標題、安全級別、所有者、釋出辦公室、釋出日期、是否已傳送、抄送辦公室、檔案地址和稽核狀態。
返回 JSON 資料:
使用 JsonResponse 將轉換後的公文資料以 JSON 格式傳送給前端。

approveDocument邏輯
@csrf_exempt
def approveDocument(request):
    current_user = request.session['username']
    if request.method == 'POST':
        data = json.loads(request.body)
        document_name = data.get('documentName')

        # 新增日誌列印
        print("Received document_name:", document_name)
        try:
            # 根據 document_id 獲取對應的文件
            document = Document.objects.get(document_name=document_name)
            document.is_pass = 1
            document.is_sent = 1

            LogData = Log.objects.create(
                username=current_user,
                documentname=document.document_name,
                operation=f'使用者{current_user}於{timezone.now()}透過了公文:{document.document_name}。'
            )
            LogData.save()

            document.save()

            return JsonResponse({'message': 'Document status updated successfully'})
        except Document.DoesNotExist:
            print(f"Document with ID {document_name} does not exist.")
            return JsonResponse({'message': 'Document does not exist'}, status=404)
        except Exception as e:
            print("Error:", e)
            return JsonResponse({'message': 'Internal Server Error'}, status=500)

    return JsonResponse({'message': 'Invalid request'}, status=400)

獲取當前登入使用者的使用者名稱:從會話中獲取當前登入使用者的使用者名稱 (current_user),用於記錄操作日誌。
處理 POST 請求:如果請求方法為 POST,則表示使用者提交了批准文件的請求。解析請求體中的 JSON 資料,提取出文件名稱 (documentName)。
更新文件狀態:根據文件名稱查詢資料庫,找到對應的文件物件。將文件的 is_pass 欄位和 is_sent 欄位都設定為 1,表示文件已經透過審批並且已傳送。
記錄操作日誌:建立一條日誌記錄,記錄當前使用者透過了哪個文件,記錄操作的型別和時間。
返回 JSON 響應:如果文件不存在,返回狀態碼 404 和訊息 "Document does not exist"。如果發生其他異常,返回狀態碼 500 和訊息 "Internal Server Error"。如果更新成功,返回狀態碼 200 和訊息 "Document status updated successfully"。

rejectDocument邏輯
@csrf_exempt
def rejectDocument(request):
    current_user = request.session['username']
    if request.method == 'POST':
        data = json.loads(request.body)
        document_name = data.get('documentName')
        doc_issuing_office = data.get('issuing_office')

        # 新增日誌列印
        print("Received document_name:", document_name)
        print("Received doc_issuing_office:", doc_issuing_office)
        try:
            # 根據 document_id 獲取對應的文件
            document = Document.objects.get(document_name=document_name)
            document.is_pass = 0
            document.is_sent = 0
            document.document_owner = document.issuing_office

            LogData = Log.objects.create(
                username=current_user,
                documentname=document.document_name,
                operation=f'使用者{current_user}於{timezone.now()}拒絕了公文:{document.document_name}。'
            )
            LogData.save()

            document.save()

            return JsonResponse({'message': 'Document status updated successfully'})
        except Document.DoesNotExist:
            print(f"Document with ID {document_name} does not exist.")
            return JsonResponse({'message': 'Document does not exist'}, status=404)
        except Exception as e:
            print("Error:", e)
            return JsonResponse({'message': 'Internal Server Error'}, status=500)

    return JsonResponse({'message': 'Invalid request'}, status=400)

獲取當前登入使用者的使用者名稱:從會話中獲取當前登入使用者的使用者名稱 (current_user),用於記錄操作日誌。
處理 POST 請求:如果請求方法為 POST,則表示使用者提交了拒絕文件的請求。解析請求體中的 JSON 資料,提取出文件名稱 (documentName) 和文件的釋出部門 (issuing_office)。
更新文件狀態:根據文件名稱查詢資料庫,找到對應的文件物件。將文件的 is_pass 欄位和 is_sent 欄位都設定為 0,表示文件被拒絕並且未傳送。將文件的 document_owner 欄位設定為文件的釋出部門,即回退至上一級審批人。
記錄操作日誌:建立一條日誌記錄,記錄當前使用者拒絕了哪個文件,記錄操作的型別和時間。
返回 JSON 響應:如果文件不存在,返回狀態碼 404 和訊息 "Document does not exist"。如果發生其他異常,返回狀態碼 500 和訊息 "Internal Server Error"。如果更新成功,返回狀態碼 200 和訊息 "Document status updated successfully"。

通知模組

  • 公文的分發和通知功能。

通知邏輯

update_document_status邏輯
@csrf_exempt
def update_document_status(request):
    current_user = request.session['username']
    if request.method == 'POST':
        data = json.loads(request.body)
        document_name = data.get('documentName')
        cc_office = data.get('ccOffice')

        # 新增日誌列印
        print("Received document_name:", document_name)
        print("Received cc_office:", cc_office)
        try:
            # 根據 document_id 獲取對應的文件
            document = Document.objects.get(document_name=document_name)
            document.is_sent = 1
            document.document_owner = cc_office

            LogData = Log.objects.create(
                username=current_user,
                documentname=document.document_name,
                operation=f'使用者{current_user}於{timezone.now()}傳送了公文:{document.document_name}。'
            )
            LogData.save()

            document.save()

            return JsonResponse({'message': 'Document status updated successfully'})
        except Document.DoesNotExist:
            print(f"Document with ID {document_name} does not exist.")
            return JsonResponse({'message': 'Document does not exist'}, status=404)
        except Exception as e:
            print("Error:", e)
            return JsonResponse({'message': 'Internal Server Error'}, status=500)

    return JsonResponse({'message': 'Invalid request'}, status=400)

獲取當前登入使用者的使用者名稱:從會話中獲取當前登入使用者的使用者名稱 (current_user),用於記錄操作日誌。
處理 POST 請求:如果請求方法為 POST,則表示使用者提交了更新文件狀態的請求。解析請求體中的 JSON 資料,提取出文件名稱 (documentName) 和抄送辦公室 (ccOffice)。
更新文件狀態:根據文件名稱查詢資料庫,找到對應的文件物件。將文件的 is_sent 欄位設定為 1,表示文件已傳送。將文件的 document_owner 欄位設定為抄送辦公室。
記錄操作日誌:建立一條日誌記錄,記錄當前使用者傳送了哪個文件,記錄操作的型別和時間。
返回 JSON 響應:如果文件不存在,返回狀態碼 404 和訊息 "Document does not exist"。如果發生其他異常,返回狀態碼 500 和訊息 "Internal Server Error"。如果更新成功,返回狀態碼 200 和訊息 "Document status updated successfully"。

存檔模組

  • 公文的存檔和查詢功能。

資料庫中的日誌模型

class Log(models.Model):
    username = models.CharField(max_length=8)
    documentname = models.CharField(max_length=255)
    operation = models.CharField(max_length=2000)

    def __str__(self):
        return self.operation

username:使用者名稱,字元長度為8。
documentname:文件名稱,字元長度為255。
operation:操作記錄,字元長度為2000。
str:定義日誌物件的字串表示形式,這裡返回操作記錄。

存檔邏輯

save_document邏輯
def save_document(request: HttpRequest):
    if request.method == 'POST':
        try:
            # 獲取當前登入的使用者
            current_user = request.session['user_id']
            current_user_name = request.session['username']

            data = json.loads(request.body)
            title = data.get('title')
            content = data.get('content')
            security_level = data.get('securityLevel')
            cc_office = data.get('cc_office')
            print(data)
            file_address = data.get('file_address')

            # 獲取當前時間
            current_time = timezone.now()
            docname = file_address + '.docx'
            # 建立一個新的文件物件
            doc = Docu()

            # 新增標題
            doc.add_heading(title, level=1)

            # 新增 HTML 內容
            doc.add_paragraph(content)

            # 儲存文件
            file_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'docx', docname)
            # 在儲存文件前列印檔案路徑
            print("File path:", file_path)

            # 檢查文件物件是否被正確建立
            print("Document:", doc)
            doc.save(file_path)

            # 儲存文件資訊到資料庫
            new_document = Document.objects.create(
                document_name=title,
                document_owner = current_user,
                issuing_office=current_user,
                issue_date=current_time,
                security_level=security_level,
                cc_office=cc_office,
                file_type='docx',
                modifier=current_user,
                modified_date=current_time,
                file_address=docname
            )
            new_document.save()
            print("Document saved successfully to the database.")

            key = get_or_generate_key(file_address) ##sm4金鑰
            encrypt_and_hash_file(file_path, key, file_address)

            # 刪除原始檔案
            os.remove(file_path)
            print(f'原檔案已刪除:{file_path}')

            sender = UserProfile.objects.get(id=current_user)
            sender_private_key = sender.private_key
            sender_public_key = sender.public_key

            cc_office_user = UserProfile.objects.get(username_up=cc_office)
            cc_office_private_key = cc_office_user.private_key
            cc_office_public_key = cc_office_user.public_key

            key_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'key', (file_address + 'key.dat'))
            print(key_path)
            key_encrypt_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'key', (file_address + 'encryptkey.dat'))
            encrypt_data(public_key=cc_office_public_key,
                         private_key=cc_office_private_key,
                         input_file=key_path,
                         output_file=key_encrypt_path)
            os.remove(key_path)
            key_sign_path = os.path.join(settings.BASE_DIR, 'web', 'static', 'sign', (file_address + 'signkey.dat'))
            print(key_sign_path)
            sign_data(public_key=sender_public_key,
                      private_key=sender_private_key,
                      input_file=key_encrypt_path,
                      signature_file=key_sign_path)

            LogData = Log.objects.create(
                username=current_user_name,
                documentname=new_document.document_name,
                operation=f'使用者{current_user_name}於{timezone.now()}建立了公文:{new_document.document_name}。'
            )
            LogData.save()

            return HttpResponse({'message': 'Document saved successfully'})  # 返回成功訊息
        except Exception as e:
            traceback.print_exc()  # 列印異常資訊到控制檯
            return HttpResponse({'message': 'Internal Server Error'}, status=500)

    return HttpResponse({'message': 'Invalid request'}, status=400)  # 處理無效請求

當請求方法為 POST 時,表示使用者提交了儲存文件的請求。
從 session 中獲取當前登入使用者的資訊,包括使用者ID和使用者名稱。
解析請求中的 JSON 資料,包括文件標題、內容、安全級別、抄送辦公室、檔案地址等資訊。
建立一個新的 Word 文件物件,並新增標題和內容。
將文件儲存到伺服器上的指定路徑。
將文件資訊儲存到資料庫中,包括文件名稱、所有者、簽發辦公室、簽發日期、安全級別、抄送辦公室等資訊。
獲取或生成 SM4 金鑰,並使用該金鑰對文件進行加密和雜湊。
刪除原始檔案。
獲取當前使用者和抄送辦公室使用者的公鑰和私鑰,並對金鑰進行加密和簽名。
記錄文件儲存操作到日誌中。
返回一個成功訊息或錯誤訊息,取決於操作的結果。如果儲存文件過程中出現異常,則會列印異常資訊並返回一個內部伺服器錯誤的訊息。

get_documents_in_docmanager邏輯
def get_documents_in_docmanager(request):
    current_user = request.session['user_id']
    # 從資料庫中獲取所有公文

    documents = Document.objects.filter(document_owner=current_user, is_sent=0)
    # 將公文資料轉換為 JSON 格式
    data = [
        {
            'current_user': current_user,
            'id': doc.document_id,
            'title': doc.document_name,
            'securityLevel': doc.security_level,
            'owner': doc.document_owner,
            'office': doc.issuing_office,
            'sendTime': doc.issue_date.strftime('%Y-%m-%d'),  # 將日期格式化為字串
            'is_sent': doc.is_sent,
            'cc_office': doc.cc_office,
            'file_address': doc.file_address
            # 可以新增其他欄位
        }
        for doc in documents
    ]

    # 傳送 JSON 格式的資料給前端
    return JsonResponse(data, safe=False)

獲取當前登入使用者的使用者 ID。
從資料庫中獲取所有當前使用者擁有且未傳送的文件 (is_sent=0 表示未傳送)。
將文件資料轉換為 JSON 格式。具體來說,對於每個文件物件,將其各個欄位(如文件 ID、標題、安全級別、所有者、發文單位、發文時間、是否已傳送、抄送單位和檔案地址等)提取出來,放入一個字典中,組成一個文件列表。
使用 JsonResponse 將文件列表以 JSON 格式傳送給前端。

get_user_logs邏輯
def get_user_logs(request):
    current_user = request.session.get('username')
    user = UserProfile.objects.get(username_up=current_user)
    print(user.username_up)

    if user.access_level == 1:  # 管理員許可權
        user_logs = Log.objects.filter(documentname="無")  # 獲取所有使用者日誌
        user_logs_data = list(user_logs.values())  # 轉換為字典列表
        print(user_logs_data)
        return JsonResponse(user_logs_data, safe=False)

    if user.access_level != 1:
        user_logs = Log.objects.filter(username=user.username_up, documentname="無")
        user_logs_data = list(user_logs.values())  # 轉換為字典列表
        print(user_logs_data)
        return JsonResponse(user_logs_data, safe=False)

從請求中獲取當前使用者的使用者名稱(username),這裡使用了 request.session.get('username') 方法。
使用當前使用者的使用者名稱從 UserProfile 模型中獲取使用者的詳細資訊,包括使用者名稱和訪問級別。
如果使用者的訪問級別為管理員(access_level == 1),則獲取所有使用者的日誌資訊,即過濾條件為 documentname="無" 的所有日誌。
如果使用者的訪問級別不是管理員,則只獲取該使用者的日誌資訊,同樣是過濾條件為 documentname="無" 的日誌,但是加上了使用者名稱的過濾條件。
將查詢到的日誌資訊轉換為字典列表,並透過 JsonResponse 返回給前端。

get_document_logs邏輯
def get_document_logs(request):
    current_user = request.session.get('username')
    user = UserProfile.objects.get(username_up=current_user)

    if user.access_level == 1:  # 管理員許可權
        document_logs = Log.objects.exclude(documentname="無")  # 獲取所有公文操作日誌
        document_logs_data = list(document_logs.values())  # 轉換為字典列表
        print(document_logs_data)
        return JsonResponse(document_logs_data, safe=False)
    if user.access_level != 1:
        document_logs = Log.objects.exclude(documentname="無").filter(username=user.username_up)
        document_logs_data = list(document_logs.values())  # 轉換為字典列表
        print(document_logs_data)
        return JsonResponse(document_logs_data, safe=False)

從請求中獲取當前使用者的使用者名稱(username),這裡使用了 request.session.get('username') 方法。
使用當前使用者的使用者名稱從 UserProfile 模型中獲取使用者的詳細資訊,包括使用者名稱和訪問級別。
如果使用者的訪問級別為管理員(access_level == 1),則獲取所有的公文操作日誌資訊,即過濾條件為 documentname!="無" 的所有日誌。
如果使用者的訪問級別不是管理員,則只獲取該使用者相關的公文操作日誌資訊,同樣是過濾條件為 documentname!="無" 的日誌,但是加上了使用者名稱的過濾條件。
將查詢到的公文操作日誌資訊轉換為字典列表,並透過 JsonResponse 返回給前端。

相關文章