如何使用jwt+redis實(shí)現(xiàn)單點(diǎn)登錄
首先理一下登錄流程
前端登錄—>賬號(hào)密碼驗(yàn)證—>成功返回token—>后續(xù)請(qǐng)求攜帶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 {
// 默認(rèn)過期時(shí)間 1 小時(shí)
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ā)時(shí)間
.withExpiresAt(expireDate) // 過期時(shí)間
.withPayload(claims) // 自定義載荷
.sign(Algorithm.HMAC256(SECRET)); // 簽名算法
}
/**
* 驗(yàn)證 token 是否有效
*
* @param token 待驗(yàn)證的 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 中的某個(gè) claim
*
* @param token JWT token
* @param key claim 的 key
* @return claim 對(duì)應(yīng)的值
*/
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 的過期時(shí)間
*
* @param token JWT token
* @return 過期時(shí)間
*/
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("獲取當(dāng)前用戶信息出錯(cuò)");
}
return username;
}
}JWT攔截器
每次更新token的過期時(shí)間
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,實(shí)現(xiàn)單點(diǎ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)在其他地方登錄,請(qǐng)重新登錄");
}
// 可選:刷新 Redis token 過期時(shí)間
String redisKey = USER_LOGIN_KEY + username;
stringRedisTemplate.expire(redisKey, 30, TimeUnit.MINUTES);
// 將用戶名放入請(qǐng)求上下文,供 Controller 使用
request.setAttribute("username", username);
return true;
}
}注冊(cè)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("/**") // 攔截所有請(qǐng)求
.excludePathPatterns(
"/api/short-link/admin/v1/user/login" // 登錄接口不攔截
);
}
}登錄方法
首先判斷賬號(hào)密碼,正確以后,判斷redis是否有這個(gè)用戶,如果有,說明已經(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,實(shí)現(xiàn)單點(diǎ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,實(shí)現(xiàn)單點(diǎ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不存在或用戶未登錄");
}補(bǔ)充:基于Redis的單點(diǎn)登錄實(shí)現(xiàn)方案
一.單點(diǎn)登錄流程分析
1.客戶端統(tǒng)一攔截過濾器

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

分析:
1.登錄接口:用戶名密碼驗(yàn)證成功后,設(shè)置cookie,設(shè)置redis緩存,登錄成功
2.登錄頁面:先判斷是否有cookie以及redis緩存,如果滿足,則重定向返回backurl以及附加參數(shù)ticket值為當(dāng)前的cookie,如果不滿足,則進(jìn)入登錄界面
3.退出登錄:刪除redis緩存,刪除cookie
4.為了防止別人拿著帶著ticket的鏈接去其他瀏覽器進(jìn)行偽造登錄,則可以對(duì)其ticket存入redis中,客戶端對(duì)其進(jìn)行一次性消費(fèi).
二.客戶端與服務(wù)端cookie同步
1.js腳本更新
可通過html加入js腳本訪問服務(wù)端,服務(wù)端再對(duì)其cookie重置時(shí)長(zhǎng)
2.增長(zhǎng)服務(wù)端cookie時(shí)長(zhǎng)
可通過增長(zhǎng)服務(wù)端cookie時(shí)長(zhǎng),如果客戶端退出了,redis緩存將清理,再次訪問服務(wù)端登錄接口,服務(wù)端查詢不到redis緩存,則對(duì)其cookie進(jìn)行刪除.但是這種不能算是完全意義上的同步.
三.關(guān)鍵代碼
1.客戶端過濾器filter
String cookieName = CookieUtil.getCookie(Constans.COOKIE_SSO, request);
if(cookieName!=null&& !cookieName.equals("")){
//驗(yàn)證cookie的有效性
User user = tokenManagerInter.getUserInfo(cookieName);
if(user!=null){
//驗(yàn)證成功,繼續(xù)執(zhí)行
//TODO 重置cookie時(shí)間,緩存時(shí)間
filterChain.doFilter(servletRequest,servletResponse);
}else {
//驗(yàn)證失敗,刪除無效cookie
CookieUtil.deleteCookie(Constans.COOKIE_SSO,response, "/");
//返回登錄界面
String qstr = makeQueryString(request); // 將所有請(qǐng)求參數(shù)重新拼接成queryString
String backUrl=request.getRequestURL() + qstr; // 回調(diào)url
String location = "127.0.0.1/login?backUrl=" + URLEncoder.encode(backUrl, "utf-8");
response.sendRedirect(location);
}
}else {
String vtParam = pasreVtParam(request); // 從請(qǐng)求中
if (vtParam == null) {
// 請(qǐng)求中中沒有vtParam,引導(dǎo)瀏覽器重定向到服務(wù)端執(zhí)行登錄校驗(yàn)
//返回登錄界面
String qstr = makeQueryString(request); // 將所有請(qǐng)求參數(shù)重新拼接成queryString
String backUrl=request.getRequestURL() + qstr; // 回調(diào)url
String location = "127.0.0.1/login?backUrl=" + URLEncoder.encode(backUrl, "utf-8");
response.sendRedirect(location);
} else if (vtParam.length() == 0) {
// 有vtParam,但內(nèi)容為空,表示到服務(wù)端loginCheck后,得到的結(jié)果是未登錄
response.sendError(403);
} else {
// 讓瀏覽器向本鏈接發(fā)起一次重定向,此過程去除vtParam,將vt寫入cookie
redirectToSelf(vtParam);
}
}2.服務(wù)端登錄界面
先判斷當(dāng)前是否有cookie,如果沒有,則進(jìn)入登錄界面,如果存在,則判斷是否有redis緩存存在,如果不存在,則進(jìn)入登錄界面,如果存在,則重定向到backurl中去,同時(shí)帶著ticket返回客戶端
@Override
public String login(String backUrl, ModelMap map) {
String cookie = CookieUtil.getCookie(Constans.COOKIE_SSO, request);
//判斷有無cookie
if(cookie==null){
//無cookie,進(jìn)入登錄界面
map.put("backUrl", backUrl);
return "login";
}else {
//當(dāng)前存在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.服務(wù)端登錄接口
驗(yàn)證完用戶名密碼后設(shè)置cookie,增加redis緩存,重定向到backurl去.
//設(shè)置cookie
String uuid = UUID.randomUUID().toString().replaceAll("-","");
Cookie cookie = new Cookie(Constans.COOKIE_SSO, uuid);
cookie.setMaxAge(Constans.COOKIE_EXPIRE_TIME);//設(shè)置cookie時(shí)間
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.退出登錄接口
退出登錄時(shí)候,檢查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";到此這篇關(guān)于如何使用jwt+redis實(shí)現(xiàn)單點(diǎn)登錄的文章就介紹到這了,更多相關(guān)jwt redis單點(diǎn)登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 使用Redis實(shí)現(xiàn)JWT令牌主動(dòng)失效機(jī)制
- SpringSecurity+Redis+Jwt實(shí)現(xiàn)用戶認(rèn)證授權(quán)
- Shiro整合Springboot和redis,jwt過程中的錯(cuò)誤shiroFilterChainDefinition問題
- jwt+redis實(shí)現(xiàn)登錄認(rèn)證的示例代碼
- 基于 Redis 的 JWT令牌失效處理方案(實(shí)現(xiàn)步驟)
- springboot+springsecurity+mybatis+JWT+Redis?實(shí)現(xiàn)前后端離實(shí)戰(zhàn)教程
- SpringBoot整合SpringSecurity和JWT和Redis實(shí)現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringSecurity+jwt+redis基于數(shù)據(jù)庫(kù)登錄認(rèn)證的實(shí)現(xiàn)
- java實(shí)現(xiàn)認(rèn)證與授權(quán)的jwt與token+redis,哪種方案更好用?
相關(guān)文章
Redis實(shí)現(xiàn)Session共享與單點(diǎn)登錄
本文主要介紹了Redis實(shí)現(xiàn)Session共享與單點(diǎn)登錄,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
Redis源碼與設(shè)計(jì)剖析之網(wǎng)絡(luò)連接庫(kù)
這篇文章主要為大家介紹了Redis源碼與設(shè)計(jì)剖析之網(wǎng)絡(luò)連接庫(kù)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
Redis?存儲(chǔ)對(duì)象信息用?Hash?和String的區(qū)別
這篇文章主要介紹了Redis存儲(chǔ)對(duì)象信息用Hash和String的區(qū)別,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
使用Redis實(shí)現(xiàn)JWT令牌主動(dòng)失效機(jī)制
JWT是一種輕量級(jí)的身份驗(yàn)證和授權(quán)機(jī)制,它是一種 JSON 格式的數(shù)據(jù)串,通常用于客戶端和服務(wù)端之間的單點(diǎn)登錄(Single Sign-On, SSO)場(chǎng)景,本文給大家介紹了如何使用Redis來實(shí)現(xiàn)JWT令牌主動(dòng)失效機(jī)制,需要的朋友可以參考下2024-08-08
redis學(xué)習(xí)之RDB、AOF與復(fù)制時(shí)對(duì)過期鍵的處理教程
這篇文章主要給大家介紹了關(guān)于redis學(xué)習(xí)之RDB、AOF與復(fù)制時(shí)對(duì)過期鍵處理的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用redis具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
Redis?key的過期時(shí)間和永久有效的實(shí)現(xiàn)
在Redis中,鍵可以設(shè)置過期時(shí)間或被永久保存,`EXPIRE`和`PEXPIRE`命令分別用于設(shè)置鍵的過期時(shí)間,具有一定的參考價(jià)值,感興趣的可以了解一下2024-09-09
redis中事務(wù)機(jī)制及樂觀鎖的實(shí)現(xiàn)
這篇文章主要介紹了redis中事務(wù)機(jī)制及樂觀鎖的相關(guān)內(nèi)容,通過事務(wù)的執(zhí)行分析Redis樂觀鎖,具有一定參考價(jià)值,需要的朋友可以了解下。2017-10-10
如何監(jiān)聽Redis中Key值的變化(SpringBoot整合)
測(cè)試過程中我們有一部分常量值放入redis,共大部分應(yīng)用調(diào)用,但在測(cè)試過程中經(jīng)常有人會(huì)清空redis,回歸測(cè)試,下面這篇文章主要給大家介紹了關(guān)于如何監(jiān)聽Redis中Key值變化的相關(guān)資料,需要的朋友可以參考下2024-03-03

