"SpringBoot+Vue+部署(7.注销登录)"这个标题描述的是一个典型的前后端分离项目中用户登出功能的实现方案。作为用户认证流程的关键环节,登出功能看似简单,实则涉及前后端会话管理、令牌失效、安全防护等多个技术要点。在实际项目中,我曾遇到过不少团队在这个"小功能"上栽跟头——有的导致会话残留引发安全问题,有的出现前端缓存未清理影响用户体验。
这个方案采用了目前主流的:
根据项目安全要求,通常有两种实现方式:
| 方案类型 | 存储位置 | 失效机制 | 适用场景 |
|---|---|---|---|
| JWT令牌 | 客户端localStorage | 依赖黑名单或短有效期 | 无状态服务、跨域场景 |
| Session会话 | 服务端内存/Redis | 服务端主动销毁 | 需要严格会话控制的系统 |
提示:金融类系统建议使用Session方案,互联网应用可考虑JWT。我们项目采用JWT方案进行演示。
首先在SecurityConfig中配置登出端点:
java复制@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
.logoutUrl("/api/auth/logout") // 自定义登出端点
.addLogoutHandler(new TokenLogoutHandler(tokenService)) // 自定义处理器
.logoutSuccessHandler((req, res, auth) -> {
res.setContentType("application/json;charset=UTF-8");
res.getWriter().write(JSON.toJSONString(Result.success()));
})
.and()
// 其他配置...
}
创建TokenLogoutHandler处理令牌失效:
java复制public class TokenLogoutHandler implements LogoutHandler {
private final TokenService tokenService;
@Override
public void logout(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
String token = request.getHeader("Authorization");
if (StringUtils.isNotBlank(token) && token.startsWith("Bearer ")) {
token = token.substring(7);
tokenService.invalidateToken(token); // 将令牌加入黑名单
}
}
}
TokenService的关键实现:
java复制public void invalidateToken(String token) {
Date expiration = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody()
.getExpiration();
long ttl = expiration.getTime() - System.currentTimeMillis();
if (ttl > 0) {
redisTemplate.opsForValue().set(
"token:blacklist:" + token,
"1",
ttl,
TimeUnit.MILLISECONDS
);
}
}
在store中管理登录状态:
javascript复制const store = new Vuex.Store({
state: {
user: null,
token: null
},
mutations: {
LOGOUT(state) {
state.user = null
state.token = null
localStorage.removeItem('token')
router.push('/login')
}
},
actions: {
async logout({ commit }) {
try {
await axios.post('/api/auth/logout')
commit('LOGOUT')
} catch (err) {
commit('LOGOUT') // 即使后端失败也清除前端状态
}
}
}
})
设置请求拦截器自动携带token:
javascript复制axios.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器处理401未授权
axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
store.commit('LOGOUT')
}
return Promise.reject(error)
}
)
确保前后端路由正确转发:
nginx复制server {
listen 80;
server_name yourdomain.com;
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
当使用JWT时需设置CORS:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://yourdomain.com")
.allowedMethods("*")
.allowedHeaders("*")
.exposedHeaders("Authorization") // 重要!
.allowCredentials(false)
.maxAge(3600);
}
}
即使使用JWT也建议添加CSRF保护:
java复制http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
记录登出日志便于追踪:
java复制@PostMapping("/logout")
@AuditLog(operation = "用户登出")
public Result logout() {
// ...
}
现象:登出后旧令牌仍能使用一段时间
解决方案:
现象:登出后仍能访问受限页面
解决方案:
javascript复制router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !store.state.token) {
next('/login')
} else {
next()
}
})
javascript复制axios.defaults.headers.common['Cache-Control'] = 'no-cache'
Redis集群部署:当用户量大时,单节点Redis可能成为瓶颈
本地缓存配合:对于高频访问的令牌验证,可采用二级缓存策略:
java复制@Cacheable(value = "token", key = "#token", unless = "#result == null")
public Boolean validateToken(String token) {
// 先查本地缓存,再查Redis
}
sql复制-- 在Redis中执行
EVAL "local keys = redis.call('keys', 'user:token:*')
for _,k in ipairs(keys) do
redis.call('del', k)
end" 0
我在实际项目中发现,完整的登出流程需要考虑至少六个关键点:
一个健壮的登出功能,应该像机场安检一样——既要确保人员确实离开限制区(会话终止),又要防止有人偷偷溜回来(令牌失效),同时还要记录完整的出入记录(审计日志)。这需要前后端开发者的紧密配合和细致测试。