feat(login): 添加记住我登录功能

前端新增记住我勾选框,登录页挂载时自动恢复已保存的用户名,勾选时将用户名存入本地存储并在登录请求中携带对应参数;后端新增rememberMe参数处理逻辑,配置7天免登录的JWT有效期,重载JWT工具类以支持自定义令牌过期时间
This commit is contained in:
wanglongjie 2026-08-03 16:18:24 +08:00
parent f400a93bdd
commit 058e1fb8a7
6 changed files with 56 additions and 4 deletions

View File

@ -24,4 +24,9 @@ public class LoginDTO implements Serializable {
*/ */
@NotBlank(message = "密码不能为空") @NotBlank(message = "密码不能为空")
private String password; private String password;
/**
* 记住我true 时签发 7 天有效期的 token
*/
private Boolean rememberMe = false;
} }

View File

@ -46,6 +46,10 @@ public class UserServiceImpl implements UserService {
throw new BusinessException("用户名或密码错误"); throw new BusinessException("用户名或密码错误");
} }
if (Boolean.TRUE.equals(loginDTO.getRememberMe())) {
return jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole(),
user.getDealerId(), jwtUtil.getRememberExpiration());
}
return jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole(), user.getDealerId()); return jwtUtil.generateToken(user.getId(), user.getUsername(), user.getRole(), user.getDealerId());
} }

View File

@ -25,24 +25,45 @@ public class JwtUtil {
@Value("${jwt.expiration}") @Value("${jwt.expiration}")
private Long expiration; private Long expiration;
@Value("${jwt.remember-expiration:604800000}")
private Long rememberExpiration;
public Long getRememberExpiration() {
return rememberExpiration;
}
/** /**
* 生成 Token * 生成 Token
*/ */
public String generateToken(Long userId, String username, Integer role, Long dealerId) { public String generateToken(Long userId, String username, Integer role, Long dealerId) {
return generateToken(userId, username, role, dealerId, expiration);
}
/**
* 生成指定有效期的 Token用于"记住我"延长有效期
*/
public String generateToken(Long userId, String username, Integer role, Long dealerId, long expirationMillis) {
Map<String, Object> claims = new HashMap<>(); Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId); claims.put("userId", userId);
claims.put("username", username); claims.put("username", username);
claims.put("role", role); claims.put("role", role);
claims.put("dealerId", dealerId); claims.put("dealerId", dealerId);
return generateToken(claims); return generateToken(claims, expirationMillis);
} }
/** /**
* 生成 Token * 生成 Token
*/ */
public String generateToken(Map<String, Object> claims) { public String generateToken(Map<String, Object> claims) {
return generateToken(claims, expiration);
}
/**
* 生成指定有效期的 Token
*/
public String generateToken(Map<String, Object> claims, long expirationMillis) {
Date now = new Date(); Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration); Date expiryDate = new Date(now.getTime() + expirationMillis);
return Jwts.builder() return Jwts.builder()
.setClaims(claims) .setClaims(claims)

View File

@ -58,6 +58,7 @@ jwt:
# 生产环境请使用环境变量或密钥管理系统存储此密钥 # 生产环境请使用环境变量或密钥管理系统存储此密钥
secret: by-crm-jwt-secret-key-2024-hs512-requires-at-least-64-bytes-for-secure-signing-please-change-in-production-environment secret: by-crm-jwt-secret-key-2024-hs512-requires-at-least-64-bytes-for-secure-signing-please-change-in-production-environment
expiration: 86400000 # 24小时单位毫秒 expiration: 86400000 # 24小时单位毫秒
remember-expiration: 604800000 # 记住我7天免登录单位毫秒
# CRM 业务配置 # CRM 业务配置
crm: crm:

View File

@ -14,6 +14,7 @@ export interface User {
export interface LoginRequest { export interface LoginRequest {
username: string username: string
password: string password: string
rememberMe?: boolean
} }
export interface LoginResponse { export interface LoginResponse {

View File

@ -21,6 +21,9 @@
@keyup.enter="handleLogin" @keyup.enter="handleLogin"
/> />
</el-form-item> </el-form-item>
<el-form-item>
<el-checkbox v-model="remember">记住我7天免登录</el-checkbox>
</el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" size="large" :loading="loading" @click="handleLogin" class="login-btn"> <el-button type="primary" size="large" :loading="loading" @click="handleLogin" class="login-btn">
登录 登录
@ -32,7 +35,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus' import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
@ -49,6 +52,16 @@ const loginForm = reactive({
password: '' password: ''
}) })
const remember = ref(false)
onMounted(() => {
const savedUsername = localStorage.getItem('remember-username')
if (savedUsername) {
loginForm.username = savedUsername
remember.value = true
}
})
const rules: FormRules = { const rules: FormRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }] password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
@ -64,8 +77,15 @@ const handleLogin = async () => {
const hashedPassword = await hashPassword(loginForm.password) const hashedPassword = await hashPassword(loginForm.password)
await userStore.login({ await userStore.login({
username: loginForm.username, username: loginForm.username,
password: hashedPassword password: hashedPassword,
rememberMe: remember.value
}) })
// ""
if (remember.value) {
localStorage.setItem('remember-username', loginForm.username)
} else {
localStorage.removeItem('remember-username')
}
ElMessage.success('登录成功') ElMessage.success('登录成功')
router.push('/') router.push('/')
} catch (error) { } catch (error) {