

250331 김혜민 세션로그인 수정
@b9e46158b27e9e997759694b78e63d69db9e7427
--- src/main/java/com/takensoft/cms/loginPolicy/service/impl/LoginModeServiceImpl.java
+++ src/main/java/com/takensoft/cms/loginPolicy/service/impl/LoginModeServiceImpl.java
... | ... | @@ -35,7 +35,7 @@ |
35 | 35 |
@Override |
36 | 36 |
public String getLoginMode() { |
37 | 37 |
|
38 |
- return loginModeDAO.selectLatestLoginMode(); |
|
38 |
+ return loginModeDAO.selectLatestLoginMode(); |
|
39 | 39 |
} |
40 | 40 |
|
41 | 41 |
/** |
--- src/main/java/com/takensoft/cms/loginPolicy/web/LoginPolicyController.java
+++ src/main/java/com/takensoft/cms/loginPolicy/web/LoginPolicyController.java
... | ... | @@ -17,6 +17,8 @@ |
17 | 17 |
import org.springframework.http.ResponseEntity; |
18 | 18 |
import org.springframework.web.bind.annotation.*; |
19 | 19 |
|
20 |
+import java.util.Set; |
|
21 |
+ |
|
20 | 22 |
|
21 | 23 |
/** |
22 | 24 |
* @author 김혜민 |
... | ... | @@ -117,7 +119,7 @@ |
117 | 119 |
|
118 | 120 |
int result = loginModeService.insertLoginMode(loginModeVO); |
119 | 121 |
|
120 |
- /* if (loginModeVO.getLgnMode().equals("J")) { |
|
122 |
+ if (loginModeVO.getLgnMode().equals("J")) { |
|
121 | 123 |
// JWT 전체 로그아웃 |
122 | 124 |
Set<String> keys = redisTemplate.keys("jwt:*"); |
123 | 125 |
if (keys != null && !keys.isEmpty()) redisTemplate.delete(keys); |
... | ... | @@ -125,7 +127,7 @@ |
125 | 127 |
} else { |
126 | 128 |
// 세션 전체 로그아웃 |
127 | 129 |
sessionUtil.invalidateAllSessions(); |
128 |
- }*/ |
|
130 |
+ } |
|
129 | 131 |
|
130 | 132 |
if (result > 0) { |
131 | 133 |
return resUtil.successRes(result, MessageCode.COMMON_SUCCESS); |
--- src/main/java/com/takensoft/cms/token/service/RefreshTokenService.java
+++ src/main/java/com/takensoft/cms/token/service/RefreshTokenService.java
... | ... | @@ -69,4 +69,11 @@ |
69 | 69 |
* refresh token 전체 삭제 |
70 | 70 |
*/ |
71 | 71 |
public int deleteAll(); |
72 |
+ |
|
73 |
+ /** |
|
74 |
+ * @return String - 쿠키에 있는 refresh token 값 |
|
75 |
+ * @param req - HTTP 요청 객체 |
|
76 |
+ * 쿠키 내 refresh token 확인 |
|
77 |
+ */ |
|
78 |
+ public String getRefreshTokenFromCookie(HttpServletRequest req); |
|
72 | 79 |
} |
--- src/main/java/com/takensoft/cms/token/service/impl/RefreshTokenServiceImpl.java
+++ src/main/java/com/takensoft/cms/token/service/impl/RefreshTokenServiceImpl.java
... | ... | @@ -62,17 +62,14 @@ |
62 | 62 |
* |
63 | 63 |
* refresh token 검증 |
64 | 64 |
*/ |
65 |
- private Map<String, Object> refreshTokenCheck(HttpServletRequest req) { |
|
65 |
+ private Map<String, Object> refreshTokenCheck(HttpServletRequest req) { |
|
66 | 66 |
Map<String, Object> result = new HashMap <String, Object>(); |
67 | 67 |
// header 방식 |
68 | 68 |
// String refreshToken = req.getHeader("refresh"); |
69 | 69 |
// 쿠키 방식 |
70 |
- String refreshToken = null; |
|
71 |
- Cookie[] cookies = req.getCookies(); |
|
72 |
- for (Cookie cookie : cookies) { |
|
73 |
- if (cookie.getName().equals("refresh")) refreshToken = cookie.getValue(); |
|
74 |
- } |
|
75 |
- if(refreshToken == null) { |
|
70 |
+ // 쿠키에서 Refresh Token 가져오기 |
|
71 |
+ String refreshToken = getRefreshTokenFromCookie(req); |
|
72 |
+ if (refreshToken == null) { |
|
76 | 73 |
result.put("result", 0); |
77 | 74 |
return result; |
78 | 75 |
} |
... | ... | @@ -278,5 +275,21 @@ |
278 | 275 |
} |
279 | 276 |
return refreshTokenDAO.deleteAll(); // DB에서 리프레시 토큰 전부 삭제 |
280 | 277 |
} |
278 |
+ /** |
|
279 |
+ * @return String - 쿠키에 있는 refresh token 값 |
|
280 |
+ * @param req - HTTP 요청 객체 |
|
281 |
+ * 쿠키 내 refresh token 확인 |
|
282 |
+ */ |
|
283 |
+ @Override |
|
284 |
+ public String getRefreshTokenFromCookie(HttpServletRequest req) { |
|
285 |
+ if (req.getCookies() != null) { |
|
286 |
+ for (Cookie cookie : req.getCookies()) { |
|
287 |
+ if ("refresh".equals(cookie.getName())) { |
|
288 |
+ return cookie.getValue(); |
|
289 |
+ } |
|
290 |
+ } |
|
291 |
+ } |
|
292 |
+ return null; |
|
293 |
+ } |
|
281 | 294 |
|
282 | 295 |
} |
--- src/main/java/com/takensoft/cms/token/web/RefreshTokenController.java
+++ src/main/java/com/takensoft/cms/token/web/RefreshTokenController.java
... | ... | @@ -68,7 +68,7 @@ |
68 | 68 |
refresh.setMbrId(mbrId); |
69 | 69 |
int result = refreshTokenService.delete(req, refresh); |
70 | 70 |
|
71 |
- if ("S".equals(loginType)) { |
|
71 |
+ if (loginType.equals("S")) { |
|
72 | 72 |
// 세션 방식: 세션 만료 + SessionMap에서 제거 |
73 | 73 |
HttpSession session = req.getSession(false); |
74 | 74 |
if (session != null) session.invalidate(); |
--- src/main/java/com/takensoft/common/config/RedisConfig.java
+++ src/main/java/com/takensoft/common/config/RedisConfig.java
... | ... | @@ -1,8 +1,6 @@ |
1 | 1 |
package com.takensoft.common.config; |
2 | 2 |
|
3 |
-import com.takensoft.cms.loginPolicy.service.LoginPolicyService; |
|
4 | 3 |
import org.springframework.beans.factory.annotation.Value; |
5 |
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|
6 | 4 |
import org.springframework.context.annotation.Bean; |
7 | 5 |
import org.springframework.context.annotation.Configuration; |
8 | 6 |
import org.springframework.data.redis.connection.RedisConnectionFactory; |
... | ... | @@ -29,12 +27,10 @@ |
29 | 27 |
private int redisPort; |
30 | 28 |
|
31 | 29 |
@Bean |
32 |
- @ConditionalOnProperty(name = "config.allow-multiple-logins", havingValue = "false", matchIfMissing = true) //redis 사용 안 할 경우 빈 등록x |
|
33 | 30 |
public RedisConnectionFactory redisConnectionFactory() { |
34 | 31 |
return new LettuceConnectionFactory(redisHost, redisPort); |
35 | 32 |
} |
36 | 33 |
@Bean |
37 |
- @ConditionalOnProperty(name = "config.allow-multiple-logins", havingValue = "false", matchIfMissing = true) //redis 사용 안 할 경우 빈 등록x |
|
38 | 34 |
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) { |
39 | 35 |
RedisTemplate<String, String> redisTemp = new RedisTemplate<>(); |
40 | 36 |
redisTemp.setConnectionFactory(redisConnectionFactory); |
--- src/main/java/com/takensoft/common/config/SecurityConfig.java
+++ src/main/java/com/takensoft/common/config/SecurityConfig.java
... | ... | @@ -171,7 +171,7 @@ |
171 | 171 |
|
172 | 172 |
// 로그인 방식에 따라 필터 적용 (JWT or 세션) |
173 | 173 |
if (loginModeService.getLoginMode().equals("S")) { |
174 |
- http.addFilterBefore(new SessionAuthFilter(jwtUtil, redisTemplate, loginPolicyService), LoginFilter.class); |
|
174 |
+ http.addFilterBefore(new SessionAuthFilter(jwtUtil, redisTemplate, loginPolicyService, refreshTokenService), LoginFilter.class); |
|
175 | 175 |
} else { |
176 | 176 |
http.addFilterBefore(new JWTFilter(jwtUtil, appConfig, loginPolicyService, redisTemplate), LoginFilter.class); |
177 | 177 |
} |
--- src/main/java/com/takensoft/common/filter/LoginFilter.java
+++ src/main/java/com/takensoft/common/filter/LoginFilter.java
... | ... | @@ -163,7 +163,7 @@ |
163 | 163 |
refresh.setToken(refreshToken); |
164 | 164 |
|
165 | 165 |
|
166 |
- if ("S".equals(loginType)) { |
|
166 |
+ if (loginType.equals("S")) { |
|
167 | 167 |
HttpSession session = req.getSession(true); |
168 | 168 |
session.setAttribute("JWT_TOKEN", accessToken); |
169 | 169 |
|
--- src/main/java/com/takensoft/common/filter/SessionAuthFilter.java
+++ src/main/java/com/takensoft/common/filter/SessionAuthFilter.java
... | ... | @@ -3,12 +3,14 @@ |
3 | 3 |
import com.takensoft.cms.loginPolicy.service.LoginPolicyService; |
4 | 4 |
import com.takensoft.cms.mber.vo.MberAuthorVO; |
5 | 5 |
import com.takensoft.cms.mber.vo.MberVO; |
6 |
+import com.takensoft.cms.token.service.RefreshTokenService; |
|
6 | 7 |
import com.takensoft.common.util.JWTUtil; |
7 | 8 |
import jakarta.servlet.FilterChain; |
8 | 9 |
import jakarta.servlet.ServletException; |
9 | 10 |
import jakarta.servlet.http.HttpServletRequest; |
10 | 11 |
import jakarta.servlet.http.HttpServletResponse; |
11 | 12 |
import jakarta.servlet.http.HttpSession; |
13 |
+import org.springframework.beans.factory.annotation.Value; |
|
12 | 14 |
import org.springframework.data.redis.core.RedisTemplate; |
13 | 15 |
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
14 | 16 |
import org.springframework.security.core.context.SecurityContextHolder; |
... | ... | @@ -33,10 +35,15 @@ |
33 | 35 |
private final JWTUtil jwtUtil; |
34 | 36 |
private final RedisTemplate<String, String> redisTemplate; |
35 | 37 |
private final LoginPolicyService loginPolicyService; |
36 |
- public SessionAuthFilter(JWTUtil jwtUtil, RedisTemplate<String, String> redisTemplate, LoginPolicyService loginPolicyService) { |
|
38 |
+ private final RefreshTokenService refreshTokenService; |
|
39 |
+ |
|
40 |
+ @Value("${jwt.accessTime}") |
|
41 |
+ private long JWT_ACCESSTIME; |
|
42 |
+ public SessionAuthFilter(JWTUtil jwtUtil, RedisTemplate<String, String> redisTemplate, LoginPolicyService loginPolicyService, RefreshTokenService refreshTokenService) { |
|
37 | 43 |
this.jwtUtil = jwtUtil; |
38 | 44 |
this.redisTemplate = redisTemplate; |
39 | 45 |
this.loginPolicyService = loginPolicyService; |
46 |
+ this.refreshTokenService = refreshTokenService; |
|
40 | 47 |
} |
41 | 48 |
/** |
42 | 49 |
* @param request HttpServletRequest 객체 |
... | ... | @@ -50,20 +57,44 @@ |
50 | 57 |
@Override |
51 | 58 |
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { |
52 | 59 |
HttpSession session = request.getSession(false); |
60 |
+ |
|
61 |
+ // 세션이 없거나 JWT 토큰이 없으면 인증 실패 처리 |
|
53 | 62 |
if (session == null || session.getAttribute("JWT_TOKEN") == null) { |
54 | 63 |
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); |
55 | 64 |
return; |
56 | 65 |
} |
57 | 66 |
|
67 |
+ // 세션에서 JWT 토큰 가져오기 |
|
58 | 68 |
String accessToken = (String) session.getAttribute("JWT_TOKEN"); |
59 | 69 |
|
60 |
- // 토큰 만료 검증 |
|
70 |
+ // 만료 여부 검증 |
|
61 | 71 |
if ((Boolean) jwtUtil.getClaim(accessToken, "isExpired")) { |
62 |
- response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token expired"); |
|
63 |
- return; |
|
72 |
+ // 만료된 경우, refresh 토큰 검사 |
|
73 |
+ String refreshToken = refreshTokenService.getRefreshTokenFromCookie(request); |
|
74 |
+ |
|
75 |
+ if (refreshToken == null || (Boolean) jwtUtil.getClaim(refreshToken, "isExpired")) { |
|
76 |
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token expired"); |
|
77 |
+ return; |
|
78 |
+ } |
|
79 |
+ |
|
80 |
+ // Refresh 토큰이 유효하다면 새로운 Access 토큰 발급 |
|
81 |
+ try { |
|
82 |
+ String newAccessToken = jwtUtil.createJwt( |
|
83 |
+ "Authorization", |
|
84 |
+ (String) jwtUtil.getClaim(refreshToken, "mbrId"), |
|
85 |
+ (String) jwtUtil.getClaim(refreshToken, "lgnId"), |
|
86 |
+ (String) jwtUtil.getClaim(refreshToken, "mbrNm"), |
|
87 |
+ (List) jwtUtil.getClaim(refreshToken, "roles"), |
|
88 |
+ JWT_ACCESSTIME); |
|
89 |
+ session.setAttribute("JWT_TOKEN", newAccessToken); |
|
90 |
+ accessToken = newAccessToken; |
|
91 |
+ } catch (Exception e) { |
|
92 |
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Failed to refresh token"); |
|
93 |
+ return; |
|
94 |
+ } |
|
64 | 95 |
} |
65 | 96 |
|
66 |
- // 중복 로그인 허용 여부 확인 |
|
97 |
+ // 중복 로그인 여부 확인 (JWT 방식과 동일하게 Redis 체크) |
|
67 | 98 |
if (!loginPolicyService.getPolicy()) { |
68 | 99 |
String mbrId = (String) jwtUtil.getClaim(accessToken, "mbrId"); |
69 | 100 |
String storedToken = redisTemplate.opsForValue().get("jwt:" + mbrId); |
... | ... | @@ -82,7 +113,6 @@ |
82 | 113 |
mber.setAuthorList(roles); |
83 | 114 |
|
84 | 115 |
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(mber, null, mber.getAuthorities()); |
85 |
- |
|
86 | 116 |
SecurityContextHolder.getContext().setAuthentication(authToken); |
87 | 117 |
|
88 | 118 |
filterChain.doFilter(request, response); |
--- src/main/java/com/takensoft/common/util/JWTUtil.java
+++ src/main/java/com/takensoft/common/util/JWTUtil.java
... | ... | @@ -4,6 +4,7 @@ |
4 | 4 |
import com.takensoft.cms.mber.vo.MberVO; |
5 | 5 |
import io.jsonwebtoken.Claims; |
6 | 6 |
import io.jsonwebtoken.ExpiredJwtException; |
7 |
+import io.jsonwebtoken.JwtException; |
|
7 | 8 |
import io.jsonwebtoken.Jwts; |
8 | 9 |
import org.springframework.beans.factory.annotation.Value; |
9 | 10 |
import org.springframework.security.core.Authentication; |
... | ... | @@ -103,7 +104,6 @@ |
103 | 104 |
Claims claims; |
104 | 105 |
try { |
105 | 106 |
claims = Jwts.parser() |
106 |
- .clockSkewSeconds(60) |
|
107 | 107 |
.verifyWith(JWT_SECRET_KEY) |
108 | 108 |
.build() |
109 | 109 |
.parseSignedClaims(tkn) |
... | ... | @@ -111,6 +111,9 @@ |
111 | 111 |
} catch (ExpiredJwtException e) { |
112 | 112 |
// 만료된 토큰이라도 claims 꺼내기 가능 |
113 | 113 |
claims = e.getClaims(); |
114 |
+ } catch (JwtException | IllegalArgumentException e) { |
|
115 |
+ // 토큰 자체가 잘못된 경우 |
|
116 |
+ throw new IllegalArgumentException("Invalid token"); |
|
114 | 117 |
} |
115 | 118 |
|
116 | 119 |
switch (knd) { |
--- src/main/java/com/takensoft/common/util/SessionUtil.java
+++ src/main/java/com/takensoft/common/util/SessionUtil.java
... | ... | @@ -1,6 +1,7 @@ |
1 | 1 |
package com.takensoft.common.util; |
2 | 2 |
|
3 | 3 |
import jakarta.servlet.http.HttpSession; |
4 |
+import org.springframework.dao.DataAccessException; |
|
4 | 5 |
import org.springframework.stereotype.Component; |
5 | 6 |
|
6 | 7 |
import java.util.Map; |
... | ... | @@ -20,19 +21,36 @@ |
20 | 21 |
|
21 | 22 |
private final Map<String, HttpSession> sessionMap = new ConcurrentHashMap<>(); |
22 | 23 |
|
24 |
+ /** |
|
25 |
+ * @param mbrId - 사용자 Id |
|
26 |
+ * @param newSession - HTTP 세션 |
|
27 |
+ * |
|
28 |
+ * 기존 세션 있으면 강제 로그아웃 |
|
29 |
+ */ |
|
23 | 30 |
public synchronized void registerSession(String mbrId, HttpSession newSession) { |
24 |
- // 기존 세션 있으면 강제 로그아웃 |
|
25 | 31 |
HttpSession oldSession = sessionMap.get(mbrId); |
26 | 32 |
if (oldSession != null && oldSession != newSession) { |
27 | 33 |
oldSession.invalidate(); |
28 | 34 |
} |
29 | 35 |
sessionMap.put(mbrId, newSession); |
30 | 36 |
} |
31 |
- //로그아웃처리 |
|
37 |
+ /** |
|
38 |
+ * @param mbrId - 사용자 Id |
|
39 |
+ * |
|
40 |
+ * 로그아웃 |
|
41 |
+ */ |
|
32 | 42 |
public void removeSession(String mbrId) { |
33 |
- sessionMap.remove(mbrId); |
|
43 |
+ HttpSession session = sessionMap.get(mbrId); |
|
44 |
+ if (session != null) { |
|
45 |
+ session.invalidate(); // 세션 무효화 |
|
46 |
+ } |
|
47 |
+ sessionMap.remove(mbrId); // 이후 맵에서 제거 |
|
34 | 48 |
} |
35 |
- //전체 로그아웃 처리 |
|
49 |
+ |
|
50 |
+ /** |
|
51 |
+ * |
|
52 |
+ * 전체 로그아웃 |
|
53 |
+ */ |
|
36 | 54 |
public void invalidateAllSessions() { |
37 | 55 |
for (HttpSession session : sessionMap.values()) { |
38 | 56 |
if (session != null) { |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?