2 Commits

Author SHA1 Message Date
진기정 f389d652d6 [docs] SSO 세션 복원 브랜치 설명 문서 추가
- fix/sso-refresh-session-restore 브랜치의 변경 배경과 목적 정리
- refreshToken 기반 SSO 세션 복원과 토큰 응답 보강 내용을 문서화
- 검증 포인트와 frontend 계약 변화를 함께 정리
2026-04-09 13:00:56 +09:00
진기정 23efdf4b9a [API] refreshToken 기반 SSO 세션 복원과 토큰 응답 보강 2026-04-08 15:47:43 +09:00
8 changed files with 378 additions and 26 deletions
+220
View File
@@ -0,0 +1,220 @@
# fix/sso-refresh-session-restore 설명 문서
## 1. 문서 목적
이 문서는 backend 프로젝트 `api``fix/sso-refresh-session-restore` 브랜치에서 반영한 **SSO 세션 복원 및 토큰 응답 보강** 변경을 설명합니다.
핵심 목적은 다음 두 가지입니다.
- `ALIST_SSO` 쿠키가 없더라도, 유효한 `refreshToken`이 있으면 SSO 세션을 복원할 수 있게 한다.
- frontend direct API 흐름에서 필요한 `accessToken` 값을 `/auth/access`, `/auth/refresh` 응답 body로도 내려준다.
---
## 2. 배경 문제
기존 흐름에서는 SSO 관련 엔드포인트가 주로 `ALIST_SSO` 쿠키를 기준으로 로그인 상태를 판단했습니다.
이 구조에서는 다음 문제가 있었습니다.
1. 브라우저에 `refreshToken`은 남아 있지만 `ALIST_SSO` 쿠키가 사라진 경우
- `/sso/loginChecked`
- `/sso/authorize`
- `/auth/access`
에서 로그인 상태를 복원하지 못했습니다.
2. frontend가 direct API 흐름으로 변경되면서
- `POST /auth/access`
- `POST /auth/refresh`
응답 body의 `accessToken`을 직접 사용하도록 바뀌었는데,
backend 응답은 cookie만 세팅하고 body에는 토큰을 충분히 내려주지 않았습니다.
즉, **refreshToken 기반 세션 복원**과 **frontend 계약(body accessToken)** 이 동시에 필요했습니다.
---
## 3. 변경 대상 파일
이 브랜치의 핵심 변경 파일은 아래 7개입니다.
- `src/main/java/com/alist/api/modules/auth/AuthController.java`
- `src/main/java/com/alist/api/modules/auth/SsoController.java`
- `src/main/java/com/alist/api/modules/auth/dto/SsoAuthorizeDto.java`
- `src/main/java/com/alist/api/modules/auth/service/AuthService.java`
- `src/main/java/com/alist/api/modules/auth/service/SsoService.java`
- `src/main/java/com/alist/api/modules/auth/vo/LoginTokenVo.java`
- `src/main/resources/mapper/auth/LoginMapper.xml`
---
## 4. 핵심 변경 요약
### 4-1. `refreshToken` 기반 SSO 세션 복원 추가
`SsoService`에 아래 역할이 추가되었습니다.
- `loginChecked(String ssoSessionId, String refreshToken)`
- `resolveOrRestoreSsoSessionId(String ssoSessionId, String refreshToken)`
- `restoreSsoSessionIfNeeded(String ssoSessionId, String refreshToken)`
- `loadSsoSession(String ssoSessionId)`
동작 방식은 아래와 같습니다.
1. 먼저 기존 `ALIST_SSO` 쿠키 값(`ssoSessionId`)이 있으면 그대로 사용합니다.
2. `ssoSessionId`가 없으면 `refreshToken`으로 사용자 토큰 정보를 조회합니다.
3. 유효한 사용자가 확인되면
- 기존 최신 SSO 세션이 있으면 TTL을 연장해서 재사용하고
- 없으면 Redis에 새로운 SSO 세션을 다시 생성합니다.
4. 이후 `loginChecked`, `authorize`, `access`는 복원된 SSO 세션 기준으로 정상 동작합니다.
즉, **SSO 쿠키가 비어 있어도 refreshToken만 유효하면 공통 로그인 상태를 다시 살릴 수 있게 변경**되었습니다.
---
### 4-2. `/auth/access` 응답 강화
`AuthController.access()` 변경 내용:
- `@CookieValue(name = "refreshToken", required = false)`를 추가하여 refreshToken을 입력으로 받습니다.
- `ssoService.loginChecked(ssoSessionId, refreshToken)`을 사용해 SSO 세션을 복원 가능하게 했습니다.
- 세션 복원 결과에 `ssoSessionId`가 있으면 `ALIST_SSO` 쿠키를 다시 발급합니다.
- 응답 body에 아래 값을 포함합니다.
- `isAccessToken`
- `userIdx`
- `userId`
- `userRole`
- `accessToken`
즉, `/auth/access`는 이제 **토큰 쿠키 발급 + frontend용 JSON body accessToken 제공**을 같이 수행합니다.
---
### 4-3. `/auth/refresh` 응답 강화
`AuthController.refresh()` 변경 내용:
- 기존처럼 `accessToken`, `refreshToken` 쿠키는 재발급합니다.
- 추가로 응답 body에 아래 값을 내려줍니다.
- `refreshed: true`
- `accessToken`
이 변경으로 frontend는 refresh 성공 후 cookie에만 의존하지 않고, **응답 body의 accessToken으로 즉시 service-local 토큰을 다시 기록**할 수 있습니다.
---
### 4-4. `/sso/loginChecked` 개선
`SsoController.loginChecked()` 변경 내용:
- `refreshToken` cookie를 함께 입력으로 받습니다.
- `ssoService.loginChecked(ssoSessionId, refreshToken)`을 사용합니다.
- access/refresh token 존재 여부를 기존처럼 함께 반환합니다.
- SSO 세션이 복원되면 `ALIST_SSO` 쿠키를 다시 써줍니다.
즉, `/sso/loginChecked`는 더 이상 단순 조회가 아니라 **필요 시 세션 복원까지 수행하는 확인 엔드포인트**가 되었습니다.
---
### 4-5. `/sso/authorize` 개선
`SsoController.authorize()``SsoService.authorize()` 변경 내용:
- `refreshToken` cookie를 함께 전달받아 세션 복원에 사용합니다.
- authorize 성공 시 `SsoAuthorizeDto``ssoSessionId`를 실어주고,
- controller에서 해당 값을 기준으로 `ALIST_SSO` 쿠키를 재발급합니다.
즉, 사용자가 이미 refreshToken을 가지고 있다면 **SSO 쿠키가 비어 있어도 authorize 진입 시 다시 공통 로그인 상태를 회복**할 수 있습니다.
---
### 4-6. refreshToken 조회용 Auth 계층 보강
`AuthService`와 DB 조회 쪽에는 아래 보강이 들어갔습니다.
- `findUserTokenByRefreshToken(String refreshToken)` 추가
- `LoginTokenVo``userIdx`, `userId` 필드 추가
- `LoginMapper.xml``selectUserTokenByUserTokenIdx``user_idx`, `user_id`, `user_role`, `refresh_token`을 함께 조회
이 변경은 SSO 세션 복원 시, 단순 token 유효성 확인을 넘어서 **어떤 사용자의 세션을 복구해야 하는지 식별하기 위해 필요**합니다.
---
## 5. 변경 후 기대 동작
### 시나리오 A. `ALIST_SSO` 없음 + `refreshToken` 유효
- `POST /auth/access`
- SSO 세션 복원 성공
- `ALIST_SSO` 재발급
- `accessToken` / `refreshToken` 재발급
- body에 `accessToken` 포함
### 시나리오 B. `GET /sso/loginChecked`
- 기존에는 로그인 false로 끝날 수 있었던 상황에서
- 이제 refreshToken이 유효하면 loggedIn true로 복원 가능
### 시나리오 C. `GET /sso/authorize`
- 기존에는 `ALIST_SSO` 없으면 `/login`으로 fallback
- 이제 refreshToken으로 세션 복원 가능하면 정상 authorize 진행
### 시나리오 D. `POST /auth/refresh`
- cookie 재발급뿐 아니라 body에도 `accessToken` 제공
- frontend가 바로 service-local accessToken을 다시 쓸 수 있음
---
## 6. frontend와의 계약 변화
이 브랜치는 frontend의 direct API 흐름과 맞물려 있습니다.
frontend는 현재 아래 계약을 기대합니다.
- `POST /auth/access` 응답 body에 `accessToken`
- `POST /auth/refresh` 응답 body에 `accessToken`
따라서 이 브랜치 변경은 단순 backend 내부 개선이 아니라,
**frontend의 direct login / callback / refresh 흐름을 안정적으로 지원하는 계약 변경**이기도 합니다.
---
## 7. 운영상 의미
이 변경을 적용하면 다음이 좋아집니다.
- SSO 쿠키 유실 상황에서 세션 복원력이 높아짐
- cross-domain authorize 진입 시 로그인 유지가 더 안정적임
- frontend가 refresh 후 토큰 재반영을 더 단순하게 처리할 수 있음
- `ALIST_SSO``refreshToken`을 각각의 역할에 맞게 유지하면서도 사용자 체감 로그인 끊김을 줄일 수 있음
---
## 8. 검증 포인트
배포 또는 MR 검토 시 아래를 확인하는 것이 좋습니다.
1. `POST /auth/access`
- `ALIST_SSO` 없이 `refreshToken`만 있을 때 200 또는 정상 복원되는지
- response body에 `accessToken`이 포함되는지
2. `POST /auth/refresh`
- response body에 `accessToken`이 포함되는지
3. `GET /sso/loginChecked`
- `refreshToken`만으로 `loggedIn=true`가 가능한지
4. `GET /sso/authorize`
- `ALIST_SSO`가 없더라도 refreshToken 기반으로 redirect가 이어지는지
5. Redis 상태
- `alist:sso:userIdx:*`
- `alist:sso:session:*`
키가 복원/갱신되는지
---
## 9. 한 줄 요약
`fix/sso-refresh-session-restore` 브랜치는 **refreshToken만 남아 있는 상황에서도 SSO 세션을 복원하고, `/auth/access``/auth/refresh`가 frontend가 바로 사용할 수 있는 `accessToken`을 응답 body로 반환하도록 보강한 변경**입니다.
@@ -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">