fix(P2): rand.Read错误处理 + JWT随机密钥 + 密码环境变量化 + 前端路由守卫
This commit is contained in:
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# feedsystem_video_go 环境变量模板
|
||||||
|
# 复制此文件为 .env 后修改实际值
|
||||||
|
# .env 已被 .gitignore 忽略,不会提交到仓库
|
||||||
|
|
||||||
|
# MySQL
|
||||||
|
MYSQL_ROOT_PASSWORD=123456
|
||||||
|
MYSQL_DATABASE=feedsystem
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_PASSWORD=123456
|
||||||
|
|
||||||
|
# RabbitMQ
|
||||||
|
RABBITMQ_USER=admin
|
||||||
|
RABBITMQ_PASS=password123
|
||||||
|
|
||||||
|
# JWT (生产环境务必修改为随机强密钥)
|
||||||
|
JWT_SECRET=change-me-in-production
|
||||||
@@ -2,7 +2,10 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -12,7 +15,13 @@ import (
|
|||||||
func jwtSecret() []byte {
|
func jwtSecret() []byte {
|
||||||
secret := os.Getenv("JWT_SECRET")
|
secret := os.Getenv("JWT_SECRET")
|
||||||
if secret == "" {
|
if secret == "" {
|
||||||
secret = "change-me-in-env"
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
|
log.Printf("FATAL: cannot generate JWT secret: %v", err)
|
||||||
|
return []byte("fallback-unsafe-key-change-me")
|
||||||
|
}
|
||||||
|
secret = hex.EncodeToString(b)
|
||||||
|
log.Printf("WARNING: JWT_SECRET not set, generated random key. All tokens invalid on restart.")
|
||||||
}
|
}
|
||||||
return []byte(secret)
|
return []byte(secret)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,12 @@ func (vh *VideoHandler) UploadVideo(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := randHex(16) + ext
|
filename, err := randHex(16)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate filename"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filename = filename + ext
|
||||||
absPath := filepath.Join(absDir, filename)
|
absPath := filepath.Join(absDir, filename)
|
||||||
|
|
||||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||||
@@ -145,7 +150,12 @@ func (vh *VideoHandler) UploadCover(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := randHex(16) + ext
|
filename, err := randHex(16)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate filename"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filename = filename + ext
|
||||||
absPath := filepath.Join(absDir, filename)
|
absPath := filepath.Join(absDir, filename)
|
||||||
|
|
||||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||||
@@ -161,10 +171,12 @@ func (vh *VideoHandler) UploadCover(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func randHex(n int) string {
|
func randHex(n int) (string, error) {
|
||||||
b := make([]byte, n)
|
b := make([]byte, n)
|
||||||
_, _ = rand.Read(b)
|
if _, err := rand.Read(b); err != nil {
|
||||||
return hex.EncodeToString(b)
|
return "", fmt.Errorf("rand.Read: %w", err)
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildAbsoluteURL(c *gin.Context, p string) string {
|
func buildAbsoluteURL(c *gin.Context, p string) string {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ services:
|
|||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: "123456"
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
|
||||||
MYSQL_DATABASE: "feedsystem"
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-feedsystem}
|
||||||
TZ: "Asia/Shanghai"
|
TZ: "Asia/Shanghai"
|
||||||
ports:
|
ports:
|
||||||
- "3307:3306"
|
- "3307:3306"
|
||||||
@@ -25,7 +25,7 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: always
|
restart: always
|
||||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "123456"]
|
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-123456}"]
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -43,8 +43,8 @@ services:
|
|||||||
- "5672:5672"
|
- "5672:5672"
|
||||||
- "15672:15672"
|
- "15672:15672"
|
||||||
environment:
|
environment:
|
||||||
RABBITMQ_DEFAULT_USER: admin
|
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-admin}
|
||||||
RABBITMQ_DEFAULT_PASS: password123
|
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-password123}
|
||||||
volumes:
|
volumes:
|
||||||
- rabbitmq_data:/var/lib/rabbitmq
|
- rabbitmq_data:/var/lib/rabbitmq
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import ChangePasswordView from '../views/ChangePasswordView.vue'
|
|||||||
import RegisterView from '../views/RegisterView.vue'
|
import RegisterView from '../views/RegisterView.vue'
|
||||||
import SettingsView from '../views/SettingsView.vue'
|
import SettingsView from '../views/SettingsView.vue'
|
||||||
import UserProfileView from '../views/UserProfileView.vue'
|
import UserProfileView from '../views/UserProfileView.vue'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
@@ -16,14 +17,23 @@ const router = createRouter({
|
|||||||
{ path: '/', name: 'home', component: HomeView },
|
{ path: '/', name: 'home', component: HomeView },
|
||||||
{ path: '/feed', redirect: '/' },
|
{ path: '/feed', redirect: '/' },
|
||||||
{ path: '/hot', name: 'hot', component: HotView },
|
{ path: '/hot', name: 'hot', component: HotView },
|
||||||
{ path: '/video', name: 'video', component: VideoView },
|
{ path: '/video', name: 'video', component: VideoView, meta: { requiresAuth: true } },
|
||||||
{ path: '/video/:id', name: 'video-detail', component: VideoDetailView, props: true },
|
{ path: '/video/:id', name: 'video-detail', component: VideoDetailView, props: true },
|
||||||
{ path: '/account', name: 'account', component: AccountView },
|
{ path: '/account', name: 'account', component: AccountView },
|
||||||
{ path: '/account/register', name: 'account-register', component: RegisterView },
|
{ path: '/account/register', name: 'account-register', component: RegisterView },
|
||||||
{ path: '/account/change-password', name: 'account-change-password', component: ChangePasswordView },
|
{ path: '/account/change-password', name: 'account-change-password', component: ChangePasswordView },
|
||||||
{ path: '/settings', name: 'settings', component: SettingsView },
|
{ path: '/settings', name: 'settings', component: SettingsView, meta: { requiresAuth: true } },
|
||||||
{ path: '/u/:id', name: 'user-profile', component: UserProfileView, props: true },
|
{ path: '/u/:id', name: 'user-profile', component: UserProfileView, props: true },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
if (to.meta.requiresAuth && !auth.isLoggedIn) {
|
||||||
|
next({ path: '/account', query: { redirect: to.fullPath } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
Reference in New Issue
Block a user