Browse Source

修改密码前台界面强度计算OK

master
hejl 2 weeks ago
parent
commit
fd9bca309b
  1. 53
      gofaster/app/dev-hot-enhanced.ps1
  2. 1259
      gofaster/app/dist/renderer/js/index.js
  3. 1
      gofaster/app/package.json
  4. 57
      gofaster/app/src/renderer/components/MainLayout.vue
  5. 822
      gofaster/app/src/renderer/components/PasswordChangeModal.vue
  6. 3
      gofaster/app/src/renderer/components/PasswordProfile.vue
  7. 40
      gofaster/app/src/renderer/services/userService.js
  8. 36
      gofaster/app/src/renderer/views/UserProfile.vue
  9. 166
      gofaster/backend/internal/auth/controller/password_controller.go
  10. 57
      gofaster/backend/internal/auth/migration/migration.go
  11. 11
      gofaster/backend/internal/auth/model/auth.go
  12. 64
      gofaster/backend/internal/auth/model/password_policy.go
  13. 22
      gofaster/backend/internal/auth/model/user.go
  14. 8
      gofaster/backend/internal/auth/module.go
  15. 45
      gofaster/backend/internal/auth/repository/password_history_repo.go
  16. 71
      gofaster/backend/internal/auth/repository/password_policy_repo.go
  17. 54
      gofaster/backend/internal/auth/repository/password_reset_repo.go
  18. 26
      gofaster/backend/internal/auth/routes/auth_routes.go
  19. 14
      gofaster/backend/internal/auth/service/auth_service.go
  20. 381
      gofaster/backend/internal/auth/service/password_service.go
  21. 12
      gofaster/backend/internal/auth/service/user_service.go
  22. 1
      gofaster/backend/tmp/build-errors.log
  23. BIN
      gofaster/backend/tmp/main.exe
  24. 15
      gofaster/dev-full.ps1

53
gofaster/app/dev-hot-enhanced.ps1

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
# GoFaster Enhanced Hot Reload Script
# 增强版热重载脚本,确保前端代码变化时能够实时刷新
Write-Host "Starting GoFaster Frontend with Enhanced Hot Reload..." -ForegroundColor Green
Write-Host ""
Write-Host "Enhanced hot reload enabled with the following features:" -ForegroundColor Yellow
Write-Host " ✓ Vue.js watch mode for automatic rebuilds" -ForegroundColor Cyan
Write-Host " ✓ Electron reload on file changes" -ForegroundColor Cyan
Write-Host " ✓ Concurrent build and electron processes" -ForegroundColor Cyan
Write-Host " ✓ File change detection and auto-reload" -ForegroundColor Cyan
Write-Host ""
Write-Host "Code changes will automatically:" -ForegroundColor White
Write-Host " 1. Trigger Vue.js rebuild" -ForegroundColor White
Write-Host " 2. Reload Electron window" -ForegroundColor White
Write-Host " 3. Apply changes immediately" -ForegroundColor White
Write-Host ""
Write-Host "Press Ctrl+C to stop" -ForegroundColor Yellow
Write-Host ""
# 检查依赖
Write-Host "Checking dependencies..." -ForegroundColor Yellow
if (-not (Test-Path "node_modules")) {
Write-Host "Dependencies not installed, installing..." -ForegroundColor Yellow
npm install
if ($LASTEXITCODE -ne 0) {
Write-Host "Dependency installation failed" -ForegroundColor Red
exit 1
}
}
# 检查必要的包
$concurrentlyInstalled = npm list concurrently 2>$null
$waitOnInstalled = npm list wait-on 2>$null
if (-not $concurrentlyInstalled) {
Write-Host "Installing concurrently..." -ForegroundColor Yellow
npm install --save-dev concurrently
}
if (-not $waitOnInstalled) {
Write-Host "Installing wait-on..." -ForegroundColor Yellow
npm install --save-dev wait-on
}
Write-Host "Dependencies check completed" -ForegroundColor Green
Write-Host ""
# 启动增强版热重载
Write-Host "Starting enhanced hot reload..." -ForegroundColor Green
Write-Host "This will run: npm run dev:watch" -ForegroundColor Cyan
Write-Host ""
npm run dev:watch

1259
gofaster/app/dist/renderer/js/index.js vendored

File diff suppressed because it is too large Load Diff

1
gofaster/app/package.json

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
"scripts": {
"dev": "cross-env VUE_CLI_BABEL_TRANSPILE_MODULES=false VUE_CLI_MODERN_BUILD=false VUE_CLI_SERVICE_CONFIG_PATH=vue.config.js vue-cli-service build --mode development --verbose && electron .",
"dev:watch": "cross-env VUE_CLI_BABEL_TRANSPILE_MODULES=false VUE_CLI_MODERN_BUILD=false VUE_CLI_SERVICE_CONFIG_PATH=vue.config.js concurrently \"vue-cli-service build --mode development --watch --verbose\" \"wait-on dist/renderer/index.html && electron .\"",
"dev:hot": "powershell -ExecutionPolicy Bypass -File dev-hot-enhanced.ps1",
"build": "cross-env VUE_CLI_BABEL_TRANSPILE_MODULES=false VUE_CLI_MODERN_BUILD=false VUE_CLI_SERVICE_CONFIG_PATH=vue.config.js vue-cli-service build --verbose && electron-builder",
"electron:serve": "cross-env VUE_CLI_BABEL_TRANSPILE_MODULES=false VUE_CLI_MODERN_BUILD=false VUE_CLI_SERVICE_CONFIG_PATH=vue.config.js vue-cli-service build --mode development --verbose && electron .",
"build:vue": "cross-env VUE_CLI_BABEL_TRANSPILE_MODULES=false VUE_CLI_MODERN_BUILD=false VUE_CLI_SERVICE_CONFIG_PATH=vue.config.js vue-cli-service build --mode development --verbose",

57
gofaster/app/src/renderer/components/MainLayout.vue

@ -182,6 +182,14 @@ @@ -182,6 +182,14 @@
@show-message="handleShowMessage"
/>
<!-- 密码修改弹窗 -->
<PasswordChangeModal
v-model:visible="showPasswordModal"
:is-force-change="isForceChange"
@close="showPasswordModal = false"
@success="handlePasswordChangeSuccess"
/>
<!-- Toast 消息提示 -->
<Toast
v-model:visible="showToast"
@ -195,6 +203,7 @@ @@ -195,6 +203,7 @@
<script>
import { ref, reactive, computed, onMounted, onUnmounted, watch, nextTick, provide } from 'vue'
import PasswordChangeModal from './PasswordChangeModal.vue'
import { useRouter, useRoute } from 'vue-router'
import { userService } from '@/services/userService'
import themeManager from '../utils/themeManager.js'
@ -205,7 +214,8 @@ export default { @@ -205,7 +214,8 @@ export default {
name: 'MainLayout',
components: {
LoginModal,
Toast
Toast,
PasswordChangeModal
},
setup() {
const router = useRouter()
@ -216,6 +226,10 @@ export default { @@ -216,6 +226,10 @@ export default {
const showUserMenu = ref(false)
const showLoginModalFlag = ref(false)
const showToast = ref(false)
//
const showPasswordModal = ref(false)
const isForceChange = ref(false)
const currentTab = ref('home')
const openTabs = ref([
{ id: 'home', title: '欢迎', path: '/', closable: false }
@ -509,7 +523,20 @@ export default { @@ -509,7 +523,20 @@ export default {
showUserMenu.value = false
}
const handleLoginSuccess = async (response) => {
//
const handlePasswordChangeSuccess = () => {
//
showPasswordModal.value = false
isForceChange.value = false
//
showToastMessage('success', '密码修改成功', '您的密码已成功修改')
//
router.push('/')
}
const handleLoginSuccess = async (response) => {
console.log('登录成功响应:', response)
// - { code: 200, message: "", data: { token: "...", user: {...} } }
@ -571,10 +598,23 @@ export default { @@ -571,10 +598,23 @@ export default {
console.log('更新后的用户信息:', currentUser)
console.log(`欢迎回来,${currentUser.name}`)
//
window.dispatchEvent(new CustomEvent('user-login-success', {
detail: { user: currentUser, token: token }
}))
//
if (userData.force_change_password) {
isForceChange.value = true
showPasswordModal.value = true
showToastMessage('warning', '密码重置提醒', '您的密码已被重置,请立即修改密码')
} else {
//
window.dispatchEvent(new CustomEvent('user-login-success', {
detail: { user: currentUser, token: token }
}))
//
showToastMessage('success', '登录成功', `欢迎回来,${currentUser.name}`)
//
router.push('/')
}
}
} catch (error) {
console.error('获取用户信息失败:', error)
@ -852,10 +892,13 @@ export default { @@ -852,10 +892,13 @@ export default {
openSettings,
logout,
handleLoginSuccess,
handlePasswordChangeSuccess,
handleShowMessage,
loadAppSettings,
handleAddTab,
addTabIfNotExists
addTabIfNotExists,
showPasswordModal,
isForceChange
}
}
}

822
gofaster/app/src/renderer/components/PasswordChangeModal.vue

@ -0,0 +1,822 @@ @@ -0,0 +1,822 @@
<template>
<div v-if="visible" class="password-modal-overlay" @click="handleOverlayClick">
<div class="password-modal" @click.stop>
<div class="modal-header">
<h3>{{ title }}</h3>
<button class="close-btn" @click="handleClose">×</button>
</div>
<div class="modal-body">
<form @submit.prevent="handleSubmit">
<!-- 当前密码 -->
<div class="form-group" v-if="!isForceChange">
<label>当前密码 *</label>
<input
v-model="form.currentPassword"
type="password"
required
placeholder="请输入当前密码"
:class="{ 'error': errors.currentPassword }"
/>
<div v-if="errors.currentPassword" class="error-message">
{{ errors.currentPassword }}
</div>
</div>
<!-- 新密码 -->
<div class="form-group">
<label>新密码 *</label>
<input
v-model="form.newPassword"
type="password"
required
placeholder="请输入新密码"
@input="validatePassword"
@keyup="updateRequirements(form.newPassword)"
:class="{ 'error': errors.newPassword }"
/>
<div v-if="errors.newPassword" class="error-message">
{{ errors.newPassword }}
</div>
<!-- 密码强度指示器 -->
<div class="password-strength" >
<div class="strength-bar">
<div
class="strength-fill"
:class="strengthClass"
:style="{ width: strengthPercentage + '%' }"
></div>
</div>
<div class="strength-text">
强度: {{ strengthText }} ({{ passwordLevel }})
</div>
</div>
<!-- 密码要求提示 -->
<div class="password-requirements" >
<div class="requirement" :class="{ 'met': requirements.length }">
密码长度{{ form.newPassword.length }}
</div>
<div class="requirement" :class="{ 'met': requirements.uppercase }">
包含大写字母
</div>
<div class="requirement" :class="{ 'met': requirements.lowercase }">
包含小写字母
</div>
<div class="requirement" :class="{ 'met': requirements.numbers }">
包含数字
</div>
<div class="requirement" :class="{ 'met': requirements.special }">
包含特殊字符
</div>
</div>
</div>
<!-- 确认新密码 -->
<div class="form-group">
<label>确认新密码 *</label>
<input
v-model="form.confirmPassword"
type="password"
required
placeholder="请再次输入新密码"
@input="validateConfirmPassword"
:class="{ 'error': errors.confirmPassword }"
/>
<div v-if="errors.confirmPassword" class="error-message">
{{ errors.confirmPassword }}
</div>
</div>
<!-- 操作按钮 -->
<div class="form-actions">
<button
type="button"
class="btn btn-secondary"
@click="handleClose"
:disabled="loading"
>
取消
</button>
<button
type="submit"
class="btn btn-primary"
:disabled="loading || !isFormValid"
>
<span v-if="loading" class="loading-spinner"></span>
{{ submitText }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script>
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { userService } from '@/services/userService'
export default {
name: 'PasswordChangeModal',
props: {
visible: {
type: Boolean,
default: false
},
isForceChange: {
type: Boolean,
default: false
}
},
emits: ['close', 'success'],
setup(props, { emit }) {
const loading = ref(false)
const policy = ref({
minLength: 6,
minCharTypes: 1,
preventReuse: 3,
level: 1,
minRequiredLevel: 1,
requireUppercase: false,
requireLowercase: false,
requireNumbers: false,
requireSpecial: false
})
const form = reactive({
currentPassword: '',
newPassword: '',
confirmPassword: ''
})
const errors = reactive({
currentPassword: '',
newPassword: '',
confirmPassword: ''
})
const requirements = reactive({
length: false,
uppercase: false,
lowercase: false,
numbers: false,
special: false
})
const passwordStrength = ref(0)
const passwordLevel = ref(0)
//
const title = computed(() => {
return props.isForceChange ? '强制修改密码' : '修改密码'
})
const submitText = computed(() => {
return props.isForceChange ? '确认修改' : '修改密码'
})
const strengthClass = computed(() => {
if (passwordStrength.value >= 4) return 'strong'
if (passwordStrength.value >= 3) return 'medium'
if (passwordStrength.value >= 2) return 'weak'
return 'very-weak'
})
const strengthText = computed(() => {
if (passwordStrength.value >= 4) return '强'
if (passwordStrength.value >= 3) return '中'
if (passwordStrength.value >= 2) return '弱'
return '很弱'
})
const strengthPercentage = computed(() => {
// 0-50-100
return (passwordStrength.value / 5) * 100
})
const isFormValid = computed(() => {
if (props.isForceChange) {
return form.newPassword && form.confirmPassword &&
form.newPassword === form.confirmPassword &&
!errors.newPassword && !errors.confirmPassword
}
return form.currentPassword && form.newPassword && form.confirmPassword &&
form.newPassword === form.confirmPassword &&
!errors.currentPassword && !errors.newPassword && !errors.confirmPassword
})
//
const loadPasswordPolicy = async () => {
try {
console.log('开始加载密码策略...')
const response = await userService.getPasswordPolicy()
console.log('密码策略API响应:', response)
console.log('响应类型:', typeof response)
console.log('response.data 类型:', typeof response.data)
console.log('response.data 内容:', response.data)
// response.data
if (response.data && response.data.code === 200 && response.data.data) {
policy.value = response.data.data
console.log('密码策略加载成功:', policy.value)
} else if (response && response.code === 200 && response.data) {
// response.dataresponse
policy.value = response.data
console.log('密码策略加载成功(备用路径):', policy.value)
} else {
console.warn('密码策略响应格式不正确:', response)
console.warn('尝试解析响应结构...')
if (response.data) {
console.warn('response.data.code:', response.data.code)
console.warn('response.data.data:', response.data.data)
}
if (response) {
console.warn('response.code:', response.code)
console.warn('response.data:', response.data)
}
// 使
policy.value = {
minLength: 6,
minCharTypes: 1,
preventReuse: 3,
level: 1,
minRequiredLevel: 1,
requireUppercase: false,
requireLowercase: false,
requireNumbers: false,
requireSpecial: false
}
}
} catch (error) {
console.error('加载密码策略失败:', error)
// 使
policy.value = {
minLength: 6,
minCharTypes: 1,
preventReuse: 3,
level: 1,
minRequiredLevel: 1,
requireUppercase: false,
requireLowercase: false,
requireNumbers: false,
requireSpecial: false
}
}
}
const validatePassword = async () => {
errors.newPassword = ''
if (!form.newPassword) {
//
updateRequirements('')
return
}
try {
console.log('正在验证密码:', form.newPassword)
const response = await userService.validatePassword(form.newPassword)
console.log('后端验证响应:', response)
//
let result = null
if (response.data && response.data.code === 200 && response.data.data) {
result = response.data.data
} else if (response && response.code === 200 && response.data) {
result = response.data
}
if (result) {
console.log('验证结果:', result)
if (!result.is_valid) {
//
if (Array.isArray(result.errors)) {
errors.newPassword = result.errors.join('; ')
} else if (typeof result.errors === 'string') {
errors.newPassword = result.errors
} else {
errors.newPassword = '密码验证失败'
}
}
passwordStrength.value = result.strength || 0
passwordLevel.value = result.level || 0
console.log('设置密码强度:', passwordStrength.value, '等级:', passwordLevel.value)
//
updateRequirements(form.newPassword)
} else {
console.warn('密码验证响应格式不正确:', response)
// 使
updateRequirements(form.newPassword)
}
} catch (error) {
console.error('密码验证失败:', error)
// 使
updateRequirements(form.newPassword)
}
}
const updateRequirements = (password) => {
console.log('updateRequirements 被调用,密码:', password)
//
requirements.length = false
requirements.uppercase = false
requirements.lowercase = false
requirements.numbers = false
requirements.special = false
//
if (!password || password.length === 0) {
return
}
let hasUppercase = false
let hasLowercase = false
let hasNumbers = false
let hasSpecial = false
for (const char of password) {
// 使
const charCode = char.charCodeAt(0)
if (charCode >= 65 && charCode <= 90) { // A-Z
hasUppercase = true
} else if (charCode >= 97 && charCode <= 122) { // a-z
hasLowercase = true
} else if (charCode >= 48 && charCode <= 57) { // 0-9
hasNumbers = true
} else {
//
hasSpecial = true
}
}
console.log('字符类型检测结果:', {
hasUppercase: hasUppercase,
hasLowercase: hasLowercase,
hasNumbers: hasNumbers,
hasSpecial: hasSpecial,
length: password.length
})
//
requirements.length = password.length >= 6 // 6
requirements.uppercase = hasUppercase
requirements.lowercase = hasLowercase
requirements.numbers = hasNumbers
requirements.special = hasSpecial
//
let charTypes = 0
if (hasUppercase) charTypes++
if (hasLowercase) charTypes++
if (hasNumbers) charTypes++
if (hasSpecial) charTypes++
//
const strength = calculatePasswordStrength(password, charTypes)
passwordStrength.value = strength
passwordLevel.value = strength
console.log('更新后的要求状态:', {
length: requirements.length,
uppercase: requirements.uppercase,
lowercase: requirements.lowercase,
numbers: requirements.numbers,
special: requirements.special
})
console.log('计算出的密码强度:', strength)
}
//
const calculatePasswordStrength = (password, charTypes) => {
//
// 5>=8>=4
if (password.length >= 8 && charTypes >= 4) {
return 5
}
// 4>=8>=3
if (password.length >= 8 && charTypes >= 3) {
return 4
}
// 3>=6>=3
if (password.length >= 6 && charTypes >= 3) {
return 3
}
// 2>=6>=2
if (password.length >= 6 && charTypes >= 2) {
return 2
}
// 1>=6>=1
if (password.length >= 6 && charTypes >= 1) {
return 1
}
// 0>=1>=1
if (password.length >= 1) {
return 0
}
// 00
return 0
}
const validateConfirmPassword = () => {
errors.confirmPassword = ''
if (form.newPassword !== form.confirmPassword) {
errors.confirmPassword = '两次输入的密码不一致'
}
}
const handleSubmit = async () => {
if (!isFormValid.value) return
loading.value = true
try {
const requestData = {
current_password: form.currentPassword,
new_password: form.newPassword,
confirm_password: form.confirmPassword
}
await userService.changePassword(requestData)
//
emit('success')
handleClose()
//
if (window.showToast) {
window.showToast({
type: 'success',
title: '密码修改成功',
content: '您的密码已成功修改'
})
}
} catch (error) {
console.error('修改密码失败:', error)
//
if (error.response?.data?.message) {
if (error.response.data.message.includes('当前密码不正确')) {
errors.currentPassword = '当前密码不正确'
} else if (error.response.data.message.includes('新密码不符合要求')) {
errors.newPassword = error.response.data.message
} else {
errors.newPassword = error.response.data.message
}
} else {
errors.newPassword = '修改密码失败,请重试'
}
} finally {
loading.value = false
}
}
const handleClose = () => {
if (loading.value) return
//
form.currentPassword = ''
form.newPassword = ''
form.confirmPassword = ''
//
Object.keys(errors).forEach(key => {
errors[key] = ''
})
//
Object.keys(requirements).forEach(key => {
requirements[key] = false
})
passwordStrength.value = 0
passwordLevel.value = 0
emit('close')
}
const handleOverlayClick = () => {
if (!props.isForceChange) {
handleClose()
}
}
//
watch(() => props.visible, (newVal) => {
if (newVal) {
console.log('模态框显示,开始加载密码策略')
loadPasswordPolicy()
}
})
//
watch(() => form.newPassword, (newPassword) => {
if (newPassword) {
console.log('密码变化,更新要求状态:', newPassword)
updateRequirements(newPassword)
} else {
//
Object.keys(requirements).forEach(key => {
requirements[key] = false
})
//
passwordStrength.value = 0
passwordLevel.value = 0
}
})
//
onMounted(() => {
console.log('组件挂载,检查是否需要加载密码策略')
if (props.visible) {
loadPasswordPolicy()
}
//
if (form.newPassword) {
updateRequirements(form.newPassword)
}
})
return {
loading,
policy,
form,
errors,
requirements,
passwordStrength,
passwordLevel,
title,
submitText,
strengthClass,
strengthText,
strengthPercentage,
isFormValid,
validatePassword,
validateConfirmPassword,
updateRequirements,
handleSubmit,
handleClose,
handleOverlayClick
}
}
}
</script>
<style scoped>
.password-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.password-modal {
background: var(--card-bg);
border-radius: 12px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 8px 32px var(--shadow-color);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--border-color);
}
.modal-header h3 {
margin: 0;
color: var(--text-primary);
font-size: 18px;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: var(--text-muted);
padding: 4px;
border-radius: 4px;
transition: all 0.2s;
}
.close-btn:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.modal-body {
padding: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--text-primary);
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
background: var(--input-bg);
color: var(--text-primary);
transition: all 0.2s;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(25, 118, 210, 0.2);
}
.form-group input.error {
border-color: #f44336;
}
.error-message {
color: #f44336;
font-size: 12px;
margin-top: 4px;
}
/* 密码强度指示器 */
.password-strength {
margin-top: 12px;
}
.strength-bar {
width: 100%;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
overflow: hidden;
margin-bottom: 8px;
}
.strength-fill {
height: 100%;
transition: width 0.3s ease;
}
.strength-fill.very-weak {
background: #f44336;
}
.strength-fill.weak {
background: #ff9800;
}
.strength-fill.medium {
background: #ffc107;
}
.strength-fill.strong {
background: #4caf50;
}
.strength-text {
font-size: 12px;
color: var(--text-secondary);
text-align: center;
}
/* 密码要求提示 */
.password-requirements {
margin-top: 12px;
padding: 12px;
background: var(--bg-secondary);
border-radius: 6px;
font-size: 12px;
}
.requirement {
color: var(--text-muted);
margin-bottom: 4px;
transition: color 0.2s;
}
.requirement.met {
color: #4caf50;
}
.requirement:last-child {
margin-bottom: 0;
}
/* 操作按钮 */
.form-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 30px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: var(--accent-color);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-hover);
}
.btn-secondary {
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover:not(:disabled) {
background: var(--bg-tertiary);
}
/* 加载动画 */
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 768px) {
.password-modal {
width: 95%;
margin: 20px;
}
.modal-header,
.modal-body {
padding: 16px;
}
.form-actions {
flex-direction: column;
}
.btn {
width: 100%;
justify-content: center;
}
}
</style>

3
gofaster/app/src/renderer/components/PasswordProfile.vue

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@

40
gofaster/app/src/renderer/services/userService.js

@ -104,6 +104,46 @@ export const userService = { @@ -104,6 +104,46 @@ export const userService = {
}
},
// 修改密码
async changePassword(passwordData) {
try {
const response = await api.post('/auth/change-password', passwordData)
return response
} catch (error) {
throw error
}
},
// 获取密码策略
async getPasswordPolicy() {
try {
const response = await api.get('/auth/password-policy')
return response
} catch (error) {
throw error
}
},
// 验证密码强度
async validatePassword(password) {
try {
const response = await api.post('/auth/validate-password', { password })
return response
} catch (error) {
throw error
}
},
// 检查密码状态
async checkPasswordStatus() {
try {
const response = await api.get('/auth/password-status')
return response
} catch (error) {
throw error
}
},
// 获取权限列表
async getPermissions() {
try {

36
gofaster/app/src/renderer/views/UserProfile.vue

@ -127,6 +127,14 @@ @@ -127,6 +127,14 @@
<p>请先登录以查看您的个人资料</p>
<button @click="goToLogin" class="login-btn">去登录</button>
</div>
<!-- 密码修改弹窗 -->
<PasswordChangeModal
v-model:visible="showPasswordModal"
:is-force-change="isForceChange"
@close="showPasswordModal = false"
@success="onPasswordChangeSuccess"
/>
</div>
</template>
@ -134,9 +142,13 @@ @@ -134,9 +142,13 @@
import { ref, onMounted, inject } from 'vue'
import { useRouter } from 'vue-router'
import { userService } from '@/services/userService'
import PasswordChangeModal from '@/components/PasswordChangeModal.vue'
export default {
name: 'UserProfile',
components: {
PasswordChangeModal
},
setup() {
const router = useRouter()
@ -150,6 +162,10 @@ export default { @@ -150,6 +162,10 @@ export default {
const error = ref(null)
const userInfo = ref(null)
//
const showPasswordModal = ref(false)
const isForceChange = ref(false)
//
const loadUserProfile = async () => {
if (!isLoggedIn.value) {
@ -187,8 +203,20 @@ export default { @@ -187,8 +203,20 @@ export default {
//
const changePassword = () => {
// TODO:
console.log('修改密码功能待实现')
isForceChange.value = false
showPasswordModal.value = true
}
//
const forceChangePassword = () => {
isForceChange.value = true
showPasswordModal.value = true
}
//
const onPasswordChangeSuccess = () => {
//
loadUserProfile()
}
//
@ -251,9 +279,13 @@ export default { @@ -251,9 +279,13 @@ export default {
userInfo,
currentUser,
isLoggedIn,
showPasswordModal,
isForceChange,
loadUserProfile,
refreshProfile,
changePassword,
forceChangePassword,
onPasswordChangeSuccess,
goToLogin,
formatDate,
getStatusText,

166
gofaster/backend/internal/auth/controller/password_controller.go

@ -0,0 +1,166 @@ @@ -0,0 +1,166 @@
package controller
import (
"net/http"
"strconv"
"gofaster/internal/auth/model"
"gofaster/internal/auth/service"
"gofaster/internal/shared/response"
"github.com/gin-gonic/gin"
)
type PasswordController struct {
passwordService *service.PasswordService
userService *service.UserService
}
func NewPasswordController(
passwordService *service.PasswordService,
userService *service.UserService,
) *PasswordController {
return &PasswordController{
passwordService: passwordService,
userService: userService,
}
}
// ChangePassword 修改密码
func (c *PasswordController) ChangePassword(ctx *gin.Context) {
var req model.PasswordChangeRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error())
return
}
// 从JWT获取用户ID
userID, exists := ctx.Get("user_id")
if !exists {
response.Error(ctx, http.StatusUnauthorized, "未授权", "用户ID不存在")
return
}
req.UserID = userID.(uint)
// 验证新密码确认
if req.NewPassword != req.ConfirmPassword {
response.Error(ctx, http.StatusBadRequest, "密码确认失败", "新密码与确认密码不匹配")
return
}
// 修改密码
err := c.passwordService.ChangePassword(req.UserID, req.CurrentPassword, req.NewPassword)
if err != nil {
response.Error(ctx, http.StatusBadRequest, "修改密码失败", err.Error())
return
}
response.Success(ctx, "密码修改成功", nil)
}
// ResetPassword 重置用户密码(管理员功能)
func (c *PasswordController) ResetPassword(ctx *gin.Context) {
userIDStr := ctx.Param("id")
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
response.Error(ctx, http.StatusBadRequest, "用户ID格式错误", err.Error())
return
}
// 从JWT获取操作人ID
operatorID, exists := ctx.Get("user_id")
if !exists {
response.Error(ctx, http.StatusUnauthorized, "未授权", "操作人ID不存在")
return
}
// 重置密码
tempPassword, err := c.passwordService.ResetPassword(uint(userID), operatorID.(uint))
if err != nil {
response.Error(ctx, http.StatusInternalServerError, "重置密码失败", err.Error())
return
}
response.Success(ctx, "密码重置成功", gin.H{
"temp_password": tempPassword,
"message": "用户首次登录后需要立即修改密码",
})
}
// GetPasswordPolicy 获取密码策略
func (c *PasswordController) GetPasswordPolicy(ctx *gin.Context) {
policy, err := c.passwordService.GetPasswordPolicy()
if err != nil {
response.Error(ctx, http.StatusInternalServerError, "获取密码策略失败", err.Error())
return
}
response.Success(ctx, "获取密码策略成功", policy)
}
// ValidatePassword 验证密码强度
func (c *PasswordController) ValidatePassword(ctx *gin.Context) {
var req struct {
Password string `json:"password" binding:"required"`
}
if err := ctx.ShouldBindJSON(&req); err != nil {
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error())
return
}
// 验证密码
result, err := c.passwordService.ValidatePassword(req.Password)
if err != nil {
response.Error(ctx, http.StatusInternalServerError, "密码验证失败", err.Error())
return
}
response.Success(ctx, "密码验证完成", result)
}
// CheckPasswordStatus 检查用户密码状态
func (c *PasswordController) CheckPasswordStatus(ctx *gin.Context) {
userID, exists := ctx.Get("user_id")
if !exists {
response.Error(ctx, http.StatusUnauthorized, "未授权", "用户ID不存在")
return
}
// 获取用户信息
user, err := c.userService.GetByID(userID.(uint))
if err != nil {
response.Error(ctx, http.StatusInternalServerError, "获取用户信息失败", err.Error())
return
}
// 检查密码过期
isExpired, err := c.passwordService.CheckPasswordExpiration(user)
if err != nil {
response.Error(ctx, http.StatusInternalServerError, "检查密码过期失败", err.Error())
return
}
// 检查是否需要强制修改密码
forceChange := c.passwordService.CheckForceChangePassword(user)
response.Success(ctx, "获取密码状态成功", gin.H{
"force_change_password": forceChange,
"password_expired": isExpired,
"password_changed_at": user.PasswordChangedAt,
})
}
// UpdatePasswordPolicy 更新密码策略(管理员功能)
func (c *PasswordController) UpdatePasswordPolicy(ctx *gin.Context) {
var policy model.PasswordPolicy
if err := ctx.ShouldBindJSON(&policy); err != nil {
response.Error(ctx, http.StatusBadRequest, "请求参数错误", err.Error())
return
}
// 这里需要调用密码策略服务来更新策略
// 暂时返回成功,具体实现需要完善
response.Success(ctx, "密码策略更新成功", policy)
}

57
gofaster/backend/internal/auth/migration/migration.go

@ -29,6 +29,15 @@ func RunMigrations(db *gorm.DB) error { @@ -29,6 +29,15 @@ func RunMigrations(db *gorm.DB) error {
return err
}
// 自动迁移密码策略相关表
if err := db.AutoMigrate(
&model.PasswordPolicy{},
&model.PasswordHistory{},
&model.PasswordReset{},
); err != nil {
return err
}
// 创建默认角色
if err := createDefaultRoles(db); err != nil {
return err
@ -39,6 +48,11 @@ func RunMigrations(db *gorm.DB) error { @@ -39,6 +48,11 @@ func RunMigrations(db *gorm.DB) error {
return err
}
// 创建默认密码策略
if err := createDefaultPasswordPolicy(db); err != nil {
return err
}
return nil
}
@ -53,18 +67,18 @@ func createDefaultRoles(db *gorm.DB) error { @@ -53,18 +67,18 @@ func createDefaultRoles(db *gorm.DB) error {
roles := []model.Role{
{
Name: "超级管理员",
Code: "SUPER_ADMIN",
Name: "超级管理员",
Code: "SUPER_ADMIN",
Description: "系统超级管理员,拥有所有权限",
},
{
Name: "管理员",
Code: "ADMIN",
Name: "管理员",
Code: "ADMIN",
Description: "系统管理员,拥有大部分权限",
},
{
Name: "普通用户",
Code: "USER",
Name: "普通用户",
Code: "USER",
Description: "普通用户,拥有基本权限",
},
}
@ -113,3 +127,34 @@ func createDefaultAdmin(db *gorm.DB) error { @@ -113,3 +127,34 @@ func createDefaultAdmin(db *gorm.DB) error {
return nil
}
// createDefaultPasswordPolicy 创建默认密码策略
func createDefaultPasswordPolicy(db *gorm.DB) error {
// 检查是否已存在默认策略
var count int64
db.Model(&model.PasswordPolicy{}).Count(&count)
if count > 0 {
return nil // 已存在默认策略,跳过
}
// 创建默认密码策略(1级)
defaultPolicy := &model.PasswordPolicy{
Level: 1,
MinRequiredLevel: 1, // 新增:要求最低1级强度
MinLength: 6,
RequireUppercase: false,
RequireLowercase: false,
RequireNumbers: false,
RequireSpecial: false,
MinCharTypes: 1,
ExpirationDays: 30,
PreventReuse: 3,
IsActive: true,
}
if err := db.Create(defaultPolicy).Error; err != nil {
return err
}
return nil
}

11
gofaster/backend/internal/auth/model/auth.go

@ -13,11 +13,12 @@ type LoginRequest struct { @@ -13,11 +13,12 @@ type LoginRequest struct {
// LoginResponse 登录响应
type LoginResponse struct {
Token string `json:"token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
User UserInfo `json:"user"`
Token string `json:"token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
User UserInfo `json:"user"`
ForceChangePassword bool `json:"force_change_password"` // 是否需要强制修改密码
}
// UserInfo 用户信息(登录后返回)

64
gofaster/backend/internal/auth/model/password_policy.go

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
package model
import (
"time"
"gorm.io/gorm"
)
// PasswordPolicy 密码策略
type PasswordPolicy struct {
ID uint `gorm:"primarykey" json:"id"`
Level int `gorm:"not null;default:1" json:"level"` // 密码策略等级 0-5
MinRequiredLevel int `gorm:"not null;default:1" json:"min_required_level"` // 新增:系统要求的最低密码强度等级
MinLength int `gorm:"not null;default:6" json:"min_length"` // 最小长度
RequireUppercase bool `gorm:"not null;default:false" json:"require_uppercase"` // 要求大写字母
RequireLowercase bool `gorm:"not null;default:false" json:"require_lowercase"` // 要求小写字母
RequireNumbers bool `gorm:"not null;default:false" json:"require_numbers"` // 要求数字
RequireSpecial bool `gorm:"not null;default:false" json:"require_special"` // 要求特殊字符
MinCharTypes int `gorm:"not null;default:1" json:"min_char_types"` // 最少字符类型数
ExpirationDays int `gorm:"not null;default:30" json:"expiration_days"` // 密码失效天数
PreventReuse int `gorm:"not null;default:3" json:"prevent_reuse"` // 防止重复使用前N次密码
IsActive bool `gorm:"not null;default:true" json:"is_active"` // 是否启用
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// PasswordHistory 密码历史记录
type PasswordHistory struct {
ID uint `gorm:"primarykey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"` // 用户ID
Password string `gorm:"not null" json:"password"` // 加密后的密码
CreatedAt time.Time `json:"created_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// PasswordReset 密码重置记录
type PasswordReset struct {
ID uint `gorm:"primarykey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"` // 用户ID
ResetBy uint `gorm:"not null" json:"reset_by"` // 重置操作人ID
ResetAt time.Time `gorm:"not null" json:"reset_at"` // 重置时间
IsUsed bool `gorm:"not null;default:false" json:"is_used"` // 是否已使用
UsedAt *time.Time `json:"used_at"` // 使用时间
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// PasswordChangeRequest 密码修改请求
type PasswordChangeRequest struct {
UserID uint `json:"user_id"`
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
ConfirmPassword string `json:"confirm_password"`
}
// PasswordValidationResult 密码验证结果
type PasswordValidationResult struct {
IsValid bool `json:"is_valid"`
Errors []string `json:"errors"`
Strength int `json:"strength"` // 密码强度 0-100
Level int `json:"level"` // 符合的密码等级
}

22
gofaster/backend/internal/auth/model/user.go

@ -7,16 +7,18 @@ import ( @@ -7,16 +7,18 @@ import (
type User struct {
model.BaseModel
Username string `gorm:"uniqueIndex;size:50" json:"username"`
Password string `gorm:"size:100" json:"-"`
Email string `gorm:"size:100" json:"email"`
Phone string `gorm:"size:20" json:"phone"`
Status int `gorm:"default:1" json:"status"` // 1-正常 2-禁用 3-锁定
PasswordErrorCount int `gorm:"default:0" json:"-"` // 密码错误次数
LockedAt *time.Time `gorm:"index" json:"-"` // 锁定时间
LastLoginAt *time.Time `gorm:"index" json:"last_login_at"` // 最后登录时间
LastLoginIP string `gorm:"size:45" json:"last_login_ip"` // 最后登录IP
Roles []Role `gorm:"many2many:user_roles;" json:"roles"`
Username string `gorm:"uniqueIndex;size:50" json:"username"`
Password string `gorm:"size:100" json:"-"`
Email string `gorm:"size:100" json:"email"`
Phone string `gorm:"size:20" json:"phone"`
Status int `gorm:"default:1" json:"status"` // 1-正常 2-禁用 3-锁定
PasswordErrorCount int `gorm:"default:0" json:"-"` // 密码错误次数
LockedAt *time.Time `gorm:"index" json:"-"` // 锁定时间
LastLoginAt *time.Time `gorm:"index" json:"last_login_at"` // 最后登录时间
LastLoginIP string `gorm:"size:45" json:"last_login_ip"` // 最后登录IP
PasswordChangedAt *time.Time `gorm:"index" json:"password_changed_at"` // 密码最后修改时间
ForceChangePassword bool `gorm:"default:false" json:"force_change_password"` // 是否强制修改密码
Roles []Role `gorm:"many2many:user_roles;" json:"roles"`
}
// IsLocked 检查用户是否被锁定

8
gofaster/backend/internal/auth/module.go

@ -16,6 +16,7 @@ import ( @@ -16,6 +16,7 @@ import (
type AuthModule struct {
logger *zap.Logger
db *gorm.DB
config *config.Config
}
func init() {
@ -29,6 +30,7 @@ func (m *AuthModule) Name() string { @@ -29,6 +30,7 @@ func (m *AuthModule) Name() string {
func (m *AuthModule) Init(config *config.Config, logger *zap.Logger, db *gorm.DB, redis *database.RedisClient) error {
m.logger = logger
m.db = db
m.config = config
// 运行数据库迁移
if err := migration.RunMigrations(db); err != nil {
@ -45,11 +47,11 @@ func (m *AuthModule) RegisterRoutes(router *gin.RouterGroup) { @@ -45,11 +47,11 @@ func (m *AuthModule) RegisterRoutes(router *gin.RouterGroup) {
m.logger.Error("Database connection not available for auth routes")
return
}
// 注册认证路由
routes.RegisterAuthRoutes(router, m.db, middleware.JWTConfig{
SecretKey: "your-secret-key", // 这里应该从配置中获取
Issuer: "gofaster",
SecretKey: m.config.JWT.Secret, // 从配置中获取JWT密钥
Issuer: m.config.JWT.Issuer, // 从配置中获取JWT发行者
})
}

45
gofaster/backend/internal/auth/repository/password_history_repo.go

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
package repository
import (
"gofaster/internal/auth/model"
"gorm.io/gorm"
)
type PasswordHistoryRepository struct {
db *gorm.DB
}
func NewPasswordHistoryRepository(db *gorm.DB) *PasswordHistoryRepository {
return &PasswordHistoryRepository{db: db}
}
// Create 创建密码历史记录
func (r *PasswordHistoryRepository) Create(history *model.PasswordHistory) error {
return r.db.Create(history).Error
}
// GetRecentPasswords 获取用户最近的N次密码
func (r *PasswordHistoryRepository) GetRecentPasswords(userID uint, limit int) ([]*model.PasswordHistory, error) {
var history []*model.PasswordHistory
err := r.db.Where("user_id = ?", userID).
Order("created_at DESC").
Limit(limit).
Find(&history).Error
return history, err
}
// DeleteOldPasswords 删除用户超过指定天数的旧密码记录
func (r *PasswordHistoryRepository) DeleteOldPasswords(userID uint, days int) error {
return r.db.Where("user_id = ? AND created_at < DATE_SUB(NOW(), INTERVAL ? DAY)", userID, days).
Delete(&model.PasswordHistory{}).Error
}
// GetByUserID 获取用户的所有密码历史
func (r *PasswordHistoryRepository) GetByUserID(userID uint) ([]*model.PasswordHistory, error) {
var history []*model.PasswordHistory
err := r.db.Where("user_id = ?", userID).
Order("created_at DESC").
Find(&history).Error
return history, err
}

71
gofaster/backend/internal/auth/repository/password_policy_repo.go

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
package repository
import (
"gofaster/internal/auth/model"
"gorm.io/gorm"
)
type PasswordPolicyRepository struct {
db *gorm.DB
}
func NewPasswordPolicyRepository(db *gorm.DB) *PasswordPolicyRepository {
return &PasswordPolicyRepository{db: db}
}
// GetActivePolicy 获取当前激活的密码策略
func (r *PasswordPolicyRepository) GetActivePolicy() (*model.PasswordPolicy, error) {
var policy model.PasswordPolicy
err := r.db.Where("is_active = ?", true).First(&policy).Error
if err != nil {
// 如果没有找到策略,返回默认策略
return r.getDefaultPolicy(), nil
}
return &policy, nil
}
// Create 创建密码策略
func (r *PasswordPolicyRepository) Create(policy *model.PasswordPolicy) error {
return r.db.Create(policy).Error
}
// Update 更新密码策略
func (r *PasswordPolicyRepository) Update(policy *model.PasswordPolicy) error {
return r.db.Save(policy).Error
}
// Delete 删除密码策略
func (r *PasswordPolicyRepository) Delete(id uint) error {
return r.db.Delete(&model.PasswordPolicy{}, id).Error
}
// GetAll 获取所有密码策略
func (r *PasswordPolicyRepository) GetAll() ([]*model.PasswordPolicy, error) {
var policies []*model.PasswordPolicy
err := r.db.Find(&policies).Error
return policies, err
}
// GetByID 根据ID获取密码策略
func (r *PasswordPolicyRepository) GetByID(id uint) (*model.PasswordPolicy, error) {
var policy model.PasswordPolicy
err := r.db.First(&policy, id).Error
return &policy, err
}
// getDefaultPolicy 获取默认密码策略
func (r *PasswordPolicyRepository) getDefaultPolicy() *model.PasswordPolicy {
return &model.PasswordPolicy{
Level: 1,
MinLength: 6,
RequireUppercase: false,
RequireLowercase: false,
RequireNumbers: false,
RequireSpecial: false,
MinCharTypes: 1,
ExpirationDays: 30,
PreventReuse: 3,
IsActive: true,
}
}

54
gofaster/backend/internal/auth/repository/password_reset_repo.go

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
package repository
import (
"gofaster/internal/auth/model"
"gorm.io/gorm"
)
type PasswordResetRepository struct {
db *gorm.DB
}
func NewPasswordResetRepository(db *gorm.DB) *PasswordResetRepository {
return &PasswordResetRepository{db: db}
}
// Create 创建密码重置记录
func (r *PasswordResetRepository) Create(reset *model.PasswordReset) error {
return r.db.Create(reset).Error
}
// GetByUserID 获取用户的密码重置记录
func (r *PasswordResetRepository) GetByUserID(userID uint) ([]*model.PasswordReset, error) {
var resets []*model.PasswordReset
err := r.db.Where("user_id = ?", userID).
Order("created_at DESC").
Find(&resets).Error
return resets, err
}
// GetUnusedByUserID 获取用户未使用的密码重置记录
func (r *PasswordResetRepository) GetUnusedByUserID(userID uint) (*model.PasswordReset, error) {
var reset model.PasswordReset
err := r.db.Where("user_id = ? AND is_used = ?", userID, false).
Order("created_at DESC").
First(&reset).Error
return &reset, err
}
// MarkAsUsed 标记密码重置记录为已使用
func (r *PasswordResetRepository) MarkAsUsed(id uint) error {
return r.db.Model(&model.PasswordReset{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"is_used": true,
"used_at": "NOW()",
}).Error
}
// DeleteOldRecords 删除旧的密码重置记录
func (r *PasswordResetRepository) DeleteOldRecords(days int) error {
return r.db.Where("created_at < DATE_SUB(NOW(), INTERVAL ? DAY)", days).
Delete(&model.PasswordReset{}).Error
}

26
gofaster/backend/internal/auth/routes/auth_routes.go

@ -2,10 +2,10 @@ package routes @@ -2,10 +2,10 @@ package routes
import (
"gofaster/internal/auth/controller"
"gofaster/internal/auth/service"
"gofaster/internal/auth/repository"
"gofaster/internal/shared/middleware"
"gofaster/internal/auth/service"
"gofaster/internal/shared/jwt"
"gofaster/internal/shared/middleware"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@ -16,29 +16,43 @@ func RegisterAuthRoutes(r *gin.RouterGroup, db *gorm.DB, jwtConfig middleware.JW @@ -16,29 +16,43 @@ func RegisterAuthRoutes(r *gin.RouterGroup, db *gorm.DB, jwtConfig middleware.JW
// 创建仓储层实例
userRepo := repository.NewUserRepository(db)
captchaRepo := repository.NewCaptchaRepository(db)
passwordPolicyRepo := repository.NewPasswordPolicyRepository(db)
passwordHistoryRepo := repository.NewPasswordHistoryRepository(db)
passwordResetRepo := repository.NewPasswordResetRepository(db)
// 创建JWT管理器
jwtManager := jwt.NewJWTManager(jwtConfig.SecretKey, jwtConfig.Issuer)
// 创建服务层实例
authService := service.NewAuthService(userRepo, captchaRepo, jwtManager)
userService := service.NewUserService(userRepo)
passwordService := service.NewPasswordService(userService, passwordPolicyRepo, passwordHistoryRepo, passwordResetRepo)
// 创建控制器实例
authController := controller.NewAuthController(authService)
passwordController := controller.NewPasswordController(passwordService, userService)
// 认证路由组
auth := r.Group("/auth")
{
// 公开接口(无需认证)
auth.POST("/login", authController.Login) // 用户登录
auth.POST("/login", authController.Login) // 用户登录
auth.GET("/captcha", authController.GenerateCaptcha) // 生成验证码
// 密码策略相关接口(无需认证)
auth.GET("/password-policy", passwordController.GetPasswordPolicy) // 获取密码策略
auth.POST("/validate-password", passwordController.ValidatePassword) // 验证密码强度
// 需要认证的接口
auth.Use(middleware.JWTAuth(jwtConfig))
{
auth.POST("/logout", authController.Logout) // 用户登出
auth.POST("/logout", authController.Logout) // 用户登出
auth.POST("/refresh", authController.RefreshToken) // 刷新令牌
auth.GET("/userinfo", authController.GetUserInfo) // 获取用户信息
auth.GET("/userinfo", authController.GetUserInfo) // 获取用户信息
// 密码管理接口
auth.POST("/change-password", passwordController.ChangePassword) // 修改密码
auth.GET("/password-status", passwordController.CheckPasswordStatus) // 检查密码状态
}
}
}

14
gofaster/backend/internal/auth/service/auth_service.go

@ -113,12 +113,16 @@ func (s *authService) Login(ctx context.Context, req *model.LoginRequest, client @@ -113,12 +113,16 @@ func (s *authService) Login(ctx context.Context, req *model.LoginRequest, client
// 8. 构建响应
userInfo := s.buildUserInfo(userWithRoles)
// 检查是否需要强制修改密码
forceChangePassword := user.ForceChangePassword
return &model.LoginResponse{
Token: token,
TokenType: "Bearer",
ExpiresIn: 24 * 60 * 60, // 24小时,单位秒
RefreshToken: refreshToken,
User: *userInfo,
Token: token,
TokenType: "Bearer",
ExpiresIn: 24 * 60 * 60, // 24小时,单位秒
RefreshToken: refreshToken,
User: *userInfo,
ForceChangePassword: forceChangePassword, // 添加强制修改密码标识
}, nil
}

381
gofaster/backend/internal/auth/service/password_service.go

@ -0,0 +1,381 @@ @@ -0,0 +1,381 @@
package service
import (
"crypto/sha256"
"encoding/hex"
"fmt"
mathrand "math/rand"
"strings"
"time"
"unicode"
"gofaster/internal/auth/model"
"gofaster/internal/auth/repository"
)
type PasswordService struct {
userService *UserService
passwordPolicyRepo *repository.PasswordPolicyRepository
passwordHistoryRepo *repository.PasswordHistoryRepository
passwordResetRepo *repository.PasswordResetRepository
}
func NewPasswordService(
userService *UserService,
passwordPolicyRepo *repository.PasswordPolicyRepository,
passwordHistoryRepo *repository.PasswordHistoryRepository,
passwordResetRepo *repository.PasswordResetRepository,
) *PasswordService {
return &PasswordService{
userService: userService,
passwordPolicyRepo: passwordPolicyRepo,
passwordHistoryRepo: passwordHistoryRepo,
passwordResetRepo: passwordResetRepo,
}
}
// GetPasswordPolicy 获取当前密码策略
func (s *PasswordService) GetPasswordPolicy() (*model.PasswordPolicy, error) {
return s.passwordPolicyRepo.GetActivePolicy()
}
// ValidatePassword 验证密码是否符合策略要求
func (s *PasswordService) ValidatePassword(password string) (*model.PasswordValidationResult, error) {
policy, err := s.GetPasswordPolicy()
if err != nil {
return nil, err
}
result := &model.PasswordValidationResult{
IsValid: true,
Errors: []string{},
}
// 检查长度
if len(password) < policy.MinLength {
result.IsValid = false
result.Errors = append(result.Errors, fmt.Sprintf("密码长度不能少于%d位", policy.MinLength))
}
// 检查字符类型要求
charTypes := 0
hasUppercase := false
hasLowercase := false
hasNumbers := false
hasSpecial := false
for _, char := range password {
switch {
case unicode.IsUpper(char):
hasUppercase = true
case unicode.IsLower(char):
hasLowercase = true
case unicode.IsNumber(char):
hasNumbers = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
if hasUppercase {
charTypes++
}
if hasLowercase {
charTypes++
}
if hasNumbers {
charTypes++
}
if hasSpecial {
charTypes++
}
// 添加调试日志
fmt.Printf("密码验证调试 - 密码: %s, 长度: %d, 字符类型: %d\n", password, len(password), charTypes)
fmt.Printf("字符类型详情 - 大写: %v, 小写: %v, 数字: %v, 特殊: %v\n", hasUppercase, hasLowercase, hasNumbers, hasSpecial)
// 检查具体类型要求
if policy.RequireUppercase && !hasUppercase {
result.IsValid = false
result.Errors = append(result.Errors, "密码必须包含大写字母")
}
if policy.RequireLowercase && !hasLowercase {
result.IsValid = false
result.Errors = append(result.Errors, "密码必须包含小写字母")
}
if policy.RequireNumbers && !hasNumbers {
result.IsValid = false
result.Errors = append(result.Errors, "密码必须包含数字")
}
if policy.RequireSpecial && !hasSpecial {
result.IsValid = false
result.Errors = append(result.Errors, "密码必须包含特殊字符")
}
// 检查最少字符类型数
if charTypes < policy.MinCharTypes {
result.IsValid = false
result.Errors = append(result.Errors, fmt.Sprintf("密码必须包含至少%d种字符类型", policy.MinCharTypes))
}
// 计算密码强度
result.Strength = s.calculatePasswordStrength(password, charTypes)
result.Level = s.calculatePasswordLevel(password, charTypes, policy)
// 检查密码强度是否达到最低要求
if result.Strength < policy.MinRequiredLevel {
result.IsValid = false
result.Errors = append(result.Errors, fmt.Sprintf("密码强度必须达到%d级或以上,当前强度为%d级", policy.MinRequiredLevel, result.Strength))
}
// 添加调试日志
fmt.Printf("密码强度计算结果 - 强度: %d, 等级: %d\n", result.Strength, result.Level)
fmt.Printf("最低要求等级: %d, 是否满足要求: %v\n", policy.MinRequiredLevel, result.Strength >= policy.MinRequiredLevel)
fmt.Printf("返回结果结构: %+v\n", result)
fmt.Printf("JSON序列化测试: Strength=%d, Level=%d\n", result.Strength, result.Level)
return result, nil
}
// calculatePasswordStrength 计算密码强度 (0-5)
func (s *PasswordService) calculatePasswordStrength(password string, charTypes int) int {
// 直接返回密码等级,不再使用百分制
return s.calculatePasswordLevel(password, charTypes, nil)
}
// calculatePasswordLevel 计算密码等级 (0-5)
func (s *PasswordService) calculatePasswordLevel(password string, charTypes int, policy *model.PasswordPolicy) int {
// 从最高等级开始判断,一旦满足条件就返回对应等级
// 5级:长度>=8,字符类型>=4
if len(password) >= 8 && charTypes >= 4 {
return 5
}
// 4级:长度>=8,字符类型>=3
if len(password) >= 8 && charTypes >= 3 {
return 4
}
// 3级:长度>=6,字符类型>=3
if len(password) >= 6 && charTypes >= 3 {
return 3
}
// 2级:长度>=6,字符类型>=2
if len(password) >= 6 && charTypes >= 2 {
return 2
}
// 1级:长度>=6,字符类型>=1
if len(password) >= 6 && charTypes >= 1 {
return 1
}
// 0级:长度>=1,字符类型>=1(任何密码都至少有一种字符类型)
if len(password) >= 1 {
return 0
}
// 如果连0级都不满足,返回-1表示无效
return -1
}
// CheckPasswordReuse 检查密码是否与历史密码重复
func (s *PasswordService) CheckPasswordReuse(userID uint, newPassword string) (bool, error) {
policy, err := s.GetPasswordPolicy()
if err != nil {
return false, err
}
if policy.PreventReuse <= 0 {
return false, nil // 不检查重复
}
// 获取历史密码
history, err := s.passwordHistoryRepo.GetRecentPasswords(userID, policy.PreventReuse)
if err != nil {
return false, err
}
// 检查新密码是否与历史密码重复
newPasswordHash := s.hashPassword(newPassword)
for _, hist := range history {
if hist.Password == newPasswordHash {
return true, nil // 发现重复
}
}
return false, nil
}
// ChangePassword 修改用户密码
func (s *PasswordService) ChangePassword(userID uint, currentPassword, newPassword string) error {
// 获取用户信息
user, err := s.userService.GetByID(userID)
if err != nil {
return err
}
// 验证当前密码
if !s.verifyPassword(currentPassword, user.Password) {
return fmt.Errorf("当前密码不正确")
}
// 验证新密码
validation, err := s.ValidatePassword(newPassword)
if err != nil {
return err
}
if !validation.IsValid {
return fmt.Errorf("新密码不符合要求: %s", strings.Join(validation.Errors, "; "))
}
// 检查是否与历史密码重复
isReused, err := s.CheckPasswordReuse(userID, newPassword)
if err != nil {
return err
}
if isReused {
policy, err := s.GetPasswordPolicy()
if err != nil {
return fmt.Errorf("获取密码策略失败: %w", err)
}
return fmt.Errorf("新密码不能与前%d次使用的密码重复", policy.PreventReuse)
}
// 更新密码
newPasswordHash := s.hashPassword(newPassword)
now := time.Now()
user.Password = newPasswordHash
user.PasswordChangedAt = &now
user.ForceChangePassword = false // 取消强制修改密码
if err := s.userService.Update(user); err != nil {
return err
}
// 记录密码历史
passwordHistory := &model.PasswordHistory{
UserID: userID,
Password: newPasswordHash,
}
if err := s.passwordHistoryRepo.Create(passwordHistory); err != nil {
return err
}
return nil
}
// ResetPassword 重置用户密码
func (s *PasswordService) ResetPassword(userID uint, resetBy uint) (string, error) {
// 生成临时密码
tempPassword := s.generateTempPassword()
// 更新用户密码
user, err := s.userService.GetByID(userID)
if err != nil {
return "", err
}
user.Password = s.hashPassword(tempPassword)
user.ForceChangePassword = true // 标记需要强制修改密码
now := time.Now()
user.PasswordChangedAt = &now
if err := s.userService.Update(user); err != nil {
return "", err
}
// 记录密码重置
passwordReset := &model.PasswordReset{
UserID: userID,
ResetBy: resetBy,
ResetAt: now,
}
if err := s.passwordResetRepo.Create(passwordReset); err != nil {
return "", err
}
return tempPassword, nil
}
// CheckPasswordExpiration 检查密码是否过期
func (s *PasswordService) CheckPasswordExpiration(user *model.User) (bool, error) {
if user.PasswordChangedAt == nil {
return true, nil // 从未修改过密码,视为过期
}
policy, err := s.GetPasswordPolicy()
if err != nil {
return false, err
}
expirationTime := user.PasswordChangedAt.Add(time.Duration(policy.ExpirationDays) * 24 * time.Hour)
return time.Now().After(expirationTime), nil
}
// CheckForceChangePassword 检查是否需要强制修改密码
func (s *PasswordService) CheckForceChangePassword(user *model.User) bool {
return user.ForceChangePassword
}
// generateTempPassword 生成临时密码
func (s *PasswordService) generateTempPassword() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
password := make([]byte, 8)
for i := range password {
password[i] = charset[mathrand.Intn(len(charset))]
}
return string(password)
}
// hashPassword 密码哈希
func (s *PasswordService) hashPassword(password string) string {
hash := sha256.Sum256([]byte(password))
return hex.EncodeToString(hash[:])
}
// verifyPassword 验证密码
func (s *PasswordService) verifyPassword(password, hash string) bool {
return s.hashPassword(password) == hash
}
// TestPasswordStrengthCalculation 测试密码强度计算(仅用于调试)
func (s *PasswordService) TestPasswordStrengthCalculation() {
testPasswords := []string{
"a", // 1位,1种类型 -> 0级
"abc123", // 6位,2种类型 -> 2级
"abc123!", // 7位,3种类型 -> 3级
"abc123!@#", // 9位,3种类型 -> 4级
"abc123!@#$", // 10位,3种类型 -> 4级
"abc123!@#$%", // 11位,3种类型 -> 4级
"abc123!@#$%^", // 12位,3种类型 -> 4级
"abc123!@#$%^&", // 13位,3种类型 -> 4级
"abc123!@#$%^&*", // 14位,3种类型 -> 4级
"abc123!@#$%^&*()", // 15位,3种类型 -> 4级
"abc123!@#$%^&*()_", // 16位,3种类型 -> 4级
"abc123!@#$%^&*()_+", // 17位,3种类型 -> 4级
"abc123!@#$%^&*()_+=", // 18位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{]", // 19位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{}|", // 20位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{}|\\", // 21位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{}|\\/", // 22位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{}|\\/<>", // 23位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{}|\\/<>?", // 24位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{}|\\/<>?\"", // 25位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{}|\\/<>?\"'", // 26位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{}|\\/<>?\"':", // 27位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{}|\\/<>?\"':;", // 28位,3种类型 -> 4级
"abc123!@#$%^&*()_+=[]{}|\\/<>?\"':;.", // 29位,3种类型 -> 4级
}
fmt.Println("=== 新密码强度计算测试(等级制)===")
for _, pwd := range testPasswords {
validation, _ := s.ValidatePassword(pwd)
fmt.Printf("密码: %-30s | 强度等级: %d | 等级: %d | 有效: %v\n",
pwd, validation.Strength, validation.Level, validation.IsValid)
}
fmt.Println("=== 测试结束 ===")
}

12
gofaster/backend/internal/auth/service/user_service.go

@ -33,3 +33,15 @@ func (s *UserService) DeleteUser(ctx context.Context, id uint) error { @@ -33,3 +33,15 @@ func (s *UserService) DeleteUser(ctx context.Context, id uint) error {
func (s *UserService) ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) {
return s.repo.List(ctx, page, pageSize)
}
// GetByID 根据ID获取用户(无context版本,用于密码服务)
func (s *UserService) GetByID(id uint) (*model.User, error) {
ctx := context.Background()
return s.repo.GetByID(ctx, id)
}
// Update 更新用户(无context版本,用于密码服务)
func (s *UserService) Update(user *model.User) error {
ctx := context.Background()
return s.repo.Update(ctx, user)
}

1
gofaster/backend/tmp/build-errors.log

@ -0,0 +1 @@ @@ -0,0 +1 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

BIN
gofaster/backend/tmp/main.exe

Binary file not shown.

15
gofaster/dev-full.ps1

@ -7,8 +7,8 @@ param( @@ -7,8 +7,8 @@ param(
)
# Set console encoding
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF-8
$OutputEncoding = [System.Text.Encoding]::UTF-8
# Set environment variables
$env:VUE_CLI_BABEL_TRANSPILE_MODULES = "false"
@ -88,8 +88,8 @@ try { @@ -88,8 +88,8 @@ try {
Write-Host "Backend dependency check completed" -ForegroundColor Green
Write-Host ""
# Select startup mode
$frontendScript = if ($Debug) { "npm run dev:debug" } elseif ($Watch) { "npm run dev:watch" } else { "npm run dev" }
# Select startup mode - 修复:默认使用watch模式确保热重载
$frontendScript = if ($Debug) { "npm run dev:debug" } elseif ($Watch) { "npm run dev:watch" } else { "npm run dev:watch" }
$backendScript = "air"
# Start services
@ -115,6 +115,7 @@ if (-not $FrontendOnly) { @@ -115,6 +115,7 @@ if (-not $FrontendOnly) {
if (-not $BackendOnly) {
Write-Host "Starting frontend hot reload..." -ForegroundColor Green
Write-Host "Using script: $frontendScript" -ForegroundColor Cyan
$frontendProcess = Start-Process powershell -ArgumentList "-NoExit", "-Command", "cd app; $frontendScript" -WindowStyle Normal -PassThru
Write-Host "Frontend started (PID: $($frontendProcess.Id))" -ForegroundColor Green
}
@ -128,13 +129,14 @@ if (-not $BackendOnly) { @@ -128,13 +129,14 @@ if (-not $BackendOnly) {
}
if (-not $FrontendOnly) {
Write-Host "Frontend: Electron app (auto-reload)" -ForegroundColor Cyan
Write-Host "Frontend: Electron app (auto-reload enabled)" -ForegroundColor Cyan
Write-Host "Note: Frontend now uses watch mode by default for better hot reload" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Usage:" -ForegroundColor Yellow
Write-Host " - Use -Debug parameter to enable detailed debug info" -ForegroundColor White
Write-Host " - Use -Watch parameter to enable file watching" -ForegroundColor White
Write-Host " - Use -Watch parameter to explicitly enable file watching" -ForegroundColor White
Write-Host " - Use -BackendOnly to start only backend" -ForegroundColor White
Write-Host " - Use -FrontendOnly to start only frontend" -ForegroundColor White
Write-Host " - Press Ctrl+C to stop services" -ForegroundColor White
@ -153,5 +155,6 @@ if (-not $BackendOnly -and -not $FrontendOnly) { @@ -153,5 +155,6 @@ if (-not $BackendOnly -and -not $FrontendOnly) {
}
Write-Host "Code changes will automatically trigger rebuilds and reloads!" -ForegroundColor Yellow
Write-Host "Frontend now uses watch mode by default for better hot reload experience" -ForegroundColor Green
Write-Host "Press any key to exit..."
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

Loading…
Cancel
Save