Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: 리플레쉬 토큰을 이용한 로그인 유지 기능 #174

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
Open
1 change: 0 additions & 1 deletion .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ protected void successfulAuthentication(HttpServletRequest request,
private String delegateAccessToken(Member member) {
Map<String, Object> claims = new HashMap<>();
claims.put("memberId", member.getMemberId()); //Context에 member객체를 올려두기 위해 추가
claims.put("email", member.getEmail());
claims.put("roles", member.getRoles());

String subject = member.getEmail();
Expand All @@ -75,11 +74,15 @@ private String delegateAccessToken(Member member) {
}

private String delegateRefreshToken(Member member) {
Map<String, Object> claims = new HashMap<>();
claims.put("memberId", member.getMemberId());
claims.put("roles", member.getRoles());

String subject = member.getEmail();
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);
String refreshToken = jwtTokenizer.generateRefreshToken(claims, subject, expiration, base64EncodedSecretKey);

return refreshToken;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.stockholm.main_project.member.entity.Member;
import com.stockholm.main_project.member.service.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
Expand All @@ -16,10 +17,13 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
@Configuration
public class JwtVerificationFilter extends OncePerRequestFilter {
private final JwtTokenizer jwtTokenizer;
private final CustomAuthorityUtils authorityUtils;
Expand All @@ -36,6 +40,7 @@ public JwtVerificationFilter(JwtTokenizer jwtTokenizer,
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Map<String, Object> claims = verifyJws(request);

setAuthenticationToContext(claims);
filterChain.doFilter(request, response);
}
Expand Down Expand Up @@ -63,4 +68,20 @@ private void setAuthenticationToContext(Map<String, Object> claims) {
Authentication authentication = new UsernamePasswordAuthenticationToken(member, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

public String generateAccessTokenFromRefreshToken(Map<String, Object> claims) {
String subject = (String) claims.get("email");

Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

Map<String, Object> accessTokenClaims = new HashMap<>();
accessTokenClaims.put("email", subject);
accessTokenClaims.put("memberId", claims.get("memberId"));
accessTokenClaims.put("roles", claims.get("roles"));

String newAccessToken = jwtTokenizer.generateAccessToken(accessTokenClaims, subject, expiration, base64EncodedSecretKey);

return newAccessToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo

// 액세스 토큰 및 리프레시 토큰 생성
String accessToken = delegateAccessToken(email, authorities);
String refreshToken = delegateRefreshToken(email);
String refreshToken = delegateRefreshToken(email, authorities);

// 헤더에 토큰 추가
response.addHeader("Authorization", "Bearer " + accessToken);
Expand All @@ -60,7 +60,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo

private void redirect(HttpServletRequest request, HttpServletResponse response, String username, List<String> authorities) throws IOException {
String accessToken = delegateAccessToken(username, authorities);
String refreshToken = delegateRefreshToken(username);
String refreshToken = delegateRefreshToken(username, authorities);

String uri = createURI(accessToken, refreshToken).toString();
getRedirectStrategy().sendRedirect(request, response, uri);
Expand All @@ -85,14 +85,19 @@ private String delegateAccessToken(String username, List<String> authorities) {
return accessToken;
}

private String delegateRefreshToken(String username) {
private String delegateRefreshToken(String username, List<String> authorities) {
int memberId = memberService.findMemberIdByEmail(username);

Map<String, Object> claims = new HashMap<>();
claims.put("email", username);
claims.put("roles", authorities);
claims.put("memberId", memberId);

String subject = username;
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);
String refreshToken = jwtTokenizer.generateRefreshToken(claims, subject, expiration, base64EncodedSecretKey);

return refreshToken;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ public String generateAccessToken(Map<String, Object> claims,
.compact();
}

public String generateRefreshToken(String subject, Date expiration, String base64EncodedSecretKey){
public String generateRefreshToken(Map<String, Object> claims, String subject, Date expiration, String base64EncodedSecretKey){
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);

return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(Calendar.getInstance().getTime())
.setExpiration(expiration)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.stockholm.main_project.auth.jwt.refreshToken;

import com.stockholm.main_project.auth.filter.JwtVerificationFilter;
import com.stockholm.main_project.auth.jwt.JwtTokenizer;
import com.stockholm.main_project.exception.BusinessLogicException;
import com.stockholm.main_project.exception.ExceptionCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class RefreshTokenService {

@Autowired
private JwtTokenizer jwtTokenizer;

private final JwtVerificationFilter jwtVerificationFilter;

public RefreshTokenService(JwtVerificationFilter jwtVerificationFilter) {
this.jwtVerificationFilter = jwtVerificationFilter;
}

public String requestNewAccessToken(String refreshToken) throws Exception {
// 리프레시 토큰을 검증하고, 필요한 클레임을 추출합니다.
Map<String, Object> refreshTokenClaims = validateRefreshToken(refreshToken);

// 새로운 액세스 토큰을 생성합니다.
String newAccessToken = jwtVerificationFilter.generateAccessTokenFromRefreshToken(refreshTokenClaims);

return newAccessToken;
}

private Map<String, Object> validateRefreshToken(String refreshToken) throws Exception {
try {
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

// 리프레시 토큰을 검증하고 클레임을 추출합니다.
Map<String, Object> refreshTokenClaims = jwtTokenizer.getClaims(refreshToken, base64EncodedSecretKey).getBody();

// 여기에서 리프레시 토큰의 유효성을 추가적으로 검증하거나 필요한 클레임을 확인할 수 있습니다.

return refreshTokenClaims;
} catch (Exception e) {
throw new BusinessLogicException(ExceptionCode.INVALID_REFRESH_TOKEN);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.stockholm.main_project.auth.jwt.refreshToken;

import com.stockholm.main_project.member.dto.MemberResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/tokens")
public class TokenController {

@Autowired
private RefreshTokenService refreshTokenService;

@Operation(summary = "엑세스 토큰 재 발급", description = "엑세스 토큰을 재 발급합니다.", tags = { "Token" })
@ApiResponse(responseCode = "200", description = "OK",
content = @Content(schema = @Schema(implementation = TokenRefreshRequestDto.class)))
@ApiResponse(responseCode = "401", description = "INVALID REFRESH TOKEN")
@PostMapping("/refresh")
public ResponseEntity<String> refreshAccessToken(@RequestBody TokenRefreshRequestDto tokenRefreshRequestDto) throws Exception {

String newAccessToken = refreshTokenService.requestNewAccessToken(tokenRefreshRequestDto.getRefreshToken());
return ResponseEntity.ok(newAccessToken);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.stockholm.main_project.auth.jwt.refreshToken;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class TokenRefreshRequestDto {
@Schema(description = "refreshToken", defaultValue = "eyJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJVU0VSIl0sIm1lbWJlcklkIjoxMDMsInN1YiI6InRlc3QwMTUxQG5hdmVyLmNvbSIsImlhdCI6MTY5NjkzNTA1NSwiZXhwIjoxNjk2OTYwMjU1fQ.F9IfWW_EKd7cFGYtd95B3_CcfSr2w1HLmIlEMK0fyN")
private String refreshToken;

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@
import com.stockholm.main_project.board.entity.Board;

import com.stockholm.main_project.board.service.BoardService;
import com.stockholm.main_project.member.dto.MemberPostDto;
import com.stockholm.main_project.member.entity.Member;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
Expand All @@ -30,8 +37,11 @@ public CommentController(CommentService commentService, BoardService boardServic
this.mapper = mapper;
}

@Operation(summary = "댓글 생성", description = "새로운 댓글을 생성합니다.", tags = { "Comment" })
@ApiResponse(responseCode = "201", description = "CREATED",
content = @Content(schema = @Schema(implementation = CommentResponseDto.class)))
@PostMapping
public ResponseEntity postComment(@PathVariable long boardId, @RequestBody CommentRequestDto commentRequestDto, @AuthenticationPrincipal Member member){
public ResponseEntity postComment(@Schema(implementation = CommentRequestDto.class)@PathVariable long boardId, @RequestBody CommentRequestDto commentRequestDto,@Parameter(hidden = true) @AuthenticationPrincipal Member member){

Comment comment = mapper.commentRequestDtoToComment(commentRequestDto);
comment.setBoard(boardService.findBoard(boardId));
Expand All @@ -41,11 +51,15 @@ public ResponseEntity postComment(@PathVariable long boardId, @RequestBody Comme
CommentResponseDto responseDto = mapper.commentToCommentResponseDto(createdComment);
return new ResponseEntity<>(responseDto, HttpStatus.CREATED);
}
@Operation(summary = "댓글 수정", description = "작성한 댓글을 수정합니다.", tags = { "Comment" })
@ApiResponse(responseCode = "200", description = "OK",
content = @Content(schema = @Schema(implementation = CommentResponseDto.class)))
@ApiResponse(responseCode = "404", description = "INVALID FAILED")
@PatchMapping("{commentId}")
public ResponseEntity updateComment(@PathVariable long boardId,
@PathVariable long commentId,
@RequestBody CommentRequestDto commentRequestDto,
@AuthenticationPrincipal Member member){
@Parameter(hidden = true) @AuthenticationPrincipal Member member){

Comment comment = mapper.commentRequestDtoToComment(commentRequestDto);
comment.setCommentId(commentId);
Expand All @@ -56,8 +70,11 @@ public ResponseEntity updateComment(@PathVariable long boardId,
return new ResponseEntity<>(responseDto, HttpStatus.OK);
}

@Operation(summary = "댓글 삭제", description = "작성한 댓글를 삭제합니다.", tags = { "Comment" })
@ApiResponse(responseCode = "204", description = "NO CONTENT")
@ApiResponse(responseCode = "404", description = "INVALID FAILED")
@DeleteMapping("{commentId}")
public ResponseEntity deleteComment(@PathVariable long boardId, @PathVariable long commentId, @AuthenticationPrincipal Member member) {
public ResponseEntity deleteComment(@PathVariable long boardId, @PathVariable long commentId,@Parameter(hidden = true) @AuthenticationPrincipal Member member) {

commentService.deleteComment(commentId, member);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.stockholm.main_project.board.commnet.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CommentRequestDto {
@Schema(description = "댓글 내용", defaultValue = "TestComments")
private String content;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package com.stockholm.main_project.board.commnet.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CommentResponseDto {
@Schema(description = "댓글 commentId", defaultValue = "1")
private Long commentId;
@Schema(description = "댓글 작성자", defaultValue = "TestAccount")
private String member;
@Schema(description = "댓글 내용", defaultValue = "TestComments")
private String content;
@Schema(description = "댓글 생성 날짜", defaultValue = "2023-11-10T10:51:17.549Z")
private String createdAt;
@Schema(description = "댓글 수정 날짜", defaultValue = "2023-12-10T10:51:17.549Z")
private String ModifiedAt;
}
Loading