Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Map;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
Expand Down Expand Up @@ -53,7 +52,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
if (token != null) { // refreshToken도 없어 AccessToken이 아예 없는 경우 지나가기
try {
// 토큰 검증 | 검증 성공 시 SecurityContext에 인증 정보 저장
String userPrincipal = jwtUtil.getSubjectFromToken(token);
String userPrincipal = jwtUtil.getSubjectFromTokenWithAuth(token);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userPrincipal, null, null);

Expand Down
51 changes: 35 additions & 16 deletions src/main/java/com/mtvs/devlinkbackend/config/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,33 @@ public JwtUtil(EpicGamesJWKCache jwkCache) {
}

// JWT 서명 및 검증을 통한 Claims 추출
public Map<String, Object> getClaimsFromToken(String token) throws Exception {
SignedJWT signedJWT = SignedJWT.parse(token);
JWK jwk = jwkCache.getCachedJWKSet().getKeyByKeyId(signedJWT.getHeader().getKeyID());

if (jwk == null || !JWSAlgorithm.RS256.equals(jwk.getAlgorithm())) {
throw new RuntimeException("JWK key is missing or invalid algorithm");
}
public Map<String, Object> getClaimsFromTokenWithAuth(String token) throws Exception {
// Claims 검증
JWTClaimsSet claims = getClaimsFromToken(token);
validateClaims(claims);

JWSVerifier verifier = new RSASSAVerifier(jwk.toRSAKey());
if (!signedJWT.verify(verifier)) {
throw new RuntimeException("JWT signature verification failed");
}
// 검증이 완료되었을 경우 모든 Claims을 Map으로 변환하여 반환
return convertClaimsToMap(claims);
}

// JWT 서명 및 검증을 통한 Claims 추출
public Map<String, Object> getClaimsFromTokenWithoutAuth(String token) throws Exception {
// Claims 검증
JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
validateClaims(claims);
JWTClaimsSet claims = getClaimsFromToken(token);

// 검증이 완료되었을 경우 모든 Claims을 Map으로 변환하여 반환
return convertClaimsToMap(claims);
}

// 'sub' 값 검증 (예제)
public String getSubjectFromToken(String token) throws Exception {
Map<String, Object> claims = getClaimsFromToken(token);
// 검증된 'sub' 값, accountId 반환
public String getSubjectFromTokenWithAuth(String token) throws Exception {
Map<String, Object> claims = getClaimsFromTokenWithAuth(token);
return (String) claims.get("sub");
}

// 검증된 'sub' 값, accountId 반환
public String getSubjectFromTokenWithoutAuth(String token) throws Exception {
Map<String, Object> claims = getClaimsFromTokenWithoutAuth(token);
return (String) claims.get("sub");
}

Expand Down Expand Up @@ -79,6 +82,22 @@ private void validateClaims(JWTClaimsSet claims) throws BadJWTException {
}
}

private JWTClaimsSet getClaimsFromToken(String token) throws Exception {
SignedJWT signedJWT = SignedJWT.parse(token);
JWK jwk = jwkCache.getCachedJWKSet().getKeyByKeyId(signedJWT.getHeader().getKeyID());

if (jwk == null || !JWSAlgorithm.RS256.equals(jwk.getAlgorithm())) {
throw new RuntimeException("JWK key is missing or invalid algorithm");
}

JWSVerifier verifier = new RSASSAVerifier(jwk.toRSAKey());
if (!signedJWT.verify(verifier)) {
throw new RuntimeException("JWT signature verification failed");
}

return signedJWT.getJWTClaimsSet();
}

// Claims를 Map<String, Object> 형식으로 변환하는 메서드
private Map<String, Object> convertClaimsToMap(JWTClaimsSet claims) {
Map<String, Object> claimsMap = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public SecurityConfig(UserService userService, OAuth2AuthorizedClientService aut
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers("/", "/login").permitAll()
.requestMatchers("/", "/question/**", "/login").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2Login -> oauth2Login
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,13 @@ public Oauth2UserController(EpicGamesTokenService epicGamesTokenService, UserSer
this.userService = userService;
}

// 로컬에 저장된 user 정보 가져오는 API
// 로컬 user 정보 가져오는 API
@GetMapping("/local/user-info")
public ResponseEntity<?> getLocalUserInfo(@RequestHeader("Authorization") String accessToken) {
public ResponseEntity<?> getLocalUserInfo(@RequestHeader("Authorization") String authorizationHeader) {
try {
// JWT 토큰에서 사용자 정보 추출
Map<String, Object> claims = epicGamesTokenService.validateAndParseToken(extractToken(accessToken));
String token = extractToken(authorizationHeader);

if(userService.findUserByAccessToken(extractToken(accessToken)) != null) {
userService.registUserByAccessToken(extractToken(accessToken));
}

return ResponseEntity.ok().build();
return ResponseEntity.ok(userService.findUserByAccessToken(token));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid token");
}
Expand All @@ -41,11 +36,11 @@ public ResponseEntity<?> getLocalUserInfo(@RequestHeader("Authorization") String
// epicgames 계정 정보 가져오는 API
@GetMapping("/epicgames/user-info")
public ResponseEntity<?> getEpicGamesUserInfo(
@RequestHeader("Authorization") String accessToken) {
@RequestHeader("Authorization") String authorizationHeader) {

try {
Map<String, Object> userAccount =
epicGamesTokenService.getEpicGamesUserAccount(extractToken(accessToken));
epicGamesTokenService.getEpicGamesUserAccount(extractToken(authorizationHeader));

return ResponseEntity.ok(userAccount);
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public EpicGamesTokenService(EpicGamesJWKCache jwkCache, JwtUtil jwtUtil) {
// 오프라인 JWT 검증 및 파싱 메서드
public Map<String, Object> validateAndParseToken(String token) throws Exception {
// JWT 토큰 검증 및 파싱하여 Claims를 추출
return jwtUtil.getClaimsFromToken(token);
return jwtUtil.getClaimsFromTokenWithoutAuth(token);
}

public String getAccessTokenByRefreshToken(String refreshToken) {
Expand Down Expand Up @@ -110,7 +110,7 @@ public Map<String, Object> getEpicGamesUserAccount(String accessToken) {

try {
response = restTemplate.exchange(
"https://api.epicgames.dev/epic/id/v2/accounts?accountId=" + jwtUtil.getClaimsFromToken(accessToken),
"https://api.epicgames.dev/epic/id/v2/accounts?accountId=" + jwtUtil.getClaimsFromTokenWithAuth(accessToken),
HttpMethod.GET,
request,
Map.class
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public UserService(UserRepository userRepository, JwtUtil jwtUtil) {

public User registUserByAccessToken(String accessToken) {
try {
String accountId = jwtUtil.getSubjectFromToken(accessToken);
String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(accessToken);
return userRepository.save(new User(
accountId
));
Expand All @@ -30,7 +30,7 @@ public User registUserByAccessToken(String accessToken) {

public User findUserByAccessToken(String accessToken) {
try {
String accountId = jwtUtil.getSubjectFromToken(accessToken);
String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(accessToken);
return userRepository.findUserByAccountId(accountId);
} catch (Exception e) {
throw new RuntimeException(e);
Expand All @@ -39,7 +39,7 @@ public User findUserByAccessToken(String accessToken) {

public void updateUserByAccessToken(String accessToken, UserUpdateRequestDTO userUpdateRequestDTO) {
try {
String accountId = jwtUtil.getSubjectFromToken(accessToken);
String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(accessToken);
User user = userRepository.findUserByAccountId(accountId);
if(user != null) {
user.setEmail(userUpdateRequestDTO.getEmail());
Expand All @@ -53,7 +53,7 @@ public void updateUserByAccessToken(String accessToken, UserUpdateRequestDTO use

public void deleteUserByAccessToken(String accessToken) {
try {
String accountId = jwtUtil.getSubjectFromToken(accessToken);
String accountId = jwtUtil.getSubjectFromTokenWithAuth(accessToken);
userRepository.deleteUserByAccountId(accountId);
} catch (Exception e) {
throw new RuntimeException(e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.mtvs.devlinkbackend.question.controller;

import com.mtvs.devlinkbackend.config.JwtUtil;
import com.mtvs.devlinkbackend.oauth2.service.EpicGamesTokenService;
import com.mtvs.devlinkbackend.question.dto.QuestionRegistRequestDTO;
import com.mtvs.devlinkbackend.question.dto.QuestionUpdateRequestDTO;
import com.mtvs.devlinkbackend.question.entity.Question;
Expand Down Expand Up @@ -31,14 +30,14 @@ public ResponseEntity<Question> createQuestion(
@RequestBody QuestionRegistRequestDTO questionRegistRequestDTO,
@RequestHeader(name = "Authorization") String accessToken) throws Exception {

String accountId = jwtUtil.getSubjectFromToken(accessToken);
String accountId = jwtUtil.getSubjectFromTokenWithAuth(accessToken);
Question createdQuestion = questionService.registQuestion(questionRegistRequestDTO, accountId);
return ResponseEntity.ok(createdQuestion);
}

// Retrieve a question by ID
@GetMapping("/{questionId}")
public ResponseEntity<Question> getQuestionById(@PathVariable long questionId) {
public ResponseEntity<Question> getQuestionById(@PathVariable Long questionId) {
Question question = questionService.findQuestionByQuestionId(questionId);
if (question != null) {
return ResponseEntity.ok(question);
Expand All @@ -60,7 +59,7 @@ public ResponseEntity<List<Question>> getQuestionsByAccountIdWithPaging(
@RequestParam int page,
@RequestHeader(name = "Authorization") String accessToken) throws Exception {

String accountId = jwtUtil.getSubjectFromToken(accessToken);
String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(accessToken);
List<Question> questions = questionService.findQuestionsByAccountIdWithPaging(page, accountId);
return ResponseEntity.ok(questions);
}
Expand All @@ -71,7 +70,7 @@ public ResponseEntity<Question> updateQuestion(
@RequestBody QuestionUpdateRequestDTO questionUpdateRequestDTO,
@RequestHeader(name = "Authorization") String accessToken) throws Exception {

String accountId = jwtUtil.getSubjectFromToken(accessToken);
String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(accessToken);
try {
Question updatedQuestion = questionService.updateQuestion(questionUpdateRequestDTO, accountId);
return ResponseEntity.ok(updatedQuestion);
Expand All @@ -81,9 +80,10 @@ public ResponseEntity<Question> updateQuestion(
}

// Delete a question by ID
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteQuestion(@PathVariable long id) {
questionService.deleteQuestion(id);
@DeleteMapping("/{questionId}")
public ResponseEntity<Void> deleteQuestion(@PathVariable Long questionId) {

questionService.deleteQuestion(questionId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.mtvs.devlinkbackend.question.entity;

import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.mtvs.devlinkbackend.reply.entity.Reply;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Table(name = "QUESTION")
@Entity(name = "QUESTION")
Expand All @@ -16,7 +21,7 @@ public class Question {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "QUESTION_ID")
private long questionId;
private Long questionId;

@Column(name = "TITLE")
private String title;
Expand All @@ -35,7 +40,12 @@ public class Question {
@Column(name = "ACCOUNT_ID") // 사용자 구분
private String accountId;

@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonManagedReference
private List<Reply> replies = new ArrayList<>();

public Question(String title, String content, String accountId) { // Create용 생성자
this.title = title;
this.content = content;
this.accountId = accountId;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.mtvs.devlinkbackend.reply.controller;

import com.mtvs.devlinkbackend.config.JwtUtil;
import com.mtvs.devlinkbackend.reply.dto.ReplyRegistRequestDTO;
import com.mtvs.devlinkbackend.reply.dto.ReplyUpdateRequestDTO;
import com.mtvs.devlinkbackend.reply.entity.Reply;
import com.mtvs.devlinkbackend.reply.service.ReplyService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/reply")
public class ReplyController {

private final ReplyService replyService;
private final JwtUtil jwtUtil;

public ReplyController(ReplyService replyService, JwtUtil jwtUtil) {
this.replyService = replyService;
this.jwtUtil = jwtUtil;
}

@PostMapping
public ResponseEntity<Reply> registReply(
@RequestBody ReplyRegistRequestDTO replyRegistRequestDTO,
@RequestHeader(name = "Authorization") String token) throws Exception {

String accountId = jwtUtil.getSubjectFromTokenWithAuth(token);
Reply reply = replyService.registReply(replyRegistRequestDTO, accountId);
return ResponseEntity.ok(reply);
}

@GetMapping("/{replyId}")
public ResponseEntity<Reply> findReplyByReplyId(@PathVariable Long replyId) {
Reply reply = replyService.findReplyByReplyId(replyId);
return reply != null ? ResponseEntity.ok(reply) : ResponseEntity.notFound().build();
}

@GetMapping("/question/{questionId}")
public ResponseEntity<List<Reply>> findRepliesByQuestionId(@PathVariable Long questionId) {
List<Reply> replies = replyService.findRepliesByQuestionId(questionId);
return ResponseEntity.ok(replies);
}

@GetMapping("/account/{accountId}")
public ResponseEntity<List<Reply>> findRepliesByAccountId(
@RequestHeader(name = "Authorization") String token) throws Exception {

String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(token);
List<Reply> replies = replyService.findRepliesByAccountId(accountId);
return ResponseEntity.ok(replies);
}

@PutMapping
public ResponseEntity<Reply> updateReply(
@RequestBody ReplyUpdateRequestDTO replyUpdateRequestDTO,
@RequestHeader(name = "Authorization") String token) throws Exception {

String accountId = jwtUtil.getSubjectFromTokenWithoutAuth(token);
try {
Reply updatedReply = replyService.updateReply(replyUpdateRequestDTO, accountId);
return ResponseEntity.ok(updatedReply);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(null);
}
}

@DeleteMapping("/{replyId}")
public ResponseEntity<Void> deleteReply(
@PathVariable Long replyId) {

replyService.deleteReply(replyId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.mtvs.devlinkbackend.reply.dto;

import lombok.*;

@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class ReplyRegistRequestDTO {
private String content;
private Long questionId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mtvs.devlinkbackend.reply.dto;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class ReplyUpdateRequestDTO {
private Long replyId;
private String content;
}
Loading