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 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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("用户名不能为空");
|
||||||
|
|||||||
@ -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 ({
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
'/': '/',
|
||||||
|
'`': '`',
|
||||||
|
'=': '='
|
||||||
|
})[s];
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user