diff --git a/src/main/java/top/baogutang/music/aspect/LoginAspect.java b/src/main/java/top/baogutang/music/aspect/LoginAspect.java index 15c91d7..2fcd292 100644 --- a/src/main/java/top/baogutang/music/aspect/LoginAspect.java +++ b/src/main/java/top/baogutang/music/aspect/LoginAspect.java @@ -2,6 +2,7 @@ package top.baogutang.music.aspect; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; @@ -53,6 +54,11 @@ public class LoginAspect { } } + @After("point()") + public void after() { + UserThreadLocal.remove(); + } + private void checkTokenWithOutRequired() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); @@ -102,7 +108,10 @@ public class LoginAspect { Long userId = userLoginRes.getId(); Object val = redisTemplate.opsForValue().get(KEY_LOGIN_PREFIX + userId); if (Objects.isNull(val)) { - redisTemplate.opsForValue().set(KEY_LOGIN_PREFIX + userId, token); + throw new LoginException("登录已失效"); + } + if (!token.equals(val.toString())) { + throw new LoginException("登录已失效"); } UserThreadLocal.set(userId); } diff --git a/src/main/java/top/baogutang/music/controller/UserController.java b/src/main/java/top/baogutang/music/controller/UserController.java index 552df0e..4361a4a 100644 --- a/src/main/java/top/baogutang/music/controller/UserController.java +++ b/src/main/java/top/baogutang/music/controller/UserController.java @@ -2,10 +2,12 @@ package top.baogutang.music.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; +import top.baogutang.music.annos.Login; import top.baogutang.music.domain.Results; import top.baogutang.music.domain.req.user.UserRegisterAndLoginReq; import top.baogutang.music.domain.res.user.UserLoginRes; import top.baogutang.music.service.IUserService; +import top.baogutang.music.utils.UserThreadLocal; import javax.annotation.Resource; @@ -34,4 +36,11 @@ public class UserController { public Results login(@RequestBody UserRegisterAndLoginReq req) { return Results.ok(userService.login(req)); } + + @Login + @PostMapping("/logout") + public Results logout() { + userService.logout(UserThreadLocal.get()); + return Results.ok(); + } } diff --git a/src/main/java/top/baogutang/music/service/IUserService.java b/src/main/java/top/baogutang/music/service/IUserService.java index 97a89a2..5f016b0 100644 --- a/src/main/java/top/baogutang/music/service/IUserService.java +++ b/src/main/java/top/baogutang/music/service/IUserService.java @@ -17,4 +17,6 @@ public interface IUserService extends IService { void register(UserRegisterAndLoginReq req); UserLoginRes login(UserRegisterAndLoginReq req); + + void logout(Long id); } diff --git a/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java b/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java index 5a23db9..ebca11f 100644 --- a/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java +++ b/src/main/java/top/baogutang/music/service/impl/UserServiceImpl.java @@ -44,26 +44,19 @@ public class UserServiceImpl extends ServiceImpl impleme @Override public void register(UserRegisterAndLoginReq req) { - String lockKey = LOCK_REGISTER_PREFIX + req.getUsername(); - CacheUtil.lockAndExecute(lockKey, - 10L, - TimeUnit.SECONDS, - redisTemplate, - () -> { - // 校验 - UserEntity existsUser = this.validate(req); - if (Objects.nonNull(existsUser)) { - throw new BusinessException("当前用户名已被注册"); - } - // 密码加密 - UserEntity user = new UserEntity(); - user.setUsername(req.getUsername()); - user.setPassword(encryptPassword(req.getPassword())); - user.setRole(UserRole.NORMAL); - user.setLevel(UserLevel.NORMAL); - user.setCreateTime(LocalDateTime.now()); - save(user); - }); + // 校验 + UserEntity existsUser = this.validate(req); + if (Objects.nonNull(existsUser)) { + throw new BusinessException("当前用户名已被注册"); + } + // 密码加密 + UserEntity user = new UserEntity(); + user.setUsername(req.getUsername()); + user.setPassword(encryptPassword(req.getPassword())); + user.setRole(UserRole.NORMAL); + user.setLevel(UserLevel.NORMAL); + user.setCreateTime(LocalDateTime.now()); + save(user); } @Override @@ -88,6 +81,11 @@ public class UserServiceImpl extends ServiceImpl impleme return loginRes; } + @Override + public void logout(Long id) { + redisTemplate.delete(KEY_LOGIN_PREFIX + id); + } + private UserEntity validate(UserRegisterAndLoginReq req) { if (StringUtils.isBlank(req.getUsername())) { throw new BusinessException("用户名不能为空"); diff --git a/src/main/resources/templates/music.html b/src/main/resources/templates/music.html index 745f159..ea08740 100644 --- a/src/main/resources/templates/music.html +++ b/src/main/resources/templates/music.html @@ -32,10 +32,41 @@ justify-content: flex-end; align-items: center; margin-bottom: 20px; + position: relative; } .user-info { font-size: 1rem; color: #333; + cursor: pointer; + position: relative; + } + /* Dropdown Menu Styles */ + .dropdown { + position: absolute; + top: 100%; + right: 0; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 5px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + display: none; + min-width: 150px; + z-index: 1000; + } + .dropdown.visible { + display: block; + } + .dropdown button { + width: 100%; + padding: 10px; + background: none; + border: none; + text-align: left; + cursor: pointer; + font-size: 1rem; + } + .dropdown button:hover { + background-color: #f0f0f0; } .form-group { margin-bottom: 20px; @@ -326,6 +357,9 @@
+

BAOGUTANG-MUSIC

@@ -379,6 +413,19 @@ fetchSearchTypes(); window.addEventListener('scroll', handleScroll); displayUsername(); + + // 点击用户名时,切换下拉菜单的显示状态 + const userInfoDiv = document.getElementById('user-info'); + const dropdownMenu = document.getElementById('dropdown-menu'); + userInfoDiv.addEventListener('click', function(event) { + event.stopPropagation(); // 防止事件冒泡 + dropdownMenu.classList.toggle('visible'); + }); + + // 点击页面其他部分时,隐藏下拉菜单 + document.addEventListener('click', function() { + dropdownMenu.classList.remove('visible'); + }); }); function fetchPlatforms() { @@ -453,7 +500,7 @@ } function startSearch() { - const keywords = document.getElementById('keywords').value; + const keywords = document.getElementById('keywords').value.trim(); if (!selectedPlatformCode || !selectedSearchTypeCode || !keywords) { alert('请填写关键词并选择一个平台和搜索类型'); return; @@ -478,7 +525,7 @@ loading = true; document.getElementById('loading').style.display = 'block'; - const keywords = document.getElementById('keywords').value; + const keywords = document.getElementById('keywords').value.trim(); fetch(`/api/v1/music/search?channel=${selectedPlatformCode}&keywords=${encodeURIComponent(keywords)}&type=${selectedSearchTypeCode}&offset=${offset}&limit=${limit}`, { credentials: 'include' // 确保请求携带cookie }) @@ -560,9 +607,9 @@ row.className = 'song-row'; row.innerHTML = `
-
${song.name}
-
${albumName}
-
${artists}
+
${escapeHTML(song.name)}
+
${escapeHTML(albumName)}
+
${escapeHTML(artists)}
`; const checkbox = row.querySelector('.song-checkbox input[type="checkbox"]'); checkbox.addEventListener('change', () => toggleSongSelection(song.id, checkbox.checked)); @@ -586,9 +633,9 @@ albumItem.className = 'album-item'; albumItem.innerHTML = ` - ${album.name} -
${album.name}
-
${album.type ? album.type : ''}
+ ${escapeHTML(album.name)} +
${escapeHTML(album.name)}
+
${escapeHTML(album.type ? album.type : '')}
`; const albumNameEl = albumItem.querySelector('.album-name'); @@ -617,8 +664,8 @@ artistItem.className = 'artist-item'; artistItem.innerHTML = ` - ${artist.name} -
${artist.name}
+ ${escapeHTML(artist.name)} +
${escapeHTML(artist.name)}
`; artistItem.addEventListener('click', () => { @@ -647,8 +694,8 @@ const playlistItem = document.createElement('div'); playlistItem.className = 'playlist-item'; playlistItem.innerHTML = ` - ${pl.name} -
${pl.name}
+ ${escapeHTML(pl.name)} +
${escapeHTML(pl.name)}
`; playlistItem.addEventListener('click', () => { @@ -863,7 +910,7 @@ if (parts.length === 2) return decodeURIComponent(parts.pop().split(';').shift()); } - // 设置Cookie的值 + // 设置Cookie的值(可选) function setCookie(name, value, days) { const d = new Date(); d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); @@ -879,23 +926,53 @@ }, 1000); } - // 全局的fetch包装函数,用于统一处理响应 - // 可选:如果您希望在所有fetch请求中自动处理code=-200,可以定义一个包装函数 - // 例如: - /* - function customFetch(url, options = {}) { - options.credentials = 'include'; // 确保携带cookie - return fetch(url, options) + // 退出登录函数 + function logout() { + fetch('/api/v1/user/logout', { // 假设后端的退出登录接口是 /api/v1/user/logout + method: 'POST', + credentials: 'include' // 确保请求携带cookie + }) .then(response => response.json()) .then(data => { - if (data.code === -200) { - showMessage(data.msg); - throw new Error(data.msg); + if (data.code === 200) { + // 清除可访问的cookie,如username + deleteCookie('username'); + deleteCookie('token'); + // 由于token是HttpOnly,无法通过JavaScript清除,后端已通过响应头清除 + // 跳转回登录页面 + window.location.href = '/login.html'; + } else { + // 显示错误信息 + showMessage(data.msg || '退出登录失败,请稍后重试'); } - return data; + }) + .catch(error => { + console.error('退出登录请求失败:', error); + showMessage('退出登录请求失败,请稍后重试'); }); } - */ + + // 删除Cookie的函数 + function deleteCookie(name) { + document.cookie = name + '=; Max-Age=0; path=/'; + } + + // 辅助函数:转义HTML,以防止XSS攻击 + function escapeHTML(str) { + if (!str) return ''; + return str.replace(/[&<>"'`=\/]/g, function(s) { + return ({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/', + '`': '`', + '=': '=' + })[s]; + }); + }