Replies: 4 comments 8 replies
-
Since this discussion was posted, I have been researching until now and have successfully combined the security of FastAPI with the convenience of NiceGUI. As a side joke, it seems that this could replace the authorization routine in NiceGUI Examples. This is implemented using a method similar to front-end and back-end separation: from datetime import datetime, timedelta, timezone
from typing import Annotated
import jwt
import json
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError
from passlib.context import CryptContext
from pydantic import BaseModel
from nicegui import ui, app
# 得到这样的字符串 / to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# 模拟存储的用户名和密码(实际应用中应该从数据库或其他安全存储中获取)
# Emulate the username and password stored in the database (in real applications, you should get them from a database or other secure storage)
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "[email protected]",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
# FastAPI 鉴权模型
# FastAPI authentication model
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
class UserInDB(User):
hashed_password: str
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/token")
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except InvalidTokenError:
raise credentials_exception
user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)],
):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
# FastAPI 登录路由 / FastAPI login route
@app.post("/api/token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
) -> Token:
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="bearer")
# FastAPI 获取个人信息路由 / FastAPI get personal information route
@app.get("/api/profile")
async def profile(
current_user: Annotated[User, Depends(get_current_active_user)],
):
"""
需要鉴权的路由示例
:param credentials: FastAPI 提供的用于解析HTTP Basic Auth头部的对象
:return: 成功时返回鉴权成功的消息,否则返回鉴权失败的HTTP错误
"""
return current_user
# NiceGUI 获取个人信息路由 / NiceGUI get personal information route
@ui.page("/profile")
async def profile():
"""
需要鉴权的路由示例
:param credentials: FastAPI 提供的用于解析HTTP Basic Auth头部的对象
:return: 成功时返回鉴权成功的消息,否则返回鉴权失败的HTTP错误
"""
ui.add_head_html("""
<script>
async function getProfile() {
const accessToken = localStorage.getItem('access_token');
if (!accessToken) {
return {'status': 'failed', 'detail': 'Access token not found'};
}
const url = '/api/profile';
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch profile');
}
const profile = await response.json();
return {'status': 'success', 'profile': profile};
} catch (error) {
return {'status': 'failed', 'detail': error.message};
}
}
function logout() {
localStorage.removeItem('access_token');
window.location.reload();
}
</script>
""")
await ui.context.client.connected()
result = await ui.run_javascript("getProfile()")
if result['status'] == 'success':
ui.label(f"Profile: {json.dumps(result['profile'])}")
ui.button("Logout", on_click=lambda: ui.run_javascript("logout()"))
else:
ui.label("Failed to fetch profile. Please login first.")
ui.button("Login", on_click=lambda: ui.navigate.to("/login"))
# 登录界面路由 / Login page route
@ui.page("/login")
async def login_page():
"""
登录界面路由示例
:return: 一个登录页面
"""
ui.add_head_html("""
<script>
async function login(username, password) {
const url = '/api/token';
const data = new URLSearchParams();
data.append('username', username);
data.append('password', password);
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
});
if (!response.ok) {
throw new Error('Invalid username or password');
}
const result = await response.json();
// 处理登录成功后的数据,返回access_token
localStorage.setItem('access_token', result.access_token);
return {'status': 'success'};
} catch (error) {
return {'status': 'failed', 'detail': error.message};
}
}
</script>
""")
async def try_login():
username = usrname.value
password = pwd.value
if username and password:
result = await ui.run_javascript(f"login('{username}', '{password}')")
if result['status'] == 'success':
ui.navigate.to("/profile")
else:
ui.notify("Login failed. Please try again.", type="negative")
usrname = ui.input("username")
pwd = ui.input("password", password=True)
ui.button("Login", on_click=try_login)
# 公开路由 / Public page route
@ui.page("/")
async def public_data():
"""
公开路由示例
:return: 一个公开的消息,无需鉴权
"""
ui.label("This is a public page.")
if __name__ in {"__main__", "__mp_main__"}:
ui.run(uvicorn_logging_level='debug') Unfortunately, even after passing the parameter
This indicates that the password is still not secure and further research is needed. |
Beta Was this translation helpful? Give feedback.
-
Using FastAPI to output a static login page seems like a temporary workaround. I asked ChatGPT to generate a MDUI3-based login page, which solved most of the problems, but the login interface was not based on NiceGUI, which left me with a strange feeling. The code is as follows: from datetime import datetime, timedelta, timezone
...
from fastapi.responses import HTMLResponse
from nicegui import ui, app
...
# 登录界面路由 / Login page route
@app.get("/login")
async def login_page():
"""
登录界面路由示例
:return: 一个登录页面
"""
login_page = """
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no"/>
<meta name="renderer" content="webkit"/>
<link rel="stylesheet" href="https://unpkg.com/mdui@2/mdui.css">
<script src="https://unpkg.com/mdui@2/mdui.global.js"></script>
<!-- 如果使用了组件的 icon 属性,还需要引入图标的 CSS 文件 -->
<title>Login</title>
</head>
<body class="mdui-theme-primary-blue mdui-theme-accent-blue">
<!-- 登录表单 -->
<div class="mdui-container" style="margin-top: 50px;">
<div class="mdui-row">
<div class="mdui-col-xs-12 mdui-col-md-4 mdui-center">
<div class="mdui-card mdui-shadow-2">
<div class="mdui-card-header">
<span class="mdui-card-header-title">Login</span>
</div>
<div class="mdui-card-content">
<mdui-text-field id="username" label="Username" type="text" required></mdui-text-field>
<mdui-text-field id="password" label="Password" type="password" required></mdui-text-field>
</div>
<div class="mdui-card-actions">
<mdui-button id="login-btn" mdui-color="primary">Login</mdui-button>
</div>
</div>
</div>
</div>
</div>
<script>
// 页面加载时检查用户是否已登录
window.onload = function() {
const accessToken = localStorage.getItem('access_token');
if (accessToken) {
// 如果存在有效的 token,提示并自动跳转
mdui.snackbar({
message: '您已登录,正在跳转...',
position: 'top'
});
// 1秒后跳转到主页
setTimeout(() => {
window.location.href = '/profile'; // 替换成你希望跳转的页面
}, 1000);
}
};
// 登录按钮点击事件
document.getElementById('login-btn').addEventListener('click', async function () {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (!username || !password) {
// 用户名或密码为空,弹出提示
mdui.snackbar({
message: '用户名或密码不能为空!',
position: 'top'
});
return;
}
const url = '/api/token'; // 替换成你的 FastAPI 后端地址
const data = new URLSearchParams();
data.append('username', username);
data.append('password', password);
try {
// 发送 POST 请求到 FastAPI 后端
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: data,
});
if (!response.ok) {
throw new Error('登录失败:用户名或密码错误');
}
const result = await response.json();
// 登录成功后存储 token
localStorage.setItem('access_token', result.access_token);
// 弹出提示并跳转到主页或用户信息页面
mdui.snackbar({
message: '登录成功!',
position: 'top'
});
// 假设登录成功后跳转到主页
setTimeout(() => {
window.location.href = '/profile'; // 替换成你希望跳转的页面
}, 1000);
} catch (error) {
// 登录失败,弹出提示
mdui.snackbar({
message: error.message,
position: 'top'
});
}
});
</script>
</body>
</html>
"""
return HTMLResponse(content=login_page)
...
if __name__ in {"__main__", "__mp_main__"}:
ui.run(uvicorn_logging_level='debug') I am still not satisfied with not using NiceGUI to fulfill this requirement, and I will continue to explore a better solution to this problem. |
Beta Was this translation helpful? Give feedback.
-
There seems to be a small Bug here: If the client has an incorrect access_token, the /profile page displays "Failed to fetch profile. Please login first." The front end recognizes that the client has an access_token and jumps to /profile. Perhaps add a validation to the /profile, or to the interface that requires authorization, and ask the client to remove the access_token if it is invalid. Modify the Javascript code for the /profile route: async function getProfile() {
...
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch profile');
}
const profile = await response.json();
return {'status': 'success', 'profile': profile};
} catch (error) {
try {
localStorage.removeItem('access_token');
} catch (e) {
console.error(e);
}
return {'status': 'failed', 'detail': error.message};
}
}
...
} Then the problem can be solved. |
Beta Was this translation helpful? Give feedback.
-
Isn't the problem described here similar to HTTP Authentication?
Yes, NiceGUI framework currently sends the credentials even not encoded, but just use HTTPS and the problem will be solved... |
Beta Was this translation helpful? Give feedback.
-
Question
Description
When I was writing the program, I encountered some problems and tried to solve them with some methods:
I have a program that requires user authentication, so I directly referred to NiceGUI Official Examples - Authentication as the implementation of my application.
My login page program is roughly as follows:
At this time, I randomly enter an account: Account: [email protected] Password: 12345678. At this point, the terminal will directly display the content entered by the user, and it is the original text:
I think this is a serious security risk, which is equivalent to saving the user's password in plain text in the database. But this is after all a demonstration program, so I began to explore how to better optimize/fix this problem:
ui.html('<input type="text" name="username">')
andui.html('<input type="password" name="password">')
, but neither of them worked: I couldn't type anything in. I opened the browser's developer tools and it seemed like they were being recreated. I also noticed that they were wrapped in a<div>
tag.ui.add_body_html()
method, but it was clearly incorrect.ui.add_head_html()
method can be used to add JavaScript methods in the header, and then hash the password on the client side and send it to the server? But since I couldn't create a normal input box in1.
, this seems very difficult to me, so I'll continue to explore other methods.It seems to work normally, but it doesn't look very nice. Because this is a login box outside the browser. In fact, this code is FastAPI Official Documents - HTTP Basic Auth. I will continue to research this issue.
Beta Was this translation helpful? Give feedback.
All reactions