Skip to content

Commit 18f3242

Browse files
authored
Merge pull request #28 from studio-recoding/feat-main-api
[🚀feat] JWT 토큰 관련 에러 로직 추가
2 parents 130fd45 + 3536842 commit 18f3242

18 files changed

+282
-105
lines changed

src/main/java/Ness/Backend/domain/auth/AuthController.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import Ness.Backend.domain.auth.dto.RegisterRequestDto;
55
import Ness.Backend.domain.member.entity.Member;
66
import Ness.Backend.global.auth.AuthUser;
7-
import Ness.Backend.global.common.response.CommonResponse;
7+
import Ness.Backend.global.response.ApiResponse;
88
import io.swagger.v3.oas.annotations.Operation;
99
import io.swagger.v3.oas.annotations.tags.Tag;
1010
import lombok.RequiredArgsConstructor;
@@ -20,7 +20,7 @@ public class AuthController {
2020
/* 스프링 시큐리티는 /login 경로의 요청을 받아 인증 과정을 처리 */
2121
@PostMapping("/login")
2222
@Operation(summary = "로그인 API", description = "사용자의 이메일과 비밀번호로 로그인하는 API 입니다.")
23-
public CommonResponse<?> userLogin(@RequestBody LoginRequestDto loginRequestDto) {
23+
public ApiResponse<?> userLogin(@RequestBody LoginRequestDto loginRequestDto) {
2424
return authService.login(loginRequestDto);
2525
}
2626

@@ -32,7 +32,7 @@ public void logout(@AuthUser Member member) {
3232

3333
@PostMapping("/signup")
3434
@Operation(summary = "회원가입 API", description = "사용자의 이메일과 비밀번호로 회원가입하는 API 입니다.")
35-
public CommonResponse<?> userRegister(@RequestBody RegisterRequestDto registerRequestDto) {
35+
public ApiResponse<?> userRegister(@RequestBody RegisterRequestDto registerRequestDto) {
3636
return authService.signUp(registerRequestDto);
3737
}
3838

src/main/java/Ness/Backend/domain/auth/AuthService.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import Ness.Backend.domain.auth.security.AuthDetails;
66
import Ness.Backend.domain.member.MemberRepository;
77
import Ness.Backend.domain.member.entity.Member;
8-
import Ness.Backend.global.common.response.CommonResponse;
8+
import Ness.Backend.global.response.ApiResponse;
99
import lombok.RequiredArgsConstructor;
1010
import org.springframework.dao.DataIntegrityViolationException;
1111
import org.springframework.http.HttpStatus;
@@ -25,7 +25,7 @@ public class AuthService {
2525
private final AuthenticationManagerBuilder authenticationManagerBuilder;
2626

2727
@Transactional
28-
public CommonResponse<?> signUp(RegisterRequestDto registerRequestDto) {
28+
public ApiResponse<?> signUp(RegisterRequestDto registerRequestDto) {
2929
try {
3030
/* 빌더 패턴을 사용해 MemberRole을 넘겨주지 않아도 객체 생성 가능 */
3131
Member member = Member.builder()
@@ -35,20 +35,20 @@ public CommonResponse<?> signUp(RegisterRequestDto registerRequestDto) {
3535

3636
memberRepository.save(member);
3737

38-
return CommonResponse.postResponse(
38+
return ApiResponse.postResponse(
3939
HttpStatus.OK.value(),
4040
"회원가입이 완료되었습니다."); //200
4141

4242
} catch (DataIntegrityViolationException e) {
4343
/* 중복된 이메일 값이 삽입되려고 할 때 발생하는 예외 처리 */
44-
return CommonResponse.postResponse(
44+
return ApiResponse.postResponse(
4545
HttpStatus.CONFLICT.value(),
4646
"이미 회원 가입된 유저입니다."); //409
4747
}
4848
}
4949

5050
@Transactional
51-
public CommonResponse<?> login(LoginRequestDto loginRequestDto) {
51+
public ApiResponse<?> login(LoginRequestDto loginRequestDto) {
5252
String email = loginRequestDto.getEmail();
5353
String password = loginRequestDto.getPassword();
5454

@@ -69,12 +69,12 @@ public CommonResponse<?> login(LoginRequestDto loginRequestDto) {
6969
String authenticatedEmail = authDetails.getMember().getEmail();
7070

7171
/* JWT 토큰 반환 */
72-
return CommonResponse.postResponse(
72+
return ApiResponse.postResponse(
7373
HttpStatus.OK.value(),
7474
"로그인에 성공했습니다."); //200
7575
}
7676

77-
return CommonResponse.postResponse(
77+
return ApiResponse.postResponse(
7878
HttpStatus.UNAUTHORIZED.value(),
7979
"로그인에 실패했습니다. 이메일 또는 비밀번호가 일치하는지 확인해주세요."); //401
8080
}

src/main/java/Ness/Backend/domain/auth/jwt/JwtAuthorizationFilter.java

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,27 @@
33
import Ness.Backend.domain.auth.security.AuthDetailService;
44
import Ness.Backend.domain.auth.security.AuthDetails;
55
import Ness.Backend.domain.member.entity.Member;
6+
import Ness.Backend.global.error.ErrorCode;
7+
import static Ness.Backend.global.error.FilterExceptionHandler.setResponse;
8+
9+
import com.auth0.jwt.exceptions.SignatureVerificationException;
10+
import com.auth0.jwt.exceptions.TokenExpiredException;
611
import jakarta.servlet.FilterChain;
712
import jakarta.servlet.ServletException;
813
import jakarta.servlet.http.HttpServletRequest;
914
import jakarta.servlet.http.HttpServletResponse;
15+
import lombok.extern.slf4j.Slf4j;
1016
import org.springframework.security.authentication.AuthenticationManager;
1117
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
1218
import org.springframework.security.core.Authentication;
1319
import org.springframework.security.core.context.SecurityContextHolder;
1420
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
15-
1621
import java.io.IOException;
1722

1823
/* 사용자의 권한 부여
1924
* 요청에 포함된 JWT 토큰을 검증하고, 토큰에서 추출한 권한 정보를 기반으로 사용자에 대한 권한을 확인
2025
* 모든 사용자가 모든 리소스에 대한 권한을 가지는 것은 아님-특정 리소스에 대한 권한만 가지도록 해야 함*/
26+
@Slf4j
2127
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
2228
private JwtTokenProvider jwtTokenProvider;
2329

@@ -41,19 +47,34 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
4147

4248
/* 헤더 안의 JWT 토큰을 검증해 정상적인 사용자인지 확인 */
4349
String jwtToken = jwtHeader.substring(7);
44-
Member tokenMember = jwtTokenProvider.validJwtToken(jwtToken);
4550

46-
if(tokenMember != null){ //토큰이 정상일 경우
47-
AuthDetails authDetails = new AuthDetails(tokenMember);
51+
try {
52+
Member tokenMember = jwtTokenProvider.validJwtToken(jwtToken);
53+
54+
if(tokenMember != null){ //토큰이 정상일 경우
55+
AuthDetails authDetails = new AuthDetails(tokenMember);
4856

49-
/* JWT 토큰 서명이 정상이면 Authentication 객체 생성 */
50-
Authentication authentication = new UsernamePasswordAuthenticationToken(authDetails, null, authDetails.getAuthorities());
57+
/* JWT 토큰 서명이 정상이면 Authentication 객체 생성 */
58+
Authentication authentication = new UsernamePasswordAuthenticationToken(authDetails, null, authDetails.getAuthorities());
5159

52-
/* 시큐리티 세션에 Authentication 을 저장 */
53-
SecurityContextHolder.getContext().setAuthentication(authentication);
60+
/* 시큐리티 세션에 Authentication 을 저장 */
61+
SecurityContextHolder.getContext().setAuthentication(authentication);
62+
}
63+
64+
chain.doFilter(request, response);
5465

55-
SecurityContextHolder.getContext().setAuthentication(authentication);
66+
} catch (TokenExpiredException e){
67+
log.error("EXPIRED_TOKEN");
68+
request.setAttribute("exception", ErrorCode.EXPIRED_TOKEN.getCode());
69+
setResponse(response, ErrorCode.EXPIRED_TOKEN);
70+
} catch (SignatureVerificationException e){
71+
log.error("INVALID_TOKEN_SIGNATURE");
72+
request.setAttribute("exception", ErrorCode.INVALID_TOKEN_SIGNATURE.getCode());
73+
setResponse(response, ErrorCode.INVALID_TOKEN_SIGNATURE);
74+
} catch (Exception e){
75+
log.error("TOKEN_EXCEPTION");
76+
request.setAttribute("exception", ErrorCode.TOKEN_ERROR.getCode());
77+
setResponse(response, ErrorCode.TOKEN_ERROR);
5678
}
57-
chain.doFilter(request, response);
5879
}
5980
}

src/main/java/Ness/Backend/domain/auth/jwt/JwtTokenProvider.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.auth0.jwt.algorithms.Algorithm;
88
import jakarta.annotation.PostConstruct;
99
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
1011
import org.springframework.beans.factory.annotation.Value;
1112
import org.springframework.web.client.HttpServerErrorException;
1213

@@ -19,6 +20,7 @@
1920

2021
/* JSON Web Token (JWT)을 생성하고 검증하는 역할 */
2122
@RequiredArgsConstructor
23+
@Slf4j
2224
public class JwtTokenProvider {
2325

2426
private final MemberRepository memberRepository;
@@ -127,21 +129,23 @@ public Member validJwtToken(String jwtToken){
127129
/* email 값이 null이 아닌지 확인 */
128130
String authKey = getAuthKeyClaim(jwtToken);
129131
if (authKey == null){ //null 값이라면 올바른 jwtToken이 아님
130-
//throw new UnauthorizedException(ErrorCode.INVALID_AUTH_TOKEN);
132+
//TODO: 명시적 throw new 에러를 하지 않아도 자동 에러 감지되는 이유 파악 필요
131133
return null;
132134
}
133135

134136
/* JWT_EXPIRATION_TIME이 지나지 않았는지 확인 */
135-
Date expiresAt =getExpireTimeClaim(jwtToken);
137+
Date expiresAt = getExpireTimeClaim(jwtToken);
136138
if (!this.validExpiredTime(expiresAt)) { //만료시간이 지났다면 올바른 jwtToken이 아님
137-
//throw new UnauthorizedException(ErrorCode.EXPIRED_TOKEN);
138139
return null;
139140
}
140141

141142
/* email 값이 정상적으로 있고, JWT_EXPIRATION_TIME도 지나지 않았다면,
142143
* 해당 토큰의 email 정보를 가진 맴버가 있는지 DB에서 확인 */
143-
//Member tokenUser = memberRepository.findMemberByEmail(email);
144+
Member tokenMember = memberRepository.findMemberByEmail(authKey);
145+
if (tokenMember == null) { //DB에 해당 맴버가 없다면 올바른 jwtToken이 아님
146+
return null;
147+
}
144148

145-
return memberRepository.findMemberByEmail(authKey);
149+
return tokenMember;
146150
}
147151
}

src/main/java/Ness/Backend/domain/auth/oAuth/OAuth2Service.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import Ness.Backend.global.auth.oAuth.naver.NaverOAuthApi;
1818
import Ness.Backend.global.auth.oAuth.naver.NaverResourceApi;
1919
import Ness.Backend.global.error.ErrorCode;
20-
import Ness.Backend.global.error.exception.UnauthorizedException;
20+
import Ness.Backend.global.error.exception.UnauthorizedAccessException;
2121
import lombok.RequiredArgsConstructor;
2222
import lombok.extern.slf4j.Slf4j;
2323
import org.springframework.core.env.Environment;
@@ -201,7 +201,7 @@ private ResourceDto getUserResource(String accessToken, String registration) {
201201
public void logout(Member member) {
202202
/* refreshToken 만료 여부 확인 */
203203
if (!refreshTokenRepository.existsById(member.getId())) {
204-
throw new UnauthorizedException(ErrorCode.INVALID_REFRESH_TOKEN);
204+
throw new UnauthorizedAccessException(ErrorCode.INVALID_REFRESH_TOKEN);
205205
}
206206

207207
refreshTokenRepository.deleteById(member.getId());
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package Ness.Backend.domain.auth.security;
2+
3+
import Ness.Backend.global.error.ErrorCode;
4+
import Ness.Backend.global.error.exception.ExpiredTokenException;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import jakarta.servlet.ServletException;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import jakarta.servlet.http.HttpServletResponse;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.security.core.AuthenticationException;
11+
import org.springframework.security.web.AuthenticationEntryPoint;
12+
13+
import java.io.IOException;
14+
15+
@Slf4j
16+
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
17+
//BasicAuthenticationEntryPoint
18+
//AuthenticationEntryPoint
19+
@Override
20+
public void commence(HttpServletRequest request,
21+
HttpServletResponse response,
22+
AuthenticationException authenticationException) throws IOException, ServletException {
23+
log.error("JwtAuthenticationEntryPoint 호출");
24+
String exception = (String)request.getAttribute("exception");
25+
if(exception.equals(ErrorCode.EXPIRED_TOKEN.getCode())) {
26+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
27+
response.setCharacterEncoding("utf-8");
28+
response.setContentType("application/json");
29+
ExpiredTokenException expiredTokenException = new ExpiredTokenException();
30+
ObjectMapper objectMapper = new ObjectMapper();
31+
String result = objectMapper.writeValueAsString(expiredTokenException);
32+
response.getWriter().write(result);
33+
}
34+
}
35+
}

src/main/java/Ness/Backend/domain/auth/security/SecurityConfig.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Excepti
8181
.sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) //세션을 생성하지 않음->토큰 기반 인증 필요
8282
.addFilter(new JwtAuthenticationFilter(authenticationManager(), jwtTokenProvider(), refreshTokenService)) //사용자 인증
8383
.addFilter(new JwtAuthorizationFilter(authenticationManager(), jwtTokenProvider(), authDetailService)) //사용자 권한 부여
84+
/*
85+
.exceptionHandling((exceptionHandling) ->
86+
87+
exceptionHandling.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
88+
) //인가(Authorization)가 실패시 실행, 항상 JwtAuthenticationFilter 뒤에 설정되어 있어야 함.
89+
*/
8490
.authorizeHttpRequests(requests -> requests
8591
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
8692
//.requestMatchers("/signup/**", "/login/**").permitAll() // 회원가입 및 로그인 경로는 인증 생략

src/main/java/Ness/Backend/domain/chat/ChatController.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,27 @@ public ResponseEntity<GetChatListDto> getUserChat(){
2727
return new ResponseEntity<>(oneUserChats, HttpStatusCode.valueOf(200));
2828
}
2929

30-
@PostMapping("/dev")
31-
@Operation(summary = "새로운 채팅으로 AI와 통신", description = "새로운 채팅 내역을 저장하고, AI의 응답을 받는 API 입니다.")
32-
public ResponseEntity<GetAiChatDto> postAiChat(@RequestBody PostUserChatDto postUserChatDto){
33-
GetAiChatDto answer = chatService.postNewUserChat(1L, postUserChatDto);
34-
return new ResponseEntity<>(answer, HttpStatusCode.valueOf(200));
35-
}
36-
3730
@GetMapping("")
3831
@Operation(summary = "한 사용자의 채팅 내역", description = "한 사용자의 일주일치 채팅 내역을 반환하는 API 입니다.")
3932
public ResponseEntity<GetChatListDto> getUserChat(@AuthUser Member member){
4033
GetChatListDto oneUserChats = chatService.getOneWeekUserChat(member.getId());
4134
return new ResponseEntity<>(oneUserChats, HttpStatusCode.valueOf(200));
4235
}
4336

37+
/*
4438
@PostMapping("")
4539
@Operation(summary = "새로운 채팅으로 AI와 통신", description = "새로운 채팅 내역을 저장하고, AI의 응답을 받는 API 입니다.")
4640
public ResponseEntity<GetAiChatDto> postAiChat(@AuthUser Member member, @RequestBody PostUserChatDto postUserChatDto){
4741
GetAiChatDto answer = chatService.postNewUserChat(member.getId(), postUserChatDto);
4842
return new ResponseEntity<>(answer, HttpStatusCode.valueOf(200));
43+
}*/
44+
45+
46+
// 03-29 수정 버전
47+
@PostMapping("")
48+
@Operation(summary = "새로운 채팅으로 AI와 통신", description = "새로운 채팅 내역을 저장하고, 새로운 내역을 포함해서 AI의 응답을 받는 API 입니다.")
49+
public ResponseEntity<GetChatListDto> postAiChat(@AuthUser Member member, @RequestBody PostUserChatDto postUserChatDto) {
50+
GetChatListDto oneUserChats = chatService.postNewUserChat(member.getId(), postUserChatDto);
51+
return new ResponseEntity<>(oneUserChats, HttpStatusCode.valueOf(200));
4952
}
5053
}

src/main/java/Ness/Backend/domain/chat/ChatRepository.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public interface ChatRepository extends JpaRepository<Chat, Long>{
1515
// 특정 맴버의 일주일치 데이터만 반환
1616
@Query( value = "SELECT * FROM chat " +
1717
"WHERE member_id = :memberId " +
18-
"AND created_date BETWEEN DATE_ADD(NOW(), INTERVAL -1 WEEK) AND NOW() " +
18+
"AND created_date >= DATE_SUB(NOW(), INTERVAL 1 WEEK) " +
19+
//"AND created_date BETWEEN DATE_ADD(NOW(), INTERVAL -1 WEEK) AND NOW() " +
1920
"ORDER BY created_date ASC",
2021
nativeQuery = true)
2122
List<Chat> findOneWeekUserChatsByMember_Id(

src/main/java/Ness/Backend/domain/chat/ChatService.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import Ness.Backend.domain.chat.dto.request.PostUserChatDto;
44
import Ness.Backend.domain.chat.dto.request.PostFastApiUserChatDto;
5-
import Ness.Backend.domain.chat.dto.response.GetAiChatDto;
65
import Ness.Backend.domain.chat.dto.response.GetChatDto;
76
import Ness.Backend.domain.chat.dto.response.GetChatListDto;
87
import Ness.Backend.domain.chat.dto.response.PostFastApiAiChatDto;
98
import Ness.Backend.domain.chat.entity.Chat;
9+
import Ness.Backend.domain.chat.entity.ChatType;
1010
import Ness.Backend.domain.member.MemberRepository;
1111
import Ness.Backend.domain.member.entity.Member;
1212
import Ness.Backend.global.fastApi.FastApiChatApi;
@@ -22,7 +22,7 @@
2222
@Service
2323
@RequiredArgsConstructor
2424
@Slf4j
25-
@Transactional(readOnly = true)
25+
@Transactional
2626
public class ChatService {
2727
private final ChatRepository chatRepository;
2828
private final MemberRepository memberRepository;
@@ -43,29 +43,36 @@ public GetChatListDto getOneWeekUserChat(Long id){
4343
return new GetChatListDto(getChatDtos);
4444
}
4545

46-
@Transactional
47-
public GetAiChatDto postNewUserChat(Long id, PostUserChatDto postUserChatDto){
46+
public GetChatListDto postNewUserChat(Long id, PostUserChatDto postUserChatDto){
4847
Member memberEntity = memberRepository.findMemberById(id);
49-
//새로운 채팅 생성
50-
Chat newChat = Chat.builder()
48+
//새로운 유저 채팅 저장
49+
Chat newUserChat = Chat.builder()
5150
.createdDate(LocalDateTime.now(ZoneId.of("Asia/Seoul"))
5251
.atZone(ZoneId.of("Asia/Seoul")))
5352
.text(postUserChatDto.getText())
5453
.chatType(postUserChatDto.getChatType())
5554
.member(memberEntity)
5655
.build();
57-
chatRepository.save(newChat);
56+
57+
chatRepository.save(newUserChat);
5858

5959
String answer = postNewAiChat(id, postUserChatDto.getText());
6060
String parsedAnswer = parseAiChat(answer);
6161

62-
GetAiChatDto getAiChatDto = GetAiChatDto.builder()
63-
.answer(parsedAnswer)
62+
//AI 챗 답변 저장
63+
Chat newAiChat = Chat.builder()
64+
.createdDate(LocalDateTime.now(ZoneId.of("Asia/Seoul"))
65+
.atZone(ZoneId.of("Asia/Seoul")))
66+
.text(parsedAnswer)
67+
.chatType(ChatType.AI)
68+
.member(memberEntity)
6469
.build();
6570

66-
return getAiChatDto;
71+
chatRepository.save(newAiChat);
72+
return getOneWeekUserChat(id);
6773
}
6874

75+
/* ChatGPT가 답변의 앞뒤에 \를 포함시키므로, 제거 필요 */
6976
public String parseAiChat(String text){
7077
return text.replace("\"", "");
7178
}

0 commit comments

Comments
 (0)