This commit is contained in:
N1KO 2024-12-13 18:32:31 +08:00
parent c2f59d9f07
commit 7ed681b078
5 changed files with 141 additions and 46 deletions

View File

@ -2,6 +2,7 @@ package top.baogutang.music.aspect;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
@ -53,6 +54,11 @@ public class LoginAspect {
} }
} }
@After("point()")
public void after() {
UserThreadLocal.remove();
}
private void checkTokenWithOutRequired() { private void checkTokenWithOutRequired() {
ServletRequestAttributes requestAttributes = ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
@ -102,7 +108,10 @@ public class LoginAspect {
Long userId = userLoginRes.getId(); Long userId = userLoginRes.getId();
Object val = redisTemplate.opsForValue().get(KEY_LOGIN_PREFIX + userId); Object val = redisTemplate.opsForValue().get(KEY_LOGIN_PREFIX + userId);
if (Objects.isNull(val)) { 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); UserThreadLocal.set(userId);
} }

View File

@ -2,10 +2,12 @@ package top.baogutang.music.controller;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import top.baogutang.music.annos.Login;
import top.baogutang.music.domain.Results; import top.baogutang.music.domain.Results;
import top.baogutang.music.domain.req.user.UserRegisterAndLoginReq; import top.baogutang.music.domain.req.user.UserRegisterAndLoginReq;
import top.baogutang.music.domain.res.user.UserLoginRes; import top.baogutang.music.domain.res.user.UserLoginRes;
import top.baogutang.music.service.IUserService; import top.baogutang.music.service.IUserService;
import top.baogutang.music.utils.UserThreadLocal;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -34,4 +36,11 @@ public class UserController {
public Results<UserLoginRes> login(@RequestBody UserRegisterAndLoginReq req) { public Results<UserLoginRes> login(@RequestBody UserRegisterAndLoginReq req) {
return Results.ok(userService.login(req)); return Results.ok(userService.login(req));
} }
@Login
@PostMapping("/logout")
public Results<Void> logout() {
userService.logout(UserThreadLocal.get());
return Results.ok();
}
} }

View File

@ -17,4 +17,6 @@ public interface IUserService extends IService<UserEntity> {
void register(UserRegisterAndLoginReq req); void register(UserRegisterAndLoginReq req);
UserLoginRes login(UserRegisterAndLoginReq req); UserLoginRes login(UserRegisterAndLoginReq req);
void logout(Long id);
} }

View File

@ -44,26 +44,19 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
@Override @Override
public void register(UserRegisterAndLoginReq req) { public void register(UserRegisterAndLoginReq req) {
String lockKey = LOCK_REGISTER_PREFIX + req.getUsername(); // 校验
CacheUtil.lockAndExecute(lockKey, UserEntity existsUser = this.validate(req);
10L, if (Objects.nonNull(existsUser)) {
TimeUnit.SECONDS, throw new BusinessException("当前用户名已被注册");
redisTemplate, }
() -> { // 密码加密
// 校验 UserEntity user = new UserEntity();
UserEntity existsUser = this.validate(req); user.setUsername(req.getUsername());
if (Objects.nonNull(existsUser)) { user.setPassword(encryptPassword(req.getPassword()));
throw new BusinessException("当前用户名已被注册"); user.setRole(UserRole.NORMAL);
} user.setLevel(UserLevel.NORMAL);
// 密码加密 user.setCreateTime(LocalDateTime.now());
UserEntity user = new UserEntity(); save(user);
user.setUsername(req.getUsername());
user.setPassword(encryptPassword(req.getPassword()));
user.setRole(UserRole.NORMAL);
user.setLevel(UserLevel.NORMAL);
user.setCreateTime(LocalDateTime.now());
save(user);
});
} }
@Override @Override
@ -88,6 +81,11 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> impleme
return loginRes; return loginRes;
} }
@Override
public void logout(Long id) {
redisTemplate.delete(KEY_LOGIN_PREFIX + id);
}
private UserEntity validate(UserRegisterAndLoginReq req) { private UserEntity validate(UserRegisterAndLoginReq req) {
if (StringUtils.isBlank(req.getUsername())) { if (StringUtils.isBlank(req.getUsername())) {
throw new BusinessException("用户名不能为空"); throw new BusinessException("用户名不能为空");

View File

@ -32,10 +32,41 @@
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 20px;
position: relative;
} }
.user-info { .user-info {
font-size: 1rem; font-size: 1rem;
color: #333; 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 { .form-group {
margin-bottom: 20px; margin-bottom: 20px;
@ -326,6 +357,9 @@
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<div class="user-info" id="user-info">欢迎, 未登录</div> <div class="user-info" id="user-info">欢迎, 未登录</div>
<div class="dropdown" id="dropdown-menu">
<button onclick="logout()">退出登录</button>
</div>
</div> </div>
<h1>BAOGUTANG-MUSIC</h1> <h1>BAOGUTANG-MUSIC</h1>
@ -379,6 +413,19 @@
fetchSearchTypes(); fetchSearchTypes();
window.addEventListener('scroll', handleScroll); window.addEventListener('scroll', handleScroll);
displayUsername(); 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() { function fetchPlatforms() {
@ -453,7 +500,7 @@
} }
function startSearch() { function startSearch() {
const keywords = document.getElementById('keywords').value; const keywords = document.getElementById('keywords').value.trim();
if (!selectedPlatformCode || !selectedSearchTypeCode || !keywords) { if (!selectedPlatformCode || !selectedSearchTypeCode || !keywords) {
alert('请填写关键词并选择一个平台和搜索类型'); alert('请填写关键词并选择一个平台和搜索类型');
return; return;
@ -478,7 +525,7 @@
loading = true; loading = true;
document.getElementById('loading').style.display = 'block'; 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}`, { fetch(`/api/v1/music/search?channel=${selectedPlatformCode}&keywords=${encodeURIComponent(keywords)}&type=${selectedSearchTypeCode}&offset=${offset}&limit=${limit}`, {
credentials: 'include' // 确保请求携带cookie credentials: 'include' // 确保请求携带cookie
}) })
@ -560,9 +607,9 @@
row.className = 'song-row'; row.className = 'song-row';
row.innerHTML = ` row.innerHTML = `
<div class="song-checkbox"><input type="checkbox"></div> <div class="song-checkbox"><input type="checkbox"></div>
<div class="song-name">${song.name}</div> <div class="song-name">${escapeHTML(song.name)}</div>
<div class="song-album">${albumName}</div> <div class="song-album">${escapeHTML(albumName)}</div>
<div class="song-artists">${artists}</div> <div class="song-artists">${escapeHTML(artists)}</div>
`; `;
const checkbox = row.querySelector('.song-checkbox input[type="checkbox"]'); const checkbox = row.querySelector('.song-checkbox input[type="checkbox"]');
checkbox.addEventListener('change', () => toggleSongSelection(song.id, checkbox.checked)); checkbox.addEventListener('change', () => toggleSongSelection(song.id, checkbox.checked));
@ -586,9 +633,9 @@
albumItem.className = 'album-item'; albumItem.className = 'album-item';
albumItem.innerHTML = ` albumItem.innerHTML = `
<img src="${album.blurPicUrl}" class="album-cover" alt="${album.name}" /> <img src="${escapeHTML(album.blurPicUrl)}" class="album-cover" alt="${escapeHTML(album.name)}" />
<div class="album-name" data-id="${album.id}">${album.name}</div> <div class="album-name" data-id="${album.id}">${escapeHTML(album.name)}</div>
<div class="album-type">${album.type ? album.type : ''}</div> <div class="album-type">${escapeHTML(album.type ? album.type : '')}</div>
`; `;
const albumNameEl = albumItem.querySelector('.album-name'); const albumNameEl = albumItem.querySelector('.album-name');
@ -617,8 +664,8 @@
artistItem.className = 'artist-item'; artistItem.className = 'artist-item';
artistItem.innerHTML = ` artistItem.innerHTML = `
<img src="${artist.picUrl}" class="artist-pic" alt="${artist.name}" /> <img src="${escapeHTML(artist.picUrl)}" class="artist-pic" alt="${escapeHTML(artist.name)}" />
<div class="artist-name">${artist.name}</div> <div class="artist-name">${escapeHTML(artist.name)}</div>
`; `;
artistItem.addEventListener('click', () => { artistItem.addEventListener('click', () => {
@ -647,8 +694,8 @@
const playlistItem = document.createElement('div'); const playlistItem = document.createElement('div');
playlistItem.className = 'playlist-item'; playlistItem.className = 'playlist-item';
playlistItem.innerHTML = ` playlistItem.innerHTML = `
<img src="${pl.coverImgUrl}" class="playlist-cover" alt="${pl.name}" /> <img src="${escapeHTML(pl.coverImgUrl)}" class="playlist-cover" alt="${escapeHTML(pl.name)}" />
<div class="playlist-name" data-id="${pl.id}">${pl.name}</div> <div class="playlist-name" data-id="${pl.id}">${escapeHTML(pl.name)}</div>
`; `;
playlistItem.addEventListener('click', () => { playlistItem.addEventListener('click', () => {
@ -863,7 +910,7 @@
if (parts.length === 2) return decodeURIComponent(parts.pop().split(';').shift()); if (parts.length === 2) return decodeURIComponent(parts.pop().split(';').shift());
} }
// 设置Cookie的值 // 设置Cookie的值(可选)
function setCookie(name, value, days) { function setCookie(name, value, days) {
const d = new Date(); const d = new Date();
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
@ -879,23 +926,53 @@
}, 1000); }, 1000);
} }
// 全局的fetch包装函数用于统一处理响应 // 退出登录函数
// 可选如果您希望在所有fetch请求中自动处理code=-200可以定义一个包装函数 function logout() {
// 例如: fetch('/api/v1/user/logout', { // 假设后端的退出登录接口是 /api/v1/user/logout
/* method: 'POST',
function customFetch(url, options = {}) { credentials: 'include' // 确保请求携带cookie
options.credentials = 'include'; // 确保携带cookie })
return fetch(url, options)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.code === -200) { if (data.code === 200) {
showMessage(data.msg); // 清除可访问的cookie如username
throw new Error(data.msg); 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 ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
})[s];
});
}
</script> </script>
</body> </body>
</html> </html>