feat: add user role menu management api

This commit is contained in:
yangshangbo
2025-07-16 18:09:52 +08:00
parent 441a8f114d
commit 38a55c41ab
5 changed files with 1569 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
# 用户角色菜单管理系统
基于 v-hono 框架的用户认证和权限管理系统,提供完整的用户角色管理和动态菜单功能。
## 功能特性
### 🔐 用户认证
- 用户注册和登录
- JWT 令牌认证
- 密码加密存储
- 会话管理
### 👥 角色管理
- 多级角色系统(admin、manager、user、guest)
- 基于角色的权限控制
- 灵活的权限分配
### 📋 菜单管理
- 动态菜单系统
- 树形菜单结构
- 基于权限的菜单显示
- 菜单排序和图标支持
### 🛡️ 安全特性
- 密码 SHA256 加密
- 令牌过期机制
- 中间件权限验证
- 防止路径遍历攻击
## 系统架构
```
v-hono/
├── hono/
│ ├── auth.v # 认证核心模块
│ ├── auth_routes.v # 认证路由
│ ├── database.v # 数据库管理
│ ├── request.v # 请求处理
│ └── app.v # 应用框架
├── auth_example.v # 完整示例
└── README_AUTH_SYSTEM.md # 本文档
```
## 快速开始
### 1. 编译和运行
```bash
# 编译示例应用
v auth_example.v
# 运行应用
./auth_example
```
### 2. 访问系统
打开浏览器访问:`http://localhost:3000`
### 3. 默认用户
系统预创建了以下示例用户:
| 用户名 | 密码 | 角色 | 权限 |
|--------|------|------|------|
| admin | admin123 | admin | 所有权限 |
| manager | manager123 | manager | 读写管理权限 |
| user | user123 | user | 读写权限 |
| guest | guest123 | guest | 只读权限 |
## API 接口
### 认证接口
#### 用户注册
```http
POST /api/auth/register
Content-Type: application/json
{
"username": "newuser",
"email": "user@example.com",
"password": "password123",
"role": "user"
}
```
#### 用户登录
```http
POST /api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "admin123"
}
```
响应:
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": 1640995200
}
```
#### 获取用户信息
```http
GET /api/auth/profile
Authorization: <token>
```
#### 用户注销
```http
POST /api/auth/logout
Authorization: <token>
```
### 菜单接口
#### 获取用户菜单
```http
GET /api/auth/menus
Authorization: <token>
```
#### 获取所有菜单(管理员)
```http
GET /api/auth/menus/all
Authorization: <token>
```
#### 创建菜单项(管理员)
```http
POST /api/auth/menus
Authorization: <token>
Content-Type: application/json
{
"name": "新菜单",
"path": "/new-menu",
"icon": "📄",
"parent_id": 0,
"sort_order": 1,
"permissions": ["read", "write"]
}
```
### 受保护的路由
#### 用户路由
```http
GET /api/protected/user
Authorization: <token>
```
#### 管理员路由
```http
GET /api/protected/admin
Authorization: <token>
```
## 角色权限说明
### Admin(超级管理员)
- 拥有所有权限
- 可以管理所有用户和菜单
- 可以访问所有系统功能
### Manager(管理员)
- 拥有读写和管理权限
- 可以管理用户和菜单
- 可以访问大部分系统功能
### User(普通用户)
- 拥有读写权限
- 可以查看和编辑内容
- 无法管理系统设置
### Guest(访客)
- 只有只读权限
- 只能查看公开内容
- 无法进行任何修改操作
## 菜单权限配置
菜单项通过 `permissions` 字段控制访问权限:
```json
{
"name": "用户管理",
"path": "/users",
"icon": "👥",
"permissions": ["read", "write", "manage"]
}
```
权限说明:
- `read`: 只读权限
- `write`: 读写权限
- `manage`: 管理权限
- `*`: 所有权限(仅管理员)
## 中间件使用
### 认证中间件
```v
// 添加认证中间件
app.use(hono.auth_middleware(auth_manager))
```
### 权限中间件
```v
// 添加权限检查中间件
app.use(hono.permission_middleware('manage'))
```
## 数据库结构
### users 表
```sql
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
status BOOLEAN DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
```
### menu_items 表
```sql
CREATE TABLE menu_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
path TEXT NOT NULL,
icon TEXT,
parent_id INTEGER DEFAULT 0,
sort_order INTEGER DEFAULT 0,
permissions TEXT,
status BOOLEAN DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
```
### user_sessions 表
```sql
CREATE TABLE user_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token TEXT UNIQUE NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
);
```
## 开发指南
### 添加新的认证功能
1. 在 `auth.v` 中添加新的方法
2. 在 `auth_routes.v` 中添加对应的路由
3. 更新前端界面
### 自定义权限
1. 修改 `UserRole` 枚举
2. 更新 `check_permission` 方法
3. 调整菜单权限配置
### 扩展菜单功能
1. 添加菜单编辑和删除接口
2. 实现菜单拖拽排序
3. 添加菜单缓存机制
## 安全建议
1. **密码安全**
- 使用强密码策略
- 定期更换密码
- 启用密码复杂度检查
2. **令牌安全**
- 设置合理的过期时间
- 实现令牌刷新机制
- 支持令牌撤销
3. **权限控制**
- 最小权限原则
- 定期审查权限
- 记录权限变更日志
4. **数据安全**
- 数据库加密
- 定期备份
- 访问日志记录
## 故障排除
### 常见问题
1. **数据库连接失败**
- 检查数据库文件权限
- 确保 SQLite 支持
- 验证数据库路径
2. **认证失败**
- 检查用户名密码
- 验证令牌格式
- 确认令牌未过期
3. **权限不足**
- 检查用户角色
- 验证菜单权限配置
- 确认中间件配置
### 调试模式
启用调试输出:
```v
// 在代码中添加调试信息
println('Debug: $debug_info')
```
## 性能优化
1. **数据库优化**
- 添加适当的索引
- 使用连接池
- 优化查询语句
2. **缓存策略**
- 菜单缓存
- 用户信息缓存
- 权限缓存
3. **并发处理**
- 使用连接池
- 实现请求队列
- 优化锁机制
## 贡献指南
1. Fork 项目
2. 创建功能分支
3. 提交更改
4. 创建 Pull Request
## 许可证
MIT License
## 联系方式
如有问题或建议,请提交 Issue 或联系开发团队。
+145
View File
@@ -0,0 +1,145 @@
module main
import hono
import net.http
import json
fn main() {
// 创建数据库管理器
db_manager := hono.new_database_manager('auth_system.db') or {
eprintln('Failed to create database manager: $err')
return
}
// 创建认证管理器
mut auth_manager := hono.new_auth_manager(db_manager)
// 初始化认证相关表
auth_manager.init_tables() or {
eprintln('Failed to initialize auth tables: $err')
return
}
// 创建 Hono 应用
mut app := hono.Hono{}
// 注册认证路由
hono.register_auth_routes(mut app, mut auth_manager)
// 添加一些示例菜单项
create_sample_menus(mut auth_manager)
// 添加一些示例用户
create_sample_users(mut auth_manager)
// 添加一些受保护的路由示例
app.get('/api/protected/user', fn [auth_manager] (mut c hono.Context) http.Response {
token := c.req.header.get_custom('Authorization') or { '' }
user := auth_manager.verify_token(token) or {
c.status(401)
return c.json(json.encode({
'error': 'User not found'
}))
}
return c.json(json.encode({
'message': 'This is a protected user route',
'user_id': user.id.str(),
'username': user.username,
'email': user.email,
'role': user.role.str()
}))
})
app.get('/api/protected/admin', fn [auth_manager] (mut c hono.Context) http.Response {
token := c.req.header.get_custom('Authorization') or { '' }
user := auth_manager.verify_token(token) or {
c.status(401)
return c.json(json.encode({
'error': 'User not found'
}))
}
if !auth_manager.check_permission(user, 'manage') {
c.status(403)
return c.json(json.encode({
'error': 'Admin access required'
}))
}
return c.json(json.encode({
'message': 'This is a protected admin route',
'user_id': user.id.str(),
'username': user.username,
'email': user.email,
'role': user.role.str()
}))
})
// 添加静态文件服务
app.get('/', fn (mut c hono.Context) http.Response {
return c.file('public/auth.html')
})
println('🚀 用户角色菜单管理系统已启动')
println('📱 访问地址: http://localhost:3000')
println('')
println('📋 示例用户:')
println(' 用户名: admin, 密码: admin123, 角色: admin')
println(' 用户名: manager, 密码: manager123, 角色: manager')
println(' 用户名: user, 密码: user123, 角色: user')
println(' 用户名: guest, 密码: guest123, 角色: guest')
println('')
// 启动服务器
app.listen(':3000')
}
// 创建示例菜单
fn create_sample_menus(mut auth_manager hono.AuthManager) {
// 创建根菜单
auth_manager.create_menu_item('仪表板', '/dashboard', '📊', 0, 1, ['read']) or { println('仪表板插入失败') }
auth_manager.create_menu_item('用户管理', '/users', '👥', 0, 2, ['read', 'write', 'manage']) or { println('用户管理插入失败') }
auth_manager.create_menu_item('系统设置', '/settings', '⚙️', 0, 3, ['manage']) or { println('系统设置插入失败') }
auth_manager.create_menu_item('文件管理', '/files', '📁', 0, 4, ['read', 'write']) or { println('文件管理插入失败') }
// 获取用户管理菜单ID(动态获取)
user_menu_id := auth_manager.get_menu_id_by_path('/users')
// 创建子菜单
auth_manager.create_menu_item('用户列表', '/users/list', '📋', user_menu_id, 1, ['read']) or { println('用户列表插入失败') }
auth_manager.create_menu_item('添加用户', '/users/add', '➕', user_menu_id, 2, ['write']) or { println('添加用户插入失败') }
auth_manager.create_menu_item('编辑用户', '/users/edit', '✏️', user_menu_id, 3, ['write']) or { println('编辑用户插入失败') }
auth_manager.create_menu_item('删除用户', '/users/delete', '🗑️', user_menu_id, 4, ['manage']) or { println('删除用户插入失败') }
// 更多测试菜单
auth_manager.create_menu_item('报表中心', '/reports', '📈', 0, 5, ['read', 'write']) or { println('报表中心插入失败') }
reports_menu_id := auth_manager.get_menu_id_by_path('/reports')
auth_manager.create_menu_item('销售报表', '/reports/sales', '💹', reports_menu_id, 1, ['read']) or { println('销售报表插入失败') }
auth_manager.create_menu_item('财务报表', '/reports/finance', '💰', reports_menu_id, 2, ['read']) or { println('财务报表插入失败') }
auth_manager.create_menu_item('消息中心', '/messages', '📨', 0, 6, ['read']) or { println('消息中心插入失败') }
auth_manager.create_menu_item('个人中心', '/profile', '👤', 0, 7, ['read', 'write']) or { println('个人中心插入失败') }
auth_manager.create_menu_item('测试菜单A', '/test/a', '🅰️', 0, 8, ['read']) or { println('测试菜单A插入失败') }
auth_manager.create_menu_item('测试菜单B', '/test/b', '🅱️', 0, 9, ['read']) or { println('测试菜单B插入失败') }
auth_manager.create_menu_item('测试菜单C', '/test/c', '🆑', 0, 10, ['read']) or { println('测试菜单C插入失败') }
test_a_id := auth_manager.get_menu_id_by_path('/test/a')
auth_manager.create_menu_item('A-子菜单1', '/test/a/1', '1️⃣', test_a_id, 1, ['read']) or { println('A-子菜单1插入失败') }
auth_manager.create_menu_item('A-子菜单2', '/test/a/2', '2️⃣', test_a_id, 2, ['read']) or { println('A-子菜单2插入失败') }
println('✅ 示例菜单已创建')
println('✅ 更多测试菜单已创建')
}
// 创建示例用户
fn create_sample_users(mut auth_manager hono.AuthManager) {
// 创建管理员用户
auth_manager.create_user('admin', 'admin@example.com', 'admin123', hono.UserRole.admin) or { return }
// 创建经理用户
auth_manager.create_user('manager', 'manager@example.com', 'manager123', hono.UserRole.manager) or { return }
// 创建普通用户
auth_manager.create_user('user', 'user@example.com', 'user123', hono.UserRole.user) or { return }
// 创建访客用户
auth_manager.create_user('guest', 'guest@example.com', 'guest123', hono.UserRole.guest) or { return }
println('✅ 示例用户已创建')
}
+404
View File
@@ -0,0 +1,404 @@
module hono
import crypto.sha256
import crypto.rand
import time
import json
// 用户角色枚举
pub enum UserRole {
admin
manager
user
guest
}
// 用户结构
pub struct User {
pub:
id int
username string
email string
password_hash string
role UserRole
status bool // true: active, false: inactive
created_at int
updated_at int
}
// 菜单项结构
pub struct MenuItem {
pub:
id int
name string
path string
icon string
parent_id int
sort_order int
permissions []string
status bool
created_at int
updated_at int
mut:
children []MenuItem
}
// 用户会话结构
pub struct UserSession {
pub:
user_id int
token string
expires_at int
created_at int
}
// 认证管理器
pub struct AuthManager {
mut:
db DatabaseManager
}
// 创建认证管理器
pub fn new_auth_manager(db DatabaseManager) AuthManager {
return AuthManager{
db: db
}
}
// 初始化认证相关表
pub fn (mut auth AuthManager) init_tables() ! {
// 创建用户表
auth.db.db.exec('CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
status BOOLEAN DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);') or {
return error('Failed to create users table: $err')
}
// 创建菜单表
auth.db.db.exec('CREATE TABLE IF NOT EXISTS menu_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
path TEXT NOT NULL,
icon TEXT,
parent_id INTEGER DEFAULT 0,
sort_order INTEGER DEFAULT 0,
permissions TEXT,
status BOOLEAN DEFAULT 1,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);') or {
return error('Failed to create menu_items table: $err')
}
// 创建用户会话表
auth.db.db.exec('CREATE TABLE IF NOT EXISTS user_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token TEXT UNIQUE NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
FOREIGN KEY (user_id) REFERENCES users (id)
);') or {
return error('Failed to create user_sessions table: $err')
}
// 创建索引
auth.db.db.exec('CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);') or {
return error('Failed to create index: $err')
}
auth.db.db.exec('CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);') or {
return error('Failed to create index: $err')
}
auth.db.db.exec('CREATE INDEX IF NOT EXISTS idx_menu_parent ON menu_items(parent_id);') or {
return error('Failed to create index: $err')
}
auth.db.db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_token ON user_sessions(token);') or {
return error('Failed to create index: $err')
}
}
// 密码哈希
fn hash_password(password string) string {
return sha256.sum(password.bytes()).hex()
}
// 生成随机令牌
fn generate_token() string {
random_bytes := rand.bytes(32) or { return '' }
mut token := ''
for byte in random_bytes {
token += '${byte:02x}'
}
return token
}
// 创建用户
pub fn (mut auth AuthManager) create_user(username string, email string, password string, role UserRole) !User {
now := int(time.now().unix())
password_hash := hash_password(password)
role_str := role.str()
auth.db.db.exec('INSERT INTO users (username, email, password_hash, role, status, created_at, updated_at) VALUES ("$username", "$email", "$password_hash", "$role_str", 1, $now, $now)') or {
return error('Failed to create user: $err')
}
// 获取插入的用户ID
rows := auth.db.db.exec('SELECT last_insert_rowid()') or {
return error('Failed to get user ID: $err')
}
user_id := rows[0].vals[0].int()
return User{
id: user_id
username: username
email: email
password_hash: password_hash
role: role
status: true
created_at: now
updated_at: now
}
}
// 用户登录
pub fn (mut auth AuthManager) login(username string, password string) !UserSession {
password_hash := hash_password(password)
rows := auth.db.db.exec('SELECT id, username, email, password_hash, role, status FROM users WHERE username = "$username" AND password_hash = "$password_hash" AND status = 1') or {
return error('Failed to query user: $err')
}
if rows.len == 0 {
return error('Invalid username or password')
}
user := rows[0]
user_id := user.vals[0].int()
// 生成会话令牌
token := generate_token()
now := int(time.now().unix())
expires_at := now + (24 * 60 * 60) // 24小时后过期
// 保存会话
auth.db.db.exec('INSERT INTO user_sessions (user_id, token, expires_at, created_at) VALUES ($user_id, "$token", $expires_at, $now)') or {
return error('Failed to create session: $err')
}
return UserSession{
user_id: user_id
token: token
expires_at: expires_at
created_at: now
}
}
// 验证令牌
pub fn (auth AuthManager) verify_token(token string) !User {
rows := auth.db.db.exec('SELECT u.id, u.username, u.email, u.password_hash, u.role, u.status FROM users u JOIN user_sessions s ON u.id = s.user_id WHERE s.token = "$token" AND s.expires_at > ${int(time.now().unix())} AND u.status = 1') or {
return error('Failed to verify token: $err')
}
if rows.len == 0 {
return error('Invalid or expired token')
}
user := rows[0]
role_str := user.vals[4]
role := match role_str {
'admin' { UserRole.admin }
'manager' { UserRole.manager }
'user' { UserRole.user }
'guest' { UserRole.guest }
else { UserRole.guest }
}
return User{
id: user.vals[0].int()
username: user.vals[1]
email: user.vals[2]
password_hash: user.vals[3]
role: role
status: user.vals[5].int() == 1
created_at: 0
updated_at: 0
}
}
// 注销会话
pub fn (mut auth AuthManager) logout(token string) ! {
auth.db.db.exec('DELETE FROM user_sessions WHERE token = "$token"') or {
return error('Failed to logout: $err')
}
}
// 创建菜单项
pub fn (mut auth AuthManager) create_menu_item(name string, path string, icon string, parent_id int, sort_order int, permissions []string) !MenuItem {
now := int(time.now().unix())
permissions_json := json.encode(permissions)
permissions_sql := permissions_json.replace('"', "''")
auth.db.db.exec('INSERT INTO menu_items (name, path, icon, parent_id, sort_order, permissions, status, created_at, updated_at) VALUES ("$name", "$path", "$icon", $parent_id, $sort_order, "$permissions_sql", 1, $now, $now)')
or {
eprintln('菜单插入SQL错误: $err')
return error('Failed to create menu item: $err')
}
// 获取插入的菜单ID
rows := auth.db.db.exec('SELECT last_insert_rowid()') or {
return error('Failed to get menu ID: $err')
}
menu_id := rows[0].vals[0].int()
return MenuItem{
id: menu_id
name: name
path: path
icon: icon
parent_id: parent_id
sort_order: sort_order
permissions: permissions
status: true
created_at: now
updated_at: now
}
}
// 获取所有菜单项
pub fn (auth AuthManager) get_all_menu_items() ![]MenuItem {
rows := auth.db.db.exec('SELECT id, name, path, icon, parent_id, sort_order, permissions, status FROM menu_items ORDER BY parent_id, sort_order') or {
return error('Failed to query all menu items: $err')
}
mut menus := []MenuItem{}
for row in rows {
permissions_str := row.vals[6]
permissions := json.decode([]string, permissions_str) or { []string{} }
menus << MenuItem{
id: row.vals[0].int()
name: row.vals[1]
path: row.vals[2]
icon: row.vals[3]
parent_id: row.vals[4].int()
sort_order: row.vals[5].int()
permissions: permissions
status: row.vals[7].int() == 1
created_at: 0
updated_at: 0
}
}
return menus
}
// 获取用户菜单
pub fn (auth AuthManager) get_user_menus(user User) ![]MenuItem {
// 根据用户角色获取菜单
role_permissions := match user.role {
.admin { ['*'] } // 管理员拥有所有权限
.manager { ['read', 'write', 'manage'] }
.user { ['read', 'write'] }
.guest { ['read'] }
}
// 构建权限查询条件
mut permission_conditions := []string{}
for permission in role_permissions {
if permission == '*' {
permission_conditions << 'permissions LIKE "%*%"'
} else {
permission_conditions << 'permissions LIKE "%$permission%"'
}
}
permission_where := permission_conditions.join(' OR ')
rows := auth.db.db.exec('SELECT id, name, path, icon, parent_id, sort_order, permissions, status FROM menu_items WHERE status = 1 AND ($permission_where) ORDER BY parent_id, sort_order') or {
return error('Failed to query menu items: $err')
}
mut menus := []MenuItem{}
for row in rows {
permissions_str := row.vals[6]
permissions := json.decode([]string, permissions_str) or { []string{} }
menus << MenuItem{
id: row.vals[0].int()
name: row.vals[1]
path: row.vals[2]
icon: row.vals[3]
parent_id: row.vals[4].int()
sort_order: row.vals[5].int()
permissions: permissions
status: row.vals[7].int() == 1
created_at: 0
updated_at: 0
}
}
return menus
}
// 构建菜单树
pub fn (auth AuthManager) build_menu_tree(menus []MenuItem) []MenuItem {
mut menu_map := map[int]MenuItem{}
mut root_menus := []MenuItem{}
// 创建菜单映射
for menu in menus {
menu_map[menu.id] = menu
}
// 构建树结构
for menu in menus {
if menu.parent_id == 0 {
// 根菜单
mut root_menu := menu
root_menu.children = auth.get_children(menu.id, menu_map)
root_menus << root_menu
}
}
return root_menus
}
// 获取子菜单
fn (auth AuthManager) get_children(parent_id int, menu_map map[int]MenuItem) []MenuItem {
mut children := []MenuItem{}
for _, menu in menu_map {
if menu.parent_id == parent_id {
mut child := menu
child.children = auth.get_children(menu.id, menu_map)
children << child
}
}
return children
}
// 检查用户权限
pub fn (auth AuthManager) check_permission(user User, required_permission string) bool {
return match user.role {
.admin { true } // 管理员拥有所有权限
.manager { required_permission in ['read', 'write', 'manage'] }
.user { required_permission in ['read', 'write'] }
.guest { required_permission == 'read' }
}
}
// 获取菜单id(通过path)
pub fn (auth AuthManager) get_menu_id_by_path(path string) int {
rows := auth.db.db.exec('SELECT id FROM menu_items WHERE path = "$path"') or { return 0 }
if rows.len > 0 {
return rows[0].vals[0].int()
}
return 0
}
+223
View File
@@ -0,0 +1,223 @@
module hono
import json
import net.http
// 登录请求结构
pub struct LoginRequest {
pub:
username string
password string
}
// 注册请求结构
pub struct RegisterRequest {
pub:
username string
email string
password string
role string
}
// 菜单创建请求结构
pub struct MenuCreateRequest {
pub:
name string
path string
icon string
parent_id int
sort_order int
permissions []string
}
// 认证中间件(只做校验,不注入 user)
pub fn auth_middleware(auth_manager AuthManager) ContextMiddleware {
return fn [auth_manager] (mut c Context, next fn (mut Context) http.Response) http.Response {
token := c.req.header.get_custom('Authorization') or { '' }
if token == '' {
c.status(401)
return c.json(json.encode({
'error': 'Authorization token required'
}))
}
_ := auth_manager.verify_token(token) or {
c.status(401)
return c.json(json.encode({
'error': 'Invalid or expired token'
}))
}
return next(mut c)
}
}
// 权限检查中间件(直接校验 token 权限)
pub fn permission_middleware(auth_manager AuthManager, required_permission string) ContextMiddleware {
return fn [auth_manager, required_permission] (mut c Context, next fn (mut Context) http.Response) http.Response {
token := c.req.header.get_custom('Authorization') or { '' }
user := auth_manager.verify_token(token) or {
c.status(401)
return c.json(json.encode({
'error': 'Invalid or expired token'
}))
}
if !auth_manager.check_permission(user, required_permission) {
c.status(403)
return c.json(json.encode({
'error': 'Insufficient permissions'
}))
}
return next(mut c)
}
}
// 注册认证路由
pub fn register_auth_routes(mut app Hono, mut auth_manager AuthManager) {
// 登录路由
app.post('/api/auth/login', fn [mut auth_manager] (mut c Context) http.Response {
body := c.body
login_req := json.decode(LoginRequest, body) or {
c.status(400)
return c.json(json.encode({
'error': 'Invalid request body'
}))
}
session := auth_manager.login(login_req.username, login_req.password) or {
c.status(401)
return c.json(json.encode({
'error': err.msg()
}))
}
return c.json(json.encode({
'token': session.token.str()
'expires_at': session.expires_at.str()
}))
})
// 注册路由
app.post('/api/auth/register', fn [mut auth_manager] (mut c Context) http.Response {
body := c.body
register_req := json.decode(RegisterRequest, body) or {
c.status(400)
return c.json(json.encode({
'error': 'Invalid request body'
}))
}
role := match register_req.role {
'admin' { UserRole.admin }
'manager' { UserRole.manager }
'user' { UserRole.user }
'guest' { UserRole.guest }
else { UserRole.user }
}
user := auth_manager.create_user(register_req.username, register_req.email, register_req.password, role) or {
c.status(400)
return c.json(json.encode({
'error': err.msg()
}))
}
return c.json(json.encode({
'user_id': user.id.str()
'username': user.username
'email': user.email
'role': user.role.str()
}))
})
// 注销路由
app.post('/api/auth/logout', fn [mut auth_manager] (mut c Context) http.Response {
token := c.req.header.get_custom('Authorization') or { '' }
if token == '' {
c.status(401)
return c.json(json.encode({
'error': 'Authorization token required'
}))
}
auth_manager.logout(token) or {
c.status(500)
return c.json(json.encode({
'error': err.msg()
}))
}
return c.json(json.encode({
'message': 'Logged out successfully'
}))
})
// 获取用户信息路由
app.get('/api/auth/profile', fn [auth_manager] (mut c Context) http.Response {
token := c.req.header.get_custom('Authorization') or { '' }
user := auth_manager.verify_token(token) or {
c.status(401)
return c.json(json.encode({
'error': 'Invalid or expired token'
}))
}
return c.json(json.encode({
'user_id': user.id.str()
'username': user.username
'email': user.email
'role': user.role.str()
'active': user.status.str()
}))
})
// 获取用户菜单路由
app.get('/api/auth/menus', fn [auth_manager] (mut c Context) http.Response {
token := c.req.header.get_custom('Authorization') or { '' }
user := auth_manager.verify_token(token) or {
c.status(401)
return c.json(json.encode({
'error': 'Invalid or expired token'
}))
}
menus := auth_manager.get_user_menus(user) or {
c.status(500)
return c.json(json.encode({
'error': err.msg()
}))
}
return c.json(json.encode({
'menus': menus
}))
})
// 创建菜单项路由 (需要管理员权限)
app.post('/api/auth/menus', fn [mut auth_manager] (mut c Context) http.Response {
body := c.body
menu_req := json.decode(MenuCreateRequest, body) or {
c.status(400)
return c.json(json.encode({
'error': 'Invalid request body'
}))
}
menu := auth_manager.create_menu_item(menu_req.name, menu_req.path, menu_req.icon, menu_req.parent_id, menu_req.sort_order, menu_req.permissions) or {
c.status(400)
return c.json(json.encode({
'error': err.msg()
}))
}
return c.json(json.encode({
'menu_id': menu.id.str()
'name': menu.name
'path': menu.path
'icon': menu.icon
'parent_id': menu.parent_id.str()
'sort_order': menu.sort_order.str()
'permissions': json.encode(menu.permissions)
'active': menu.status.str()
}))
})
// 获取所有菜单项路由 (需要管理员权限)
app.get('/api/auth/menus/all', fn [auth_manager] (mut c Context) http.Response {
menus := auth_manager.get_all_menu_items() or {
c.status(500)
return c.json(json.encode({
'error': err.msg()
}))
}
return c.json(json.encode({
'menus': menus
}))
})
}
+436
View File
@@ -0,0 +1,436 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户角色菜单管理系统</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: center;
}
.content {
padding: 30px;
}
.section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #f9f9f9;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
input, select, button {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
cursor: pointer;
font-weight: bold;
margin-top: 10px;
}
button:hover {
opacity: 0.9;
}
.result {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
background: #f0f0f0;
border-left: 4px solid #667eea;
}
.menu-tree {
margin-top: 15px;
}
.menu-item {
padding: 8px;
margin: 2px 0;
background: white;
border-radius: 4px;
border-left: 3px solid #667eea;
}
.menu-item .icon {
margin-right: 8px;
color: #667eea;
}
.submenu {
margin-left: 20px;
}
.error {
background: #ffebee;
border-left-color: #f44336;
color: #c62828;
}
.success {
background: #e8f5e8;
border-left-color: #4caf50;
color: #2e7d32;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>用户角色菜单管理系统</h1>
<p>基于 v-hono 框架的用户认证和权限管理</p>
</div>
<div class="content">
<!-- 用户注册 -->
<div class="section">
<h2>用户注册</h2>
<div class="form-group">
<label for="reg-username">用户名:</label>
<input type="text" id="reg-username" placeholder="输入用户名">
</div>
<div class="form-group">
<label for="reg-email">邮箱:</label>
<input type="email" id="reg-email" placeholder="输入邮箱">
</div>
<div class="form-group">
<label for="reg-password">密码:</label>
<input type="password" id="reg-password" placeholder="输入密码">
</div>
<div class="form-group">
<label for="reg-role">角色:</label>
<select id="reg-role">
<option value="user">普通用户</option>
<option value="manager">管理员</option>
<option value="admin">超级管理员</option>
<option value="guest">访客</option>
</select>
</div>
<button onclick="register()">注册</button>
<div id="reg-result" class="result" style="display: none;"></div>
</div>
<!-- 用户登录 -->
<div class="section">
<h2>用户登录</h2>
<div class="form-group">
<label for="login-username">用户名:</label>
<input type="text" id="login-username" placeholder="输入用户名">
</div>
<div class="form-group">
<label for="login-password">密码:</label>
<input type="password" id="login-password" placeholder="输入密码">
</div>
<button onclick="login()">登录</button>
<div id="login-result" class="result" style="display: none;"></div>
</div>
<!-- 用户信息 -->
<div class="section">
<h2>用户信息</h2>
<button onclick="getProfile()">获取用户信息</button>
<div id="profile-result" class="result" style="display: none;"></div>
</div>
<!-- 菜单管理 -->
<div class="section">
<h2>菜单管理</h2>
<button onclick="getMenus()">获取用户菜单</button>
<button onclick="getAllMenus()">获取所有菜单</button>
<div id="menus-result" class="result" style="display: none;"></div>
</div>
<!-- 受保护的路由测试 -->
<div class="section">
<h2>权限测试</h2>
<button onclick="testUserRoute()">测试用户路由</button>
<button onclick="testAdminRoute()">测试管理员路由</button>
<div id="test-result" class="result" style="display: none;"></div>
</div>
<!-- 注销 -->
<div class="section">
<h2>注销</h2>
<button onclick="logout()">注销</button>
<div id="logout-result" class="result" style="display: none;"></div>
</div>
</div>
</div>
<script>
let authToken = '';
function showResult(elementId, message, isError = false) {
const element = document.getElementById(elementId);
element.textContent = message;
element.style.display = 'block';
element.className = 'result ' + (isError ? 'error' : 'success');
}
function hideResult(elementId) {
document.getElementById(elementId).style.display = 'none';
}
async function register() {
hideResult('reg-result');
const username = document.getElementById('reg-username').value;
const email = document.getElementById('reg-email').value;
const password = document.getElementById('reg-password').value;
const role = document.getElementById('reg-role').value;
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, email, password, role })
});
const data = await response.json();
if (response.ok) {
showResult('reg-result', '注册成功: ' + JSON.stringify(data, null, 2));
} else {
showResult('reg-result', '注册失败: ' + data.error, true);
}
} catch (error) {
showResult('reg-result', '请求失败: ' + error.message, true);
}
}
async function login() {
hideResult('login-result');
const username = document.getElementById('login-username').value;
const password = document.getElementById('login-password').value;
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
authToken = data.token;
showResult('login-result', '登录成功: ' + JSON.stringify(data, null, 2));
} else {
showResult('login-result', '登录失败: ' + data.error, true);
}
} catch (error) {
showResult('login-result', '请求失败: ' + error.message, true);
}
}
async function getProfile() {
hideResult('profile-result');
if (!authToken) {
showResult('profile-result', '请先登录', true);
return;
}
try {
const response = await fetch('/api/auth/profile', {
headers: {
'Authorization': authToken
}
});
const data = await response.json();
if (response.ok) {
showResult('profile-result', '用户信息: ' + JSON.stringify(data, null, 2));
} else {
showResult('profile-result', '获取失败: ' + data.error, true);
}
} catch (error) {
showResult('profile-result', '请求失败: ' + error.message, true);
}
}
async function getMenus() {
hideResult('menus-result');
if (!authToken) {
showResult('menus-result', '请先登录', true);
return;
}
try {
const response = await fetch('/api/auth/menus', {
headers: {
'Authorization': authToken
}
});
const data = await response.json();
if (response.ok) {
const menuHtml = renderMenuTree(data.menus);
showResult('menus-result', '用户菜单: ' + menuHtml);
} else {
showResult('menus-result', '获取失败: ' + data.error, true);
}
} catch (error) {
showResult('menus-result', '请求失败: ' + error.message, true);
}
}
async function getAllMenus() {
hideResult('menus-result');
if (!authToken) {
showResult('menus-result', '请先登录', true);
return;
}
try {
const response = await fetch('/api/auth/menus/all', {
headers: {
'Authorization': authToken
}
});
const data = await response.json();
if (response.ok) {
const menuHtml = renderMenuTree(data.menus);
showResult('menus-result', '所有菜单: ' + menuHtml);
} else {
showResult('menus-result', '获取失败: ' + data.error, true);
}
} catch (error) {
showResult('menus-result', '请求失败: ' + error.message, true);
}
}
function renderMenuTree(menus) {
if (!menus || menus.length === 0) {
return '<div class="menu-tree">暂无菜单</div>';
}
let html = '<div class="menu-tree">';
for (const menu of menus) {
html += renderMenuItem(menu);
}
html += '</div>';
return html;
}
function renderMenuItem(menu) {
let html = '<div class="menu-item">';
html += '<span class="icon">' + (menu.icon || '📄') + '</span>';
html += '<strong>' + menu.name + '</strong> (' + menu.path + ')';
if (menu.children && menu.children.length > 0) {
html += '<div class="submenu">';
for (const child of menu.children) {
html += renderMenuItem(child);
}
html += '</div>';
}
html += '</div>';
return html;
}
async function testUserRoute() {
hideResult('test-result');
if (!authToken) {
showResult('test-result', '请先登录', true);
return;
}
try {
const response = await fetch('/api/protected/user', {
headers: {
'Authorization': authToken
}
});
const data = await response.json();
if (response.ok) {
showResult('test-result', '用户路由测试成功: ' + JSON.stringify(data, null, 2));
} else {
showResult('test-result', '测试失败: ' + data.error, true);
}
} catch (error) {
showResult('test-result', '请求失败: ' + error.message, true);
}
}
async function testAdminRoute() {
hideResult('test-result');
if (!authToken) {
showResult('test-result', '请先登录', true);
return;
}
try {
const response = await fetch('/api/protected/admin', {
headers: {
'Authorization': authToken
}
});
const data = await response.json();
if (response.ok) {
showResult('test-result', '管理员路由测试成功: ' + JSON.stringify(data, null, 2));
} else {
showResult('test-result', '测试失败: ' + data.error, true);
}
} catch (error) {
showResult('test-result', '请求失败: ' + error.message, true);
}
}
async function logout() {
hideResult('logout-result');
if (!authToken) {
showResult('logout-result', '请先登录', true);
return;
}
try {
const response = await fetch('/api/auth/logout', {
method: 'POST',
headers: {
'Authorization': authToken
}
});
const data = await response.json();
if (response.ok) {
authToken = '';
showResult('logout-result', '注销成功: ' + data.message);
} else {
showResult('logout-result', '注销失败: ' + data.error, true);
}
} catch (error) {
showResult('logout-result', '请求失败: ' + error.message, true);
}
}
</script>
</body>
</html>