fix(P2): rand.Read错误处理 + JWT随机密钥 + 密码环境变量化 + 前端路由守卫

This commit is contained in:
Sisyphus
2026-04-25 15:57:15 +08:00
parent 9d903ad8e1
commit 7d02262098
5 changed files with 489 additions and 441 deletions

17
.env.example Normal file
View 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

View File

@@ -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)
} }

View File

@@ -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 {

View File

@@ -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:

View File

@@ -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