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

View File

@ -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<UserLoginRes> login(@RequestBody UserRegisterAndLoginReq 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);
UserLoginRes login(UserRegisterAndLoginReq req);
void logout(Long id);
}

View File

@ -44,26 +44,19 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> 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<UserMapper, UserEntity> 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("用户名不能为空");

View File

@ -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 @@
<div class="container">
<div class="header">
<div class="user-info" id="user-info">欢迎, 未登录</div>
<div class="dropdown" id="dropdown-menu">
<button onclick="logout()">退出登录</button>
</div>
</div>
<h1>BAOGUTANG-MUSIC</h1>
@ -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 = `
<div class="song-checkbox"><input type="checkbox"></div>
<div class="song-name">${song.name}</div>
<div class="song-album">${albumName}</div>
<div class="song-artists">${artists}</div>
<div class="song-name">${escapeHTML(song.name)}</div>
<div class="song-album">${escapeHTML(albumName)}</div>
<div class="song-artists">${escapeHTML(artists)}</div>
`;
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 = `
<img src="${album.blurPicUrl}" class="album-cover" alt="${album.name}" />
<div class="album-name" data-id="${album.id}">${album.name}</div>
<div class="album-type">${album.type ? album.type : ''}</div>
<img src="${escapeHTML(album.blurPicUrl)}" class="album-cover" alt="${escapeHTML(album.name)}" />
<div class="album-name" data-id="${album.id}">${escapeHTML(album.name)}</div>
<div class="album-type">${escapeHTML(album.type ? album.type : '')}</div>
`;
const albumNameEl = albumItem.querySelector('.album-name');
@ -617,8 +664,8 @@
artistItem.className = 'artist-item';
artistItem.innerHTML = `
<img src="${artist.picUrl}" class="artist-pic" alt="${artist.name}" />
<div class="artist-name">${artist.name}</div>
<img src="${escapeHTML(artist.picUrl)}" class="artist-pic" alt="${escapeHTML(artist.name)}" />
<div class="artist-name">${escapeHTML(artist.name)}</div>
`;
artistItem.addEventListener('click', () => {
@ -647,8 +694,8 @@
const playlistItem = document.createElement('div');
playlistItem.className = 'playlist-item';
playlistItem.innerHTML = `
<img src="${pl.coverImgUrl}" class="playlist-cover" alt="${pl.name}" />
<div class="playlist-name" data-id="${pl.id}">${pl.name}</div>
<img src="${escapeHTML(pl.coverImgUrl)}" class="playlist-cover" alt="${escapeHTML(pl.name)}" />
<div class="playlist-name" data-id="${pl.id}">${escapeHTML(pl.name)}</div>
`;
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 ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
'`': '&#x60;',
'=': '&#x3D;'
})[s];
});
}
</script>
</body>
</html>