[API] refreshToken 기반 SSO 세션 복원과 토큰 응답 보강
This commit is contained in:
@@ -76,32 +76,38 @@ public class AuthController {
|
|||||||
@PostMapping("/access")
|
@PostMapping("/access")
|
||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> access(
|
public ResponseEntity<ApiResponse<Map<String, Object>>> access(
|
||||||
HttpServletRequest request,
|
HttpServletRequest request,
|
||||||
HttpServletResponse response
|
HttpServletResponse response,
|
||||||
|
@CookieValue(name = "refreshToken", required = false) String refreshToken
|
||||||
) {
|
) {
|
||||||
String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName);
|
String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName);
|
||||||
SsoExchangeDto ssoSession = ssoService.loginChecked(ssoSessionId);
|
SsoExchangeDto ssoSession = ssoService.loginChecked(ssoSessionId, refreshToken);
|
||||||
|
|
||||||
if (ssoSession == null) {
|
if (ssoSession == null) {
|
||||||
return ApiResponse.entity(Map.of("isAccessToken", false), ApiResponseCode.CODE_401);
|
return ApiResponse.entity(Map.of("isAccessToken", false), ApiResponseCode.CODE_401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ssoSession.getSsoSessionId() != null && !ssoSession.getSsoSessionId().isBlank()) {
|
||||||
|
SessionUtil.addTokenCookie(response, ssoCookieName, ssoSession.getSsoSessionId(), cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds);
|
||||||
|
}
|
||||||
|
|
||||||
String accessToken = jwtTokenProvider.createAccessToken(ssoSession.getUserTokenIdx(), ssoSession.getUserRole());
|
String accessToken = jwtTokenProvider.createAccessToken(ssoSession.getUserTokenIdx(), ssoSession.getUserRole());
|
||||||
String refreshToken = jwtTokenProvider.createRefreshToken(ssoSession.getUserTokenIdx());
|
String issuedRefreshToken = jwtTokenProvider.createRefreshToken(ssoSession.getUserTokenIdx());
|
||||||
|
|
||||||
LoginDto loginDto = new LoginDto();
|
LoginDto loginDto = new LoginDto();
|
||||||
loginDto.setUserTokenIdx(ssoSession.getUserTokenIdx());
|
loginDto.setUserTokenIdx(ssoSession.getUserTokenIdx());
|
||||||
loginDto.setRefreshToken(refreshToken);
|
loginDto.setRefreshToken(issuedRefreshToken);
|
||||||
|
|
||||||
authService.updateRefreshToken(loginDto);
|
authService.updateRefreshToken(loginDto);
|
||||||
|
|
||||||
SessionUtil.addTokenCookie(response, "accessToken", accessToken, cookieDomain, cookieSecure, cookieSameSite, accessTokenValiditySeconds);
|
SessionUtil.addTokenCookie(response, "accessToken", accessToken, cookieDomain, cookieSecure, cookieSameSite, accessTokenValiditySeconds);
|
||||||
SessionUtil.addTokenCookie(response,"refreshToken", refreshToken, cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds);
|
SessionUtil.addTokenCookie(response,"refreshToken", issuedRefreshToken, cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds);
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
result.put("isAccessToken", true);
|
result.put("isAccessToken", true);
|
||||||
result.put("userIdx", ssoSession.getUserIdx());
|
result.put("userIdx", ssoSession.getUserIdx());
|
||||||
result.put("userId", ssoSession.getUserId());
|
result.put("userId", ssoSession.getUserId());
|
||||||
result.put("userRole", ssoSession.getUserRole());
|
result.put("userRole", ssoSession.getUserRole());
|
||||||
|
result.put("accessToken", accessToken);
|
||||||
|
|
||||||
return ApiResponse.entity(result, ApiResponseCode.CODE_2001, "토큰");
|
return ApiResponse.entity(result, ApiResponseCode.CODE_2001, "토큰");
|
||||||
}
|
}
|
||||||
@@ -125,7 +131,11 @@ public class AuthController {
|
|||||||
SessionUtil.addTokenCookie(response, "accessToken", result.getAccessToken(), cookieDomain, cookieSecure, cookieSameSite, accessTokenValiditySeconds);
|
SessionUtil.addTokenCookie(response, "accessToken", result.getAccessToken(), cookieDomain, cookieSecure, cookieSameSite, accessTokenValiditySeconds);
|
||||||
SessionUtil.addTokenCookie(response, "refreshToken", result.getRefreshToken(), cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds);
|
SessionUtil.addTokenCookie(response, "refreshToken", result.getRefreshToken(), cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds);
|
||||||
|
|
||||||
return ApiResponse.entity(Map.of("refreshed", true), ApiResponseCode.CODE_200);
|
Map<String, Object> responseBody = new HashMap<>();
|
||||||
|
responseBody.put("refreshed", true);
|
||||||
|
responseBody.put("accessToken", result.getAccessToken());
|
||||||
|
|
||||||
|
return ApiResponse.entity(responseBody, ApiResponseCode.CODE_200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.alist.api.modules.auth;
|
|||||||
import com.alist.api.common.response.ApiResponse;
|
import com.alist.api.common.response.ApiResponse;
|
||||||
import com.alist.api.common.response.ApiResponseCode;
|
import com.alist.api.common.response.ApiResponseCode;
|
||||||
import com.alist.api.common.utils.SessionUtil;
|
import com.alist.api.common.utils.SessionUtil;
|
||||||
|
import com.alist.api.modules.auth.dto.SsoAuthorizeDto;
|
||||||
import com.alist.api.modules.auth.dto.SsoExchangeDto;
|
import com.alist.api.modules.auth.dto.SsoExchangeDto;
|
||||||
import com.alist.api.modules.auth.dto.SsoLoginCheckDto;
|
import com.alist.api.modules.auth.dto.SsoLoginCheckDto;
|
||||||
import com.alist.api.modules.auth.dto.SsoLoginDto;
|
import com.alist.api.modules.auth.dto.SsoLoginDto;
|
||||||
@@ -86,18 +87,19 @@ public class SsoController {
|
|||||||
public ResponseEntity<ApiResponse<Map<String, Object>>> loginChecked(
|
public ResponseEntity<ApiResponse<Map<String, Object>>> loginChecked(
|
||||||
HttpServletRequest request
|
HttpServletRequest request
|
||||||
, HttpServletResponse response
|
, HttpServletResponse response
|
||||||
|
, @CookieValue(name = "refreshToken", required = false) String refreshToken
|
||||||
) {
|
) {
|
||||||
String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName);
|
String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName);
|
||||||
|
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
SsoExchangeDto ssoSession = ssoService.loginChecked(ssoSessionId);
|
SsoExchangeDto ssoSession = ssoService.loginChecked(ssoSessionId, refreshToken);
|
||||||
|
|
||||||
String accessToken = SessionUtil.resolveSsoCookieValue(request, "accessToken");
|
String accessToken = SessionUtil.resolveSsoCookieValue(request, "accessToken");
|
||||||
String refreshToken = SessionUtil.resolveSsoCookieValue(request, "refreshToken");
|
String refreshTokenValue = SessionUtil.resolveSsoCookieValue(request, "refreshToken");
|
||||||
|
|
||||||
result.put("isAccessToken", accessToken != null && !accessToken.trim().isEmpty());
|
result.put("isAccessToken", accessToken != null && !accessToken.trim().isEmpty());
|
||||||
result.put("isRefreshToken", refreshToken != null && !refreshToken.trim().isEmpty());
|
result.put("isRefreshToken", refreshTokenValue != null && !refreshTokenValue.trim().isEmpty());
|
||||||
|
|
||||||
if (ssoSession == null) {
|
if (ssoSession == null) {
|
||||||
result.put("loggedIn", false);
|
result.put("loggedIn", false);
|
||||||
@@ -126,11 +128,17 @@ public class SsoController {
|
|||||||
, @RequestParam("redirectUri") String redirectUri
|
, @RequestParam("redirectUri") String redirectUri
|
||||||
, @RequestParam(value = "state", required = false) String state
|
, @RequestParam(value = "state", required = false) String state
|
||||||
, HttpServletRequest request
|
, HttpServletRequest request
|
||||||
|
, HttpServletResponse response
|
||||||
|
, @CookieValue(name = "refreshToken", required = false) String refreshToken
|
||||||
) {
|
) {
|
||||||
String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName);
|
String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName);
|
||||||
String redirectUrl = ssoService.authorize(clientId, redirectUri, state, ssoSessionId);
|
SsoAuthorizeDto authorizeResult = ssoService.authorize(clientId, redirectUri, state, ssoSessionId, refreshToken);
|
||||||
|
|
||||||
return ResponseEntity.status(302).location(URI.create(redirectUrl)).build();
|
if (authorizeResult.getSsoSessionId() != null && !authorizeResult.getSsoSessionId().isBlank()) {
|
||||||
|
SessionUtil.addTokenCookie(response, ssoCookieName, authorizeResult.getSsoSessionId(), cookieDomain, cookieSecure, cookieSameSite, ssoSessionTtlSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.status(302).location(URI.create(authorizeResult.getRedirectUrl())).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(
|
@Operation(
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.alist.api.modules.auth.dto;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class SsoAuthorizeDto {
|
||||||
|
private String redirectUrl;
|
||||||
|
private String ssoSessionId;
|
||||||
|
}
|
||||||
@@ -120,6 +120,30 @@ public class AuthService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoginTokenVo findUserTokenByRefreshToken(String refreshToken) {
|
||||||
|
if (refreshToken == null || !jwtTokenProvider.validateToken(refreshToken)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int userTokenIdx;
|
||||||
|
try {
|
||||||
|
userTokenIdx = Integer.parseInt(jwtTokenProvider.getUserTokenIdx(refreshToken));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginTokenVo tokenVo = authMapper.selectUserTokenByUserTokenIdx(userTokenIdx);
|
||||||
|
if (tokenVo == null || tokenVo.getRefreshToken() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!refreshToken.equals(tokenVo.getRefreshToken())) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenVo;
|
||||||
|
}
|
||||||
|
|
||||||
public void clearRefreshToken(String refreshToken) {
|
public void clearRefreshToken(String refreshToken) {
|
||||||
if (refreshToken == null || !jwtTokenProvider.validateToken(refreshToken)) {
|
if (refreshToken == null || !jwtTokenProvider.validateToken(refreshToken)) {
|
||||||
return;
|
return;
|
||||||
@@ -141,4 +165,4 @@ public class AuthService {
|
|||||||
|
|
||||||
authMapper.updateRefreshToken(loginDto);
|
authMapper.updateRefreshToken(loginDto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.alist.api.modules.auth.service;
|
package com.alist.api.modules.auth.service;
|
||||||
|
|
||||||
|
import com.alist.api.modules.auth.dto.SsoAuthorizeDto;
|
||||||
import com.alist.api.modules.auth.dto.SsoExchangeDto;
|
import com.alist.api.modules.auth.dto.SsoExchangeDto;
|
||||||
import com.alist.api.modules.auth.dto.SsoLoginDto;
|
import com.alist.api.modules.auth.dto.SsoLoginDto;
|
||||||
import com.alist.api.modules.auth.dto.LoginDto;
|
import com.alist.api.modules.auth.dto.LoginDto;
|
||||||
|
import com.alist.api.modules.auth.vo.LoginTokenVo;
|
||||||
import com.alist.api.modules.auth.mapper.SsoMapper;
|
import com.alist.api.modules.auth.mapper.SsoMapper;
|
||||||
import com.alist.api.modules.auth.vo.SsoClientVo;
|
import com.alist.api.modules.auth.vo.SsoClientVo;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -69,6 +71,23 @@ public class SsoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public SsoExchangeDto loginChecked(String ssoSessionId) {
|
public SsoExchangeDto loginChecked(String ssoSessionId) {
|
||||||
|
return loginChecked(ssoSessionId, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SsoExchangeDto loginChecked(String ssoSessionId, String refreshToken) {
|
||||||
|
String resolvedSsoSessionId = resolveOrRestoreSsoSessionId(ssoSessionId, refreshToken);
|
||||||
|
if (resolvedSsoSessionId == null || resolvedSsoSessionId.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadSsoSession(resolvedSsoSessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveOrRestoreSsoSessionId(String ssoSessionId, String refreshToken) {
|
||||||
|
return restoreSsoSessionIfNeeded(ssoSessionId, refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SsoExchangeDto loadSsoSession(String ssoSessionId) {
|
||||||
if (ssoSessionId == null || ssoSessionId.isBlank()) {
|
if (ssoSessionId == null || ssoSessionId.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -101,20 +120,30 @@ public class SsoService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String authorize(String clientId, String redirectUri, String state, String ssoSessionId) {
|
public SsoAuthorizeDto authorize(String clientId, String redirectUri, String state, String ssoSessionId) {
|
||||||
if (ssoSessionId == null || ssoSessionId.isBlank()) {
|
return authorize(clientId, redirectUri, state, ssoSessionId, null);
|
||||||
return "/login";
|
}
|
||||||
|
|
||||||
|
public SsoAuthorizeDto authorize(String clientId, String redirectUri, String state, String ssoSessionId, String refreshToken) {
|
||||||
|
SsoAuthorizeDto result = new SsoAuthorizeDto();
|
||||||
|
String resolvedSsoSessionId = resolveOrRestoreSsoSessionId(ssoSessionId, refreshToken);
|
||||||
|
|
||||||
|
if (resolvedSsoSessionId == null || resolvedSsoSessionId.isBlank()) {
|
||||||
|
result.setRedirectUrl("/login");
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
String sessionKey = "alist:sso:session:" + ssoSessionId;
|
String sessionKey = "alist:sso:session:" + resolvedSsoSessionId;
|
||||||
String userIdx = (String) redisTemplate.opsForHash().get(sessionKey, "userIdx");
|
String userIdx = (String) redisTemplate.opsForHash().get(sessionKey, "userIdx");
|
||||||
if (userIdx == null) {
|
if (userIdx == null) {
|
||||||
return "/login";
|
result.setRedirectUrl("/login");
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
String latestSsoSessionId = redisTemplate.opsForValue().get("alist:sso:userIdx:" + userIdx);
|
String latestSsoSessionId = redisTemplate.opsForValue().get("alist:sso:userIdx:" + userIdx);
|
||||||
if (!ssoSessionId.equals(latestSsoSessionId)) {
|
if (!resolvedSsoSessionId.equals(latestSsoSessionId)) {
|
||||||
return "/login";
|
result.setRedirectUrl("/login");
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
SsoClientVo ssoClientVo = ssoMapper.selectSsoClient(clientId);
|
SsoClientVo ssoClientVo = ssoMapper.selectSsoClient(clientId);
|
||||||
@@ -126,12 +155,12 @@ public class SsoService {
|
|||||||
throw new IllegalArgumentException("비활성화된 SSO 클라이언트입니다.");
|
throw new IllegalArgumentException("비활성화된 SSO 클라이언트입니다.");
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshSsoSessionTtl(ssoSessionId, userIdx);
|
refreshSsoSessionTtl(resolvedSsoSessionId, userIdx);
|
||||||
|
|
||||||
String code = "CODE_" + UUID.randomUUID();
|
String code = "CODE_" + UUID.randomUUID();
|
||||||
String codeKey = "alist:sso:code:" + code;
|
String codeKey = "alist:sso:code:" + code;
|
||||||
|
|
||||||
redisTemplate.opsForHash().put(codeKey, "ssoSessionId", ssoSessionId);
|
redisTemplate.opsForHash().put(codeKey, "ssoSessionId", resolvedSsoSessionId);
|
||||||
redisTemplate.opsForHash().put(codeKey, "clientId", clientId);
|
redisTemplate.opsForHash().put(codeKey, "clientId", clientId);
|
||||||
redisTemplate.opsForHash().put(codeKey, "redirectUri", redirectUri);
|
redisTemplate.opsForHash().put(codeKey, "redirectUri", redirectUri);
|
||||||
redisTemplate.expire(codeKey, ssoCodeTtl);
|
redisTemplate.expire(codeKey, ssoCodeTtl);
|
||||||
@@ -151,7 +180,51 @@ public class SsoService {
|
|||||||
}
|
}
|
||||||
redirect.append(encodedState);
|
redirect.append(encodedState);
|
||||||
|
|
||||||
return redirect.toString();
|
result.setRedirectUrl(redirect.toString());
|
||||||
|
result.setSsoSessionId(resolvedSsoSessionId);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String restoreSsoSessionIfNeeded(String ssoSessionId, String refreshToken) {
|
||||||
|
if (ssoSessionId != null && !ssoSessionId.isBlank()) {
|
||||||
|
return ssoSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginTokenVo loginTokenVo = authService.findUserTokenByRefreshToken(refreshToken);
|
||||||
|
if (loginTokenVo == null
|
||||||
|
|| loginTokenVo.getUserIdx() == null
|
||||||
|
|| loginTokenVo.getUserId() == null
|
||||||
|
|| loginTokenVo.getUserTokenIdx() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String restoredUserRole = loginTokenVo.getUserRole();
|
||||||
|
if (restoredUserRole == null || restoredUserRole.isBlank()) {
|
||||||
|
restoredUserRole = "USER";
|
||||||
|
}
|
||||||
|
|
||||||
|
String latestSessionKey = "alist:sso:userIdx:" + loginTokenVo.getUserIdx();
|
||||||
|
String existingSsoSessionId = redisTemplate.opsForValue().get(latestSessionKey);
|
||||||
|
if (existingSsoSessionId != null && !existingSsoSessionId.isBlank()) {
|
||||||
|
String existingSessionKey = "alist:sso:session:" + existingSsoSessionId;
|
||||||
|
String existingUserIdx = (String) redisTemplate.opsForHash().get(existingSessionKey, "userIdx");
|
||||||
|
if (existingUserIdx != null && !existingUserIdx.isBlank()) {
|
||||||
|
refreshSsoSessionTtl(existingSsoSessionId, existingUserIdx);
|
||||||
|
return existingSsoSessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String restoredSsoSessionId = "SSO_" + UUID.randomUUID();
|
||||||
|
String sessionKey = "alist:sso:session:" + restoredSsoSessionId;
|
||||||
|
|
||||||
|
redisTemplate.opsForHash().put(sessionKey, "userIdx", String.valueOf(loginTokenVo.getUserIdx()));
|
||||||
|
redisTemplate.opsForHash().put(sessionKey, "userTokenIdx", String.valueOf(loginTokenVo.getUserTokenIdx()));
|
||||||
|
redisTemplate.opsForHash().put(sessionKey, "userId", loginTokenVo.getUserId());
|
||||||
|
redisTemplate.opsForHash().put(sessionKey, "userRole", restoredUserRole);
|
||||||
|
redisTemplate.expire(sessionKey, ssoSessionTtl);
|
||||||
|
redisTemplate.opsForValue().set(latestSessionKey, restoredSsoSessionId, ssoSessionTtl);
|
||||||
|
|
||||||
|
return restoredSsoSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SsoExchangeDto exchange(SsoExchangeDto ssoExchangeDto) {
|
public SsoExchangeDto exchange(SsoExchangeDto ssoExchangeDto) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import lombok.Getter;
|
|||||||
@Getter
|
@Getter
|
||||||
public class LoginTokenVo {
|
public class LoginTokenVo {
|
||||||
private Integer userTokenIdx;
|
private Integer userTokenIdx;
|
||||||
|
private Integer userIdx;
|
||||||
|
private String userId;
|
||||||
private String userApiKey;
|
private String userApiKey;
|
||||||
private String userRole;
|
private String userRole;
|
||||||
private String refreshToken;
|
private String refreshToken;
|
||||||
|
|||||||
@@ -41,11 +41,15 @@
|
|||||||
|
|
||||||
<select id="selectUserTokenByUserTokenIdx" resultType="com.alist.api.modules.auth.vo.LoginTokenVo">
|
<select id="selectUserTokenByUserTokenIdx" resultType="com.alist.api.modules.auth.vo.LoginTokenVo">
|
||||||
/*LoginMapper.selectUserTokenByUserTokenIdx*/
|
/*LoginMapper.selectUserTokenByUserTokenIdx*/
|
||||||
select user_token_idx
|
select tut.user_token_idx
|
||||||
, user_role
|
, tut.user_idx
|
||||||
, refresh_token
|
, id as user_id
|
||||||
from ALISTLMS.test_user_token
|
, tut.user_role
|
||||||
|
, tut.refresh_token
|
||||||
|
from ALISTLMS.test_user_token tut
|
||||||
|
join ALISTLMS.test_user tu on tu.user_idx = tut.user_idx
|
||||||
where user_token_idx = #{userTokenIdx}
|
where user_token_idx = #{userTokenIdx}
|
||||||
|
and tu.del_yn = 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<update id="clearRefreshTokenByUserTokenIdx">
|
<update id="clearRefreshTokenByUserTokenIdx">
|
||||||
|
|||||||
Reference in New Issue
Block a user