如何使用jwt+redis實現(xiàn)單點登錄
首先理一下登錄流程
前端登錄—>賬號密碼驗證—>成功返回token—>后續(xù)請求攜帶token---->用戶異地登錄---->本地用戶token不能用,不能再訪問需要攜帶token的網(wǎng)頁
jwt工具類
package com.nageoffer.shortlink.admin.util;
import cn.hutool.core.util.ObjectUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.nageoffer.shortlink.admin.common.constant.UserConstant;
import com.nageoffer.shortlink.admin.common.convention.exception.ClientException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
// 默認過期時間 1 小時
private static final long EXPIRE_TIME = 60 * 60 * 1000L;
// 簽名密鑰
private static final String SECRET = "short-link-secret-key";
/**
* 生成 token
*
* @param claims 自定義的載荷
* @return JWT token
*/
public static String generateToken(Map<String, Object> claims) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + EXPIRE_TIME);
return JWT.create()
.withIssuedAt(now) // 簽發(fā)時間
.withExpiresAt(expireDate) // 過期時間
.withPayload(claims) // 自定義載荷
.sign(Algorithm.HMAC256(SECRET)); // 簽名算法
}
/**
* 驗證 token 是否有效
*
* @param token 待驗證的 JWT
* @return 是否有效
*/
public static boolean verifyToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
return true;
} catch (JWTVerificationException e) {
return false;
}
}
/**
* 獲取 token 中的某個 claim
*
* @param token JWT token
* @param key claim 的 key
* @return claim 對應的值
*/
public static String getClaim(String token, String key) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(key).asString();
} catch (JWTDecodeException e) {
return null;
}
}
/**
* 獲取 token 的過期時間
*
* @param token JWT token
* @return 過期時間
*/
public static Date getExpireAt(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt();
} catch (JWTDecodeException e) {
return null;
}
}
public static String getCurrentUser() {
String username = null;
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader(UserConstant.TOKEN);
if (ObjectUtil.isNotEmpty(token)) {
username = JWT.decode(token).getClaim("username").asString();
}
} catch (Exception e) {
throw new ClientException("獲取當前用戶信息出錯");
}
return username;
}
}JWT攔截器
每次更新token的過期時間
package com.nageoffer.shortlink.admin.config;
import com.nageoffer.shortlink.admin.common.constant.UserConstant;
import com.nageoffer.shortlink.admin.common.convention.exception.ClientException;
import com.nageoffer.shortlink.admin.util.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.concurrent.TimeUnit;
import static com.nageoffer.shortlink.admin.common.constant.RedisCacheConstant.USER_LOGIN_KEY;
/**
* jwt攔截器
*/
@Component
@RequiredArgsConstructor
public class JwtInterceptor implements HandlerInterceptor {
private final StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader(UserConstant.TOKEN);
if (token == null || !JwtUtil.verifyToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
throw new ClientException("token無效或已過期");
}
// 從 token 獲取用戶名
String username = JwtUtil.getClaim(token,"username");
// 可選:檢查 Redis 是否存在 token,實現(xiàn)單點登錄
String redisToken = stringRedisTemplate.opsForValue().get(USER_LOGIN_KEY + username);
if (redisToken == null || !redisToken.equals(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
throw new ClientException("您已經(jīng)在其他地方登錄,請重新登錄");
}
// 可選:刷新 Redis token 過期時間
String redisKey = USER_LOGIN_KEY + username;
stringRedisTemplate.expire(redisKey, 30, TimeUnit.MINUTES);
// 將用戶名放入請求上下文,供 Controller 使用
request.setAttribute("username", username);
return true;
}
}注冊JWT攔截器,并選擇放行哪些接口
package com.nageoffer.shortlink.admin.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/**") // 攔截所有請求
.excludePathPatterns(
"/api/short-link/admin/v1/user/login" // 登錄接口不攔截
);
}
}登錄方法
首先判斷賬號密碼,正確以后,判斷redis是否有這個用戶,如果有,說明已經(jīng)登錄過了,把原來的token刪除了。
接下來統(tǒng)一生成新token,存入redis
@Override
public UserLoginRespDTO login(UserLoginReqDTO requestParam) {
LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
.eq(UserDO::getUsername, requestParam.getUsername())
.eq(UserDO::getPassword, requestParam.getPassword())
.eq(UserDO::getDelFlag, 0);
UserDO userDO = baseMapper.selectOne(queryWrapper);
if (userDO == null) {
throw new ClientException("用戶不存在");
}
String redisKey = USER_LOGIN_KEY + requestParam.getUsername();
// 檢查 Redis 是否已存在 token,實現(xiàn)單點登錄
String existingToken = stringRedisTemplate.opsForValue().get(redisKey);
if (existingToken != null) {
stringRedisTemplate.delete(redisKey);
}
// 自定義載荷,如何還需要添加別的信息,可以繼續(xù)添加,如用戶ID
Map<String, Object> claims = new HashMap<>();
claims.put("username", requestParam.getUsername());
// 生成新 token
String token = JwtUtil.generateToken(claims);
// 存入 Redis,實現(xiàn)單點登錄
stringRedisTemplate.opsForValue().set(redisKey, token, 30, TimeUnit.MINUTES);
return new UserLoginRespDTO(token);
}退出登錄
在redis中刪除用戶即可
@Override
public void logout(String username) {
if (checkLogin(username)) {
stringRedisTemplate.delete(USER_LOGIN_KEY + username);
return;
}
throw new ClientException("用戶Token不存在或用戶未登錄");
}補充:基于Redis的單點登錄實現(xiàn)方案
一.單點登錄流程分析
1.客戶端統(tǒng)一攔截過濾器

流程分析:
1.在對需要進行授權登錄的url連接,訪問統(tǒng)一被上放的過濾器流程所攔截
2.進入過濾器,首先判斷當前域名下是否有cookie,如果存在,則判斷該cookie的值作為key在redis中是否存在,如果存在則進入進行,不存在則進入服務端的登錄授權界面.
3.如果當前域名下的cookie值不存在,則判斷當前鏈接是否帶有一個參數(shù)值為ticket的值,如果存在,則設置該值為cookie,然后重定向到本地址.
2.服務端流程

分析:
1.登錄接口:用戶名密碼驗證成功后,設置cookie,設置redis緩存,登錄成功
2.登錄頁面:先判斷是否有cookie以及redis緩存,如果滿足,則重定向返回backurl以及附加參數(shù)ticket值為當前的cookie,如果不滿足,則進入登錄界面
3.退出登錄:刪除redis緩存,刪除cookie
4.為了防止別人拿著帶著ticket的鏈接去其他瀏覽器進行偽造登錄,則可以對其ticket存入redis中,客戶端對其進行一次性消費.
二.客戶端與服務端cookie同步
1.js腳本更新
可通過html加入js腳本訪問服務端,服務端再對其cookie重置時長
2.增長服務端cookie時長
可通過增長服務端cookie時長,如果客戶端退出了,redis緩存將清理,再次訪問服務端登錄接口,服務端查詢不到redis緩存,則對其cookie進行刪除.但是這種不能算是完全意義上的同步.
三.關鍵代碼
1.客戶端過濾器filter
String cookieName = CookieUtil.getCookie(Constans.COOKIE_SSO, request);
if(cookieName!=null&& !cookieName.equals("")){
//驗證cookie的有效性
User user = tokenManagerInter.getUserInfo(cookieName);
if(user!=null){
//驗證成功,繼續(xù)執(zhí)行
//TODO 重置cookie時間,緩存時間
filterChain.doFilter(servletRequest,servletResponse);
}else {
//驗證失敗,刪除無效cookie
CookieUtil.deleteCookie(Constans.COOKIE_SSO,response, "/");
//返回登錄界面
String qstr = makeQueryString(request); // 將所有請求參數(shù)重新拼接成queryString
String backUrl=request.getRequestURL() + qstr; // 回調url
String location = "127.0.0.1/login?backUrl=" + URLEncoder.encode(backUrl, "utf-8");
response.sendRedirect(location);
}
}else {
String vtParam = pasreVtParam(request); // 從請求中
if (vtParam == null) {
// 請求中中沒有vtParam,引導瀏覽器重定向到服務端執(zhí)行登錄校驗
//返回登錄界面
String qstr = makeQueryString(request); // 將所有請求參數(shù)重新拼接成queryString
String backUrl=request.getRequestURL() + qstr; // 回調url
String location = "127.0.0.1/login?backUrl=" + URLEncoder.encode(backUrl, "utf-8");
response.sendRedirect(location);
} else if (vtParam.length() == 0) {
// 有vtParam,但內容為空,表示到服務端loginCheck后,得到的結果是未登錄
response.sendError(403);
} else {
// 讓瀏覽器向本鏈接發(fā)起一次重定向,此過程去除vtParam,將vt寫入cookie
redirectToSelf(vtParam);
}
}2.服務端登錄界面
先判斷當前是否有cookie,如果沒有,則進入登錄界面,如果存在,則判斷是否有redis緩存存在,如果不存在,則進入登錄界面,如果存在,則重定向到backurl中去,同時帶著ticket返回客戶端
@Override
public String login(String backUrl, ModelMap map) {
String cookie = CookieUtil.getCookie(Constans.COOKIE_SSO, request);
//判斷有無cookie
if(cookie==null){
//無cookie,進入登錄界面
map.put("backUrl", backUrl);
return "login";
}else {
//當前存在cookie
if(tokenManagerInter.checkToken(cookie)){
//緩存存在
if (backUrl != null) {
try {
response.sendRedirect(StringUtils.appendUrlParameter(backUrl, Constans.PARAGRAM_VT, cookie));
} catch (IOException e) {
e.printStackTrace();
logger.error("登錄重定向失敗:"+e);
}
return null;
} else {
AccountUser user = tokenManagerInter.getUserInfo(cookie);
if(user!=null){
map.put("user", user);
}
map.put("vt", cookie);
return "loginSuccess";
}
}else {
//緩存不存在,刪除cookie,返回登錄界面
CookieUtil.deleteCookie(Constans.COOKIE_SSO,response,"/");
map.put("backUrl", backUrl);
return "login";
}
}
}3.服務端登錄接口
驗證完用戶名密碼后設置cookie,增加redis緩存,重定向到backurl去.
//設置cookie
String uuid = UUID.randomUUID().toString().replaceAll("-","");
Cookie cookie = new Cookie(Constans.COOKIE_SSO, uuid);
cookie.setMaxAge(Constans.COOKIE_EXPIRE_TIME);//設置cookie時間
cookie.setPath("/");
response.addCookie(cookie);
//存入redis中
System.out.println("創(chuàng)建uuid:"+uuid);
accountUser.setUuid(uuid);
result.setModel(accountUser);
boolean creatToken = tokenManagerInter.createToken(accountUser);
System.out.println("創(chuàng)建token:"+creatToken);
if(!creatToken){
return resultUtil.setErrResult(ReturnCodeBase.ERR5002);
}4.退出登錄接口
退出登錄時候,檢查cookie是否存在,然后刪除redis緩存,刪除cookie,重定向到登錄界面
String cookie = CookieUtil.getCookie(Constans.COOKIE_SSO, request);
if (cookie != null && !cookie.equals("")) {
tokenManagerInter.deleteToken(cookie);
CookieUtil.deleteCookie(Constans.COOKIE_SSO, response, "/");
}
if (backUrl != null && !backUrl.equals("")) {
try {
return "redirect:"+Constans.URL.PREFIX+"login?backUrl=" + URLEncoder.encode(backUrl, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.error("退出登錄重定向失敗:" + e);
}
}
return "redirect:/login";到此這篇關于如何使用jwt+redis實現(xiàn)單點登錄的文章就介紹到這了,更多相關jwt redis單點登錄內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
使用Docker部署Redis并配置持久化與密碼保護的詳細步驟
本文將詳細介紹如何使用 Docker 部署 Redis,并通過 redis.conf 配置文件實現(xiàn)數(shù)據(jù)持久化和密碼保護,適合在生產環(huán)境中使用,文章通過代碼示例講解的非常詳細,需要的朋友可以參考下2025-03-03

