Authentication¶
GreenMap sử dụng JWT (JSON Web Token) cho xác thực và phân quyền.
Luồng Xác Thực¶
sequenceDiagram
participant Client
participant API
participant DB
Client->>API: POST /login (email, password)
API->>DB: Verify credentials
DB-->>API: User data
API->>API: Generate JWT
API-->>Client: { access_token, token_type }
Note over Client: Store token
Client->>API: GET /users/me (Authorization: Bearer token)
API->>API: Verify JWT
API->>DB: Get user data
DB-->>API: User data
API-->>Client: User profile
JWT Token¶
Structure¶
Token JWT bao gồm 3 phần:
Ví dụ Payload:
{
"sub": "1",
"email": "admin@greenmap.hanoi",
"role": "admin",
"exp": 1702224600,
"iat": 1702222800
}
Token Expiration¶
- Access Token: 30 phút (có thể cấu hình)
- Sau khi hết hạn, cần đăng nhập lại
Endpoints¶
Login¶
POST /login
Content-Type: application/x-www-form-urlencoded
username=admin@greenmap.hanoi&password=yourpassword
Success Response (200):
Error Response (401):
Protected Endpoints¶
Thêm header Authorization với prefix Bearer:
Roles & Permissions¶
Roles¶
| Role | Mô tả |
|---|---|
admin |
Toàn quyền quản trị hệ thống |
citizen |
Người dùng thông thường |
Permissions Matrix¶
| Resource | Action | Admin | Citizen |
|---|---|---|---|
| Users | List | ✅ | ❌ |
| Users | Create | ✅ | ❌ |
| Reports | Create | ✅ | ✅ |
| Reports | List Own | ✅ | ✅ |
| Reports | List All | ✅ | ❌ |
| Reports | Update Status | ✅ | ❌ |
| Locations | List | ✅ | ✅ |
| Locations | Create/Update/Delete | ✅ | ❌ |
Implementation¶
Backend (FastAPI)¶
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(status_code=401, detail="Invalid token")
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
user = await get_user(user_id)
if user is None:
raise HTTPException(status_code=401, detail="User not found")
return user
async def require_admin(current_user = Depends(get_current_user)):
if current_user.role != "admin":
raise HTTPException(status_code=403, detail="Admin access required")
return current_user
Frontend (React)¶
// Store token
const login = async (email, password) => {
const response = await api.post('/login',
new URLSearchParams({ username: email, password })
);
localStorage.setItem('token', response.data.access_token);
};
// Axios interceptor
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Handle 401
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
Mobile (Kotlin)¶
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = dataStore.getToken()
val request = if (token != null) {
chain.request().newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
} else {
chain.request()
}
return chain.proceed(request)
}
}
Security Best Practices¶
Lưu ý bảo mật
- ❌ Không lưu token trong code
- ❌ Không log token ra console
- ✅ Sử dụng HTTPS trong production
- ✅ Rotate SECRET_KEY định kỳ
- ✅ Implement token refresh (nếu cần)