diff --git a/src/main/java/com/alist/api/modules/auth/AuthController.java b/src/main/java/com/alist/api/modules/auth/AuthController.java index 3b24aa7..470ec55 100644 --- a/src/main/java/com/alist/api/modules/auth/AuthController.java +++ b/src/main/java/com/alist/api/modules/auth/AuthController.java @@ -76,32 +76,38 @@ public class AuthController { @PostMapping("/access") public ResponseEntity>> access( HttpServletRequest request, - HttpServletResponse response + HttpServletResponse response, + @CookieValue(name = "refreshToken", required = false) String refreshToken ) { String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName); - SsoExchangeDto ssoSession = ssoService.loginChecked(ssoSessionId); + SsoExchangeDto ssoSession = ssoService.loginChecked(ssoSessionId, refreshToken); if (ssoSession == null) { 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 refreshToken = jwtTokenProvider.createRefreshToken(ssoSession.getUserTokenIdx()); + String issuedRefreshToken = jwtTokenProvider.createRefreshToken(ssoSession.getUserTokenIdx()); LoginDto loginDto = new LoginDto(); loginDto.setUserTokenIdx(ssoSession.getUserTokenIdx()); - loginDto.setRefreshToken(refreshToken); + loginDto.setRefreshToken(issuedRefreshToken); authService.updateRefreshToken(loginDto); 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 result = new HashMap<>(); result.put("isAccessToken", true); result.put("userIdx", ssoSession.getUserIdx()); result.put("userId", ssoSession.getUserId()); result.put("userRole", ssoSession.getUserRole()); + result.put("accessToken", accessToken); 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, "refreshToken", result.getRefreshToken(), cookieDomain, cookieSecure, cookieSameSite, refreshTokenValiditySeconds); - return ApiResponse.entity(Map.of("refreshed", true), ApiResponseCode.CODE_200); + Map responseBody = new HashMap<>(); + responseBody.put("refreshed", true); + responseBody.put("accessToken", result.getAccessToken()); + + return ApiResponse.entity(responseBody, ApiResponseCode.CODE_200); } @Operation( diff --git a/src/main/java/com/alist/api/modules/auth/SsoController.java b/src/main/java/com/alist/api/modules/auth/SsoController.java index 9ec2ecb..e2c6832 100644 --- a/src/main/java/com/alist/api/modules/auth/SsoController.java +++ b/src/main/java/com/alist/api/modules/auth/SsoController.java @@ -3,6 +3,7 @@ package com.alist.api.modules.auth; import com.alist.api.common.response.ApiResponse; import com.alist.api.common.response.ApiResponseCode; 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.SsoLoginCheckDto; import com.alist.api.modules.auth.dto.SsoLoginDto; @@ -86,18 +87,19 @@ public class SsoController { public ResponseEntity>> loginChecked( HttpServletRequest request , HttpServletResponse response + , @CookieValue(name = "refreshToken", required = false) String refreshToken ) { String ssoSessionId = SessionUtil.resolveSsoCookieValue(request, ssoCookieName); Map result = new HashMap<>(); - SsoExchangeDto ssoSession = ssoService.loginChecked(ssoSessionId); + SsoExchangeDto ssoSession = ssoService.loginChecked(ssoSessionId, refreshToken); 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("isRefreshToken", refreshToken != null && !refreshToken.trim().isEmpty()); + result.put("isRefreshToken", refreshTokenValue != null && !refreshTokenValue.trim().isEmpty()); if (ssoSession == null) { result.put("loggedIn", false); @@ -126,11 +128,17 @@ public class SsoController { , @RequestParam("redirectUri") String redirectUri , @RequestParam(value = "state", required = false) String state , HttpServletRequest request + , HttpServletResponse response + , @CookieValue(name = "refreshToken", required = false) String refreshToken ) { 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( diff --git a/src/main/java/com/alist/api/modules/auth/dto/SsoAuthorizeDto.java b/src/main/java/com/alist/api/modules/auth/dto/SsoAuthorizeDto.java new file mode 100644 index 0000000..23f3060 --- /dev/null +++ b/src/main/java/com/alist/api/modules/auth/dto/SsoAuthorizeDto.java @@ -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; +} diff --git a/src/main/java/com/alist/api/modules/auth/service/AuthService.java b/src/main/java/com/alist/api/modules/auth/service/AuthService.java index 7b1208a..f606aec 100644 --- a/src/main/java/com/alist/api/modules/auth/service/AuthService.java +++ b/src/main/java/com/alist/api/modules/auth/service/AuthService.java @@ -120,6 +120,30 @@ public class AuthService { 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) { if (refreshToken == null || !jwtTokenProvider.validateToken(refreshToken)) { return; @@ -141,4 +165,4 @@ public class AuthService { authMapper.updateRefreshToken(loginDto); } -} \ No newline at end of file +} diff --git a/src/main/java/com/alist/api/modules/auth/service/SsoService.java b/src/main/java/com/alist/api/modules/auth/service/SsoService.java index 442e0b4..42b6c75 100644 --- a/src/main/java/com/alist/api/modules/auth/service/SsoService.java +++ b/src/main/java/com/alist/api/modules/auth/service/SsoService.java @@ -1,8 +1,10 @@ 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.SsoLoginDto; 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.vo.SsoClientVo; import jakarta.servlet.http.HttpServletRequest; @@ -69,6 +71,23 @@ public class SsoService { } 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()) { return null; } @@ -101,20 +120,30 @@ public class SsoService { return result; } - public String authorize(String clientId, String redirectUri, String state, String ssoSessionId) { - if (ssoSessionId == null || ssoSessionId.isBlank()) { - return "/login"; + public SsoAuthorizeDto authorize(String clientId, String redirectUri, String state, String ssoSessionId) { + return authorize(clientId, redirectUri, state, ssoSessionId, null); + } + + 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"); if (userIdx == null) { - return "/login"; + result.setRedirectUrl("/login"); + return result; } String latestSsoSessionId = redisTemplate.opsForValue().get("alist:sso:userIdx:" + userIdx); - if (!ssoSessionId.equals(latestSsoSessionId)) { - return "/login"; + if (!resolvedSsoSessionId.equals(latestSsoSessionId)) { + result.setRedirectUrl("/login"); + return result; } SsoClientVo ssoClientVo = ssoMapper.selectSsoClient(clientId); @@ -126,12 +155,12 @@ public class SsoService { throw new IllegalArgumentException("비활성화된 SSO 클라이언트입니다."); } - refreshSsoSessionTtl(ssoSessionId, userIdx); + refreshSsoSessionTtl(resolvedSsoSessionId, userIdx); String code = "CODE_" + UUID.randomUUID(); 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, "redirectUri", redirectUri); redisTemplate.expire(codeKey, ssoCodeTtl); @@ -151,7 +180,51 @@ public class SsoService { } 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) { diff --git a/src/main/java/com/alist/api/modules/auth/vo/LoginTokenVo.java b/src/main/java/com/alist/api/modules/auth/vo/LoginTokenVo.java index dd1d73c..15689c5 100644 --- a/src/main/java/com/alist/api/modules/auth/vo/LoginTokenVo.java +++ b/src/main/java/com/alist/api/modules/auth/vo/LoginTokenVo.java @@ -5,6 +5,8 @@ import lombok.Getter; @Getter public class LoginTokenVo { private Integer userTokenIdx; + private Integer userIdx; + private String userId; private String userApiKey; private String userRole; private String refreshToken; diff --git a/src/main/resources/mapper/auth/LoginMapper.xml b/src/main/resources/mapper/auth/LoginMapper.xml index 5590cf4..5d7ee4a 100644 --- a/src/main/resources/mapper/auth/LoginMapper.xml +++ b/src/main/resources/mapper/auth/LoginMapper.xml @@ -41,11 +41,15 @@