logout
This commit is contained in:
parent
c2f59d9f07
commit
7ed681b078
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,4 +17,6 @@ public interface IUserService extends IService<UserEntity> {
|
||||
void register(UserRegisterAndLoginReq req);
|
||||
|
||||
UserLoginRes login(UserRegisterAndLoginReq req);
|
||||
|
||||
void logout(Long id);
|
||||
}
|
||||
|
||||
@ -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("用户名不能为空");
|
||||
|
||||
@ -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 ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
})[s];
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user