
--- src/main/java/kr/co/takensoft/ai/system/auth/service/RefreshService.java
+++ src/main/java/kr/co/takensoft/ai/system/auth/service/RefreshService.java
... | ... | @@ -20,4 +20,18 @@ |
20 | 20 |
* 토큰 재발급 |
21 | 21 |
*/ |
22 | 22 |
String tokenReissueProc(HttpServletRequest req, HttpServletResponse res) throws Exception; |
23 |
+ /** |
|
24 |
+ * @param req 클라이언트 요청 |
|
25 |
+ * @return String 쿠키 정보 |
|
26 |
+ * |
|
27 |
+ * 쿠키의 리프레시 토큰 추출 |
|
28 |
+ */ |
|
29 |
+ String getRefreshTokenFromCookie(HttpServletRequest request); |
|
30 |
+ |
|
31 |
+ /** |
|
32 |
+ * @param res 클라이언트 요청 응답 |
|
33 |
+ * |
|
34 |
+ * 쿠키의 리프레시 토큰 삭제 |
|
35 |
+ */ |
|
36 |
+ void clearRefreshTokenCookie(HttpServletResponse response); |
|
23 | 37 |
} |
--- src/main/java/kr/co/takensoft/ai/system/auth/service/impl/AuthServiceImpl.java
+++ src/main/java/kr/co/takensoft/ai/system/auth/service/impl/AuthServiceImpl.java
... | ... | @@ -67,7 +67,7 @@ |
67 | 67 |
* @param LoginDTO 로그인 정보 |
68 | 68 |
* @return Map<String, String> 액세스 토큰과 리프레시 토큰 |
69 | 69 |
* |
70 |
- * 사용자 회원가입 |
|
70 |
+ * 사용자 로그인 |
|
71 | 71 |
*/ |
72 | 72 |
public Map<String, String> memberLogin(LoginDTO loginDTO) throws Exception{ |
73 | 73 |
MemberVO member = authDAO.findMemberInfo(loginDTO.getLoginId()); |
--- src/main/java/kr/co/takensoft/ai/system/auth/service/impl/RefreshServiceImpl.java
+++ src/main/java/kr/co/takensoft/ai/system/auth/service/impl/RefreshServiceImpl.java
... | ... | @@ -1,6 +1,7 @@ |
1 | 1 |
package kr.co.takensoft.ai.system.auth.service.impl; |
2 | 2 |
|
3 | 3 |
import io.jsonwebtoken.ExpiredJwtException; |
4 |
+import jakarta.servlet.http.Cookie; |
|
4 | 5 |
import jakarta.servlet.http.HttpServletRequest; |
5 | 6 |
import jakarta.servlet.http.HttpServletResponse; |
6 | 7 |
import kr.co.takensoft.ai.system.auth.dao.AuthDAO; |
... | ... | @@ -36,7 +37,7 @@ |
36 | 37 |
private final RefreshDAO refreshDAO; |
37 | 38 |
|
38 | 39 |
public String tokenReissueProc(HttpServletRequest req, HttpServletResponse res) throws Exception { |
39 |
- String refreshToken = req.getHeader("RefreshToken"); |
|
40 |
+ String refreshToken = getRefreshTokenFromCookie(req); |
|
40 | 41 |
if (refreshToken == null) { |
41 | 42 |
throw new Exception("Refresh token이 전달되지 않았습니다."); |
42 | 43 |
} |
... | ... | @@ -53,16 +54,20 @@ |
53 | 54 |
// 만료 여부 체크 |
54 | 55 |
if (jwtUtil.isExpired(refreshToken)) { |
55 | 56 |
refreshDAO.deleteRefreshToken(memberId); |
57 |
+ // 만료된 쿠키 삭제 |
|
58 |
+ clearRefreshTokenCookie(res); |
|
56 | 59 |
throw new Exception("Refresh token이 만료되었습니다."); |
57 | 60 |
} |
58 | 61 |
} catch (ExpiredJwtException e) { |
59 | 62 |
// 여기서도 따로 만료된 경우 캐치 |
60 | 63 |
refreshDAO.deleteRefreshToken(memberId); |
64 |
+ clearRefreshTokenCookie(res); |
|
61 | 65 |
throw new Exception("Refresh token이 만료되었습니다."); |
62 | 66 |
} |
63 | 67 |
|
64 | 68 |
// 리프레시 토큰이 DB에 존재하는지 확인 |
65 | 69 |
if (refreshDAO.getRefreshTokenByUserId(memberId) == null) { |
70 |
+ clearRefreshTokenCookie(res); |
|
66 | 71 |
throw new Exception("리프레시 토큰이 존재하지 않습니다."); |
67 | 72 |
} |
68 | 73 |
|
... | ... | @@ -70,6 +75,7 @@ |
70 | 75 |
MemberVO member = authDAO.findMemberInfo(memberId); |
71 | 76 |
if (member == null) { |
72 | 77 |
refreshDAO.deleteRefreshToken(memberId); // 자동 로그아웃 |
78 |
+ clearRefreshTokenCookie(res); |
|
73 | 79 |
throw new Exception("유효한 사용자가 아닙니다."); |
74 | 80 |
} |
75 | 81 |
|
... | ... | @@ -84,4 +90,35 @@ |
84 | 90 |
|
85 | 91 |
return newAccessToken; |
86 | 92 |
} |
93 |
+ |
|
94 |
+ /** |
|
95 |
+ * @param req 클라이언트 요청 |
|
96 |
+ * |
|
97 |
+ * 쿠키의 리프레시 토큰 추출 |
|
98 |
+ */ |
|
99 |
+ public String getRefreshTokenFromCookie(HttpServletRequest request) { |
|
100 |
+ Cookie[] cookies = request.getCookies(); |
|
101 |
+ if (cookies != null) { |
|
102 |
+ for (Cookie cookie : cookies) { |
|
103 |
+ if ("refreshToken".equals(cookie.getName())) { |
|
104 |
+ return cookie.getValue(); |
|
105 |
+ } |
|
106 |
+ } |
|
107 |
+ } |
|
108 |
+ return null; |
|
109 |
+ } |
|
110 |
+ |
|
111 |
+ /** |
|
112 |
+ * @param res 클라이언트 요청 응답 |
|
113 |
+ * |
|
114 |
+ * 쿠키의 리프레시 토큰 삭제 |
|
115 |
+ */ |
|
116 |
+ public void clearRefreshTokenCookie(HttpServletResponse response) { |
|
117 |
+ Cookie cookie = new Cookie("refreshToken", null); |
|
118 |
+ //cookie.setHttpOnly(true); |
|
119 |
+ //cookie.setSecure(true); |
|
120 |
+ cookie.setPath("/"); |
|
121 |
+ cookie.setMaxAge(0); // 즉시 만료 |
|
122 |
+ response.addCookie(cookie); |
|
123 |
+ } |
|
87 | 124 |
} |
--- src/main/java/kr/co/takensoft/ai/system/auth/vo/MemberVO.java
+++ src/main/java/kr/co/takensoft/ai/system/auth/vo/MemberVO.java
... | ... | @@ -20,13 +20,14 @@ |
20 | 20 |
@AllArgsConstructor |
21 | 21 |
@NoArgsConstructor |
22 | 22 |
public class MemberVO { |
23 |
- private String memberId; |
|
24 |
- private String loginId; |
|
25 |
- private String password; |
|
26 |
- private String email; |
|
27 |
- private String phoneNumber; |
|
28 |
- private String memberName; |
|
29 |
- private String salt; |
|
30 |
- private String createdAt; |
|
31 |
- private String updatedDt; |
|
23 |
+ private String memberId; // 사용자 아이디 |
|
24 |
+ private String loginId; // 로그인 아이디 |
|
25 |
+ private String password; // 비밀번호 |
|
26 |
+ private String email; // 이메일 |
|
27 |
+ private String phoneNumber; // 전화번호 |
|
28 |
+ private String memberName; // 사용자 이름 |
|
29 |
+ private String salt; // 고유 솔트값 |
|
30 |
+ private String createdAt; // 생성일 |
|
31 |
+ private String updatedDt; // 수정일 |
|
32 |
+ private String useAt; // 사용여부 |
|
32 | 33 |
} |
--- src/main/java/kr/co/takensoft/ai/system/auth/web/AuthController.java
+++ src/main/java/kr/co/takensoft/ai/system/auth/web/AuthController.java
... | ... | @@ -1,8 +1,14 @@ |
1 | 1 |
package kr.co.takensoft.ai.system.auth.web; |
2 | 2 |
|
3 |
+import jakarta.servlet.http.Cookie; |
|
4 |
+import jakarta.servlet.http.HttpServletRequest; |
|
5 |
+import jakarta.servlet.http.HttpServletResponse; |
|
6 |
+import kr.co.takensoft.ai.system.auth.dao.RefreshDAO; |
|
3 | 7 |
import kr.co.takensoft.ai.system.auth.dto.LoginDTO; |
4 | 8 |
import kr.co.takensoft.ai.system.auth.service.AuthService; |
9 |
+import kr.co.takensoft.ai.system.auth.service.RefreshService; |
|
5 | 10 |
import kr.co.takensoft.ai.system.auth.vo.MemberVO; |
11 |
+import kr.co.takensoft.ai.system.common.util.JwtUtil; |
|
6 | 12 |
import lombok.RequiredArgsConstructor; |
7 | 13 |
import org.springframework.http.HttpStatus; |
8 | 14 |
import org.springframework.http.ResponseEntity; |
... | ... | @@ -29,6 +35,9 @@ |
29 | 35 |
public class AuthController { |
30 | 36 |
|
31 | 37 |
private final AuthService authService; |
38 |
+ private final RefreshService refreshService; |
|
39 |
+ private final RefreshDAO refreshDAO; |
|
40 |
+ private final JwtUtil jwtUtil; |
|
32 | 41 |
|
33 | 42 |
/** |
34 | 43 |
* @param member 사용자 정보 |
... | ... | @@ -50,12 +59,20 @@ |
50 | 59 |
* 사용자 로그인 |
51 | 60 |
*/ |
52 | 61 |
@PostMapping("/login.json") |
53 |
- public ResponseEntity<?> login(@RequestBody LoginDTO loginDTO) throws Exception { |
|
62 |
+ public ResponseEntity<?> login(@RequestBody LoginDTO loginDTO, HttpServletResponse response) throws Exception { |
|
54 | 63 |
HashMap<String, Object> result = new HashMap<>(); |
55 | 64 |
try { |
56 | 65 |
Map<String, String> tokens = authService.memberLogin(loginDTO); |
57 | 66 |
result.put("accessToken", tokens.get("accessToken")); |
58 |
- result.put("refreshToken", tokens.get("refreshToken")); |
|
67 |
+ Cookie refreshTokenCookie = new Cookie("refreshToken", tokens.get("refreshToken")); |
|
68 |
+ //refreshTokenCookie.setHttpOnly(true); // JavaScript 접근 차단 |
|
69 |
+ //refreshTokenCookie.setSecure(true); // HTTPS에서만 전송 |
|
70 |
+ refreshTokenCookie.setPath("/"); // 전체 경로에서 사용 |
|
71 |
+ refreshTokenCookie.setMaxAge(24 * 60 * 60); // 1일 (초 단위) |
|
72 |
+ refreshTokenCookie.setAttribute("SameSite", "Strict"); // CSRF 방지 |
|
73 |
+ |
|
74 |
+ response.addCookie(refreshTokenCookie); |
|
75 |
+ |
|
59 | 76 |
return new ResponseEntity<>(result, HttpStatus.OK); |
60 | 77 |
} catch (Exception e) { |
61 | 78 |
result.put("message", e.getMessage()); |
... | ... | @@ -63,5 +80,39 @@ |
63 | 80 |
} |
64 | 81 |
} |
65 | 82 |
|
83 |
+ /** |
|
84 |
+ * @param req, res 로그아웃 요청과 응답 |
|
85 |
+ * @return ResponseEntity 로그아웃 결과 |
|
86 |
+ * |
|
87 |
+ * 사용자 로그인 |
|
88 |
+ */ |
|
89 |
+ @PostMapping("/logout.json") |
|
90 |
+ public ResponseEntity<?> logout(HttpServletRequest req, HttpServletResponse res) { |
|
91 |
+ try { |
|
92 |
+ // 쿠키에서 리프레시 토큰 추출 |
|
93 |
+ String refreshToken = refreshService.getRefreshTokenFromCookie(req); |
|
94 |
+ if (refreshToken != null) { |
|
95 |
+ String memberId = jwtUtil.getUsid(refreshToken); |
|
96 |
+ // DB에서 리프레시 토큰 삭제 |
|
97 |
+ refreshDAO.deleteRefreshToken(memberId); |
|
98 |
+ } |
|
99 |
+ |
|
100 |
+ // 쿠키 삭제 |
|
101 |
+ Cookie cookie = new Cookie("refreshToken", null); |
|
102 |
+ cookie.setHttpOnly(true); |
|
103 |
+ cookie.setSecure(true); |
|
104 |
+ cookie.setPath("/"); |
|
105 |
+ cookie.setMaxAge(0); |
|
106 |
+ res.addCookie(cookie); |
|
107 |
+ |
|
108 |
+ Map<String, Object> result = new HashMap<>(); |
|
109 |
+ result.put("message", "로그아웃이 완료되었습니다."); |
|
110 |
+ return new ResponseEntity<>(result, HttpStatus.OK); |
|
111 |
+ } catch (Exception e) { |
|
112 |
+ Map<String, Object> result = new HashMap<>(); |
|
113 |
+ result.put("message", "로그아웃 처리 중 오류가 발생했습니다."); |
|
114 |
+ return new ResponseEntity<>(result, HttpStatus.INTERNAL_SERVER_ERROR); |
|
115 |
+ } |
|
116 |
+ } |
|
66 | 117 |
|
67 | 118 |
}(No newline at end of file) |
--- src/main/resources/mybatis/mapper/auth/auth-SQL.xml
+++ src/main/resources/mybatis/mapper/auth/auth-SQL.xml
... | ... | @@ -16,7 +16,8 @@ |
16 | 16 |
phone_number, |
17 | 17 |
member_name, |
18 | 18 |
created_at, |
19 |
- updated_at |
|
19 |
+ updated_at, |
|
20 |
+ use_at |
|
20 | 21 |
) |
21 | 22 |
VALUES ( |
22 | 23 |
#{memberId}, |
... | ... | @@ -27,7 +28,8 @@ |
27 | 28 |
#{phoneNumber}, |
28 | 29 |
#{memberName}, |
29 | 30 |
CURRENT_TIMESTAMP, |
30 |
- CURRENT_TIMESTAMP |
|
31 |
+ CURRENT_TIMESTAMP, |
|
32 |
+ 'Y' |
|
31 | 33 |
) |
32 | 34 |
</insert> |
33 | 35 |
|
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?