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

[BE] ♻️ Refactor : Security 리팩토링 #232

Merged
merged 4 commits into from
Sep 15, 2023
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 @@ -54,7 +54,7 @@ public static class PasswordPatch {
}

@Getter
@Builder
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public static class PasswordVerify {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,19 +161,19 @@ public void deleteAccount() {
accountRepository.delete(findAccount);
}

public Boolean verifyPassword(AccountDto.PasswordVerify passwordVerifyDto) {
Account findAccount = authUserUtils.getAuthUser();

return passwordEncoder.matches(passwordVerifyDto.getPassword(), findAccount.getPassword());
}

private void verifyExistsEmail(String email) {
Optional<Account> findAccount = accountRepository.findByEmail(email);

if(findAccount.isPresent())
throw new BusinessLogicException(ExceptionCode.ACCOUNT_ALREADY_EXISTS);
}

public Boolean verifyPassword(AccountDto.PasswordVerify passwordVerifyDto) {
Account findAccount = authUserUtils.getAuthUser();

return passwordEncoder.matches(passwordVerifyDto.getPassword(), findAccount.getPassword());
}

@Transactional(readOnly = true)
public Account findVerifiedAccount(Long accountId) {
return accountRepository.findById(accountId).orElseThrow(() ->
Expand All @@ -196,7 +196,7 @@ public void isAuthIdMatching(Long accountId) {
}

// 사용자가 일치하지 않으면 405 예외 던지기
if (Long.valueOf((Integer) claims.get("accountId")) != accountId)
if (Long.valueOf((String) claims.get("accountId")) != accountId)
throw new BusinessLogicException(ExceptionCode.ACCOUNT_NOT_ALLOW);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public class Comment extends BaseTimeEntity {
private List<CommentLike> commentLikes = new ArrayList<>();


@Builder
@Builder(toBuilder = true)
public Comment(Long commentId, String content, Account account, Board board) {
this.commentId = commentId;
this.content = content;
Expand Down
14 changes: 14 additions & 0 deletions server/src/main/java/com/growstory/domain/leaf/dto/LeafDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public static class Post {

@NotBlank
private String content;

@Builder
public Post(String leafName, String content) {
this.leafName = leafName;
this.content = content;
}
}

@Getter
Expand All @@ -32,6 +38,14 @@ public static class Patch {
private String content;

private Boolean isImageUpdated;

@Builder
public Patch(Long leafId, String leafName, String content, Boolean isImageUpdated) {
this.leafId = leafId;
this.leafName = leafName;
this.content = content;
this.isImageUpdated = isImageUpdated;
}
}

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,13 @@ public LeafDto.Response createLeaf(LeafDto.Post leafPostDto, MultipartFile leafI
.account(findAccount)
.build();

String leafImageUrl = leaf.getLeafImageUrl();

if (Optional.ofNullable(leafImage).isPresent())
leafImageUrl = s3Uploader.uploadImageToS3(leafImage, LEAF_IMAGE_PROCESS_TYPE);

Leaf savedLeaf = leafRepository.save(leaf.toBuilder()
.leafImageUrl(leafImageUrl)
.leafImageUrl(s3Uploader.uploadImageToS3(leafImage, LEAF_IMAGE_PROCESS_TYPE))
.build());

findAccount.addLeaf(savedLeaf);
updateAccountGrade(findAccount);
findAccount.updateGrade(updateAccountGrade(findAccount));

return LeafDto.Response.builder()
.leafId(savedLeaf.getLeafId())
Expand All @@ -63,12 +59,11 @@ public void updateLeaf(LeafDto.Patch leafPatchDto, MultipartFile leafImage) {
Leaf findLeaf = findVerifiedLeafByAccount(findAccount.getAccountId(), leafPatchDto.getLeafId());
String leafImageUrl = findLeaf.getLeafImageUrl();

if (leafPatchDto.getIsImageUpdated()) {
Optional.ofNullable(leafImageUrl).ifPresent(imageUrl ->
s3Uploader.deleteImageFromS3(imageUrl, LEAF_IMAGE_PROCESS_TYPE));
Optional.ofNullable(leafImageUrl).ifPresent(imageUrl ->
s3Uploader.deleteImageFromS3(imageUrl, LEAF_IMAGE_PROCESS_TYPE));

if (Optional.ofNullable(leafImage).isPresent())
leafImageUrl = s3Uploader.uploadImageToS3(leafImage, LEAF_IMAGE_PROCESS_TYPE);
}

leafRepository.save(findLeaf.toBuilder()
.leafName(Optional.ofNullable(leafPatchDto.getLeafName()).orElse(findLeaf.getLeafName()))
Expand Down Expand Up @@ -136,14 +131,14 @@ private Leaf findVerifiedLeaf(Long leafId) {
return findLeaf;
}

private static void updateAccountGrade(Account findAccount) {
public Account.AccountGrade updateAccountGrade(Account findAccount) {
int leavesNum = findAccount.getLeaves().size();
if (leavesNum < 50) {
findAccount.updateGrade(Account.AccountGrade.GRADE_BRONZE);
return Account.AccountGrade.GRADE_BRONZE;
} else if (leavesNum < 100) {
findAccount.updateGrade(Account.AccountGrade.GRADE_SILVER);
return Account.AccountGrade.GRADE_SILVER;
} else {
findAccount.updateGrade(Account.AccountGrade.GRADE_GOLD);
return Account.AccountGrade.GRADE_GOLD;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import javax.persistence.*;

@Builder
@Builder(toBuilder = true)
@Getter
@AllArgsConstructor
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public CorsFilter corsFilter() {

config.addAllowedOriginPattern("http://localhost:80"); // 로컬 아파치 환경에서 접근하는 CORS 허용
config.addAllowedOriginPattern("http://localhost:3000"); // 로컬 프론트 환경에서 접근하는 CORS 허용
config.addAllowedOriginPattern("http://growstory.s3-website.ap-northeast-2.amazonaws.com"); // 배포 환경
config.addAllowedOriginPattern("https://grow-story.vercel.app"); // 배포 환경

// //응답 헤더에 Authorization 헤더를 노출하도록 설정
config.addExposedHeader("Authorization");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR

private String delegateAccessToken(Account account) {
Map<String, Object> claims = new HashMap<>();
claims.put("accountId", account.getAccountId());
claims.put("accountId", account.getAccountId().toString());
claims.put("username", account.getEmail());
claims.put("profileImageUrl", account.getProfileImageUrl());
claims.put("roles", account.getRoles());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,94 @@

import com.growstory.global.auth.jwt.JwtTokenizer;
import com.growstory.global.auth.utils.CustomAuthorityUtils;
import com.growstory.global.exception.BusinessLogicException;
import com.growstory.global.exception.ExceptionCode;
import com.growstory.global.response.ErrorResponder;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.SignatureException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.json.BasicJsonParser;
import org.springframework.boot.json.JsonParser;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
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;

@Component
@RequiredArgsConstructor
public class JwtVerificationFilter extends OncePerRequestFilter {
@Value("${jwt.access-token-expiration-minutes}")
private int accessTokenExpirationMinutes;

@Value("${jwt.refresh-token-expiration-minutes}")
private int refreshTokenExpirationMinutes;

private final JwtTokenizer jwtTokenizer;
private final CustomAuthorityUtils authorityUtils;

@Override // 다음 필터 사이에 동작할 로직으로 JWT 검증 및 인증컨텍스트 저장을 수행한다.
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 토큰 재발급의 2가지 방법
// 1. 토큰이 만료되었을 때 응답 헤더로 예외를 추가하고
// 프런트에서 다시 로그인 or 토큰 재발급 api 요청 후
// 새로 발급한 토큰으로 이전 실제 요청을 하느냐

// 2. 토큰이 만료되었을 때 토큰을 재발급한 후
// 응답 헤더로 다시 새 토큰을 리턴해주고
// 프런트에서 응답을 받았을 때 응답 헤더에 토큰이 있으면
// 다시 저장하는 로직으로 처리하느냐
JsonParser jsonParser = new BasicJsonParser();

String accessToken = request.getHeader("Authorization").replace("Bearer ", "");
String refreshToken = request.getHeader("Refresh");

String accessTokenPayload = new String(Decoders.BASE64URL.decode(accessToken.split("\\.")[1]));
Map<String, Object> accessTokenClaims = jsonParser.parseMap(new String(accessTokenPayload));
// accessTokenClaims.put("accountId", accessTokenClaims.get("accountId").toString());

String refreshTokenPayload = new String(Decoders.BASE64URL.decode(refreshToken.split("\\.")[1]));
Map<String, Object> refreshTokenClaims = jsonParser.parseMap(new String(refreshTokenPayload));

// 이걸 저장할 때 설정
Date accessTokenExpiration = new Date((Long) accessTokenClaims.get("exp") * 1000L);
Date refreshTokenExpiration = new Date((Long) refreshTokenClaims.get("exp") * 1000L);
System.out.println("before:" + accessTokenExpiration);
System.out.println("before:" + refreshTokenExpiration);
Date now = new Date();

// accessToken 만료시간이 지금보다 이전이면(accessToken 만료 O), refreshToken 만료시간이 지금보다 이후라면(refreshToken 만료 X)
if (accessTokenExpiration.before(now) && refreshTokenExpiration.after(now)) {
accessToken = recreateAccessToken(accessTokenClaims, refreshTokenExpiration);
}

// accessToken 만료시간이 지금보다 이전이면(accessToken 만료 O), refreshToken 만료시간이 지금보다 이전이면(refreshToken 만료 O)
if (accessTokenExpiration.before(now) && refreshTokenExpiration.before(now)) {
accessToken = recreateAccessToken(accessTokenClaims, jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes()));
refreshToken = recreateRefreshToken(refreshTokenClaims);
}

try {
Map<String, Object> claims = verifyJws(request); // JWT 검증
setAuthenticationToContext(claims);
//jwt 검증에 실패할 경우 발생하는 예외를 HttpServletRequest의 속성(Attribute)으로 추가
Map<String,Object> recreatedAccessTokenClaims = verifyJws(accessToken); // JWT 검증
verifyJws(refreshToken);
setAuthenticationToContext(recreatedAccessTokenClaims);

//jwt 검증에 실패할 경우 발생하는 예외를 HttpServletRequest의 속성(Attribute)으로 추가
} catch (SignatureException se) {
request.setAttribute("exception", se);
} catch (ExpiredJwtException ee) {
Expand All @@ -39,6 +98,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
request.setAttribute("exception", e);
}

response.setHeader("Authorization", "Bearer " + accessToken);
response.setHeader("Refresh", refreshToken);
filterChain.doFilter(request, response);
}

Expand All @@ -51,13 +112,13 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce
}

// JWT 검증
private Map<String, Object> verifyJws(HttpServletRequest request) {
private Map<String, Object> verifyJws(String token) {
//request의 header에서 JWT 얻기
String jws = request.getHeader("Authorization").replace("Bearer ", "");
// String jws = request.getHeader("Authorization").replace("Bearer ", "");
//서버에 저장된 비밀키 호출
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
//Claims (JWT의 Payload, 사용자 정보인 username, roles 얻기) < - 내부적으로 서명(Signature) 검증에 성공한 상태
Map<String, Object> claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody();
Map<String, Object> claims = jwtTokenizer.getClaims(token, base64EncodedSecretKey).getBody();

return claims;
}
Expand All @@ -70,4 +131,29 @@ private void setAuthenticationToContext(Map<String, Object> claims) {
Authentication authentication = new UsernamePasswordAuthenticationToken(claims, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

private String recreateAccessToken(Map<String, Object> accessTokenClaims, Date refreshTokenExpiration) {
accessTokenClaims.remove("iat");
accessTokenClaims.remove("exp");

String subject = (String) accessTokenClaims.get("username");
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

// 만약 갱신된 accessToken 만료시간이 refreshToken보다 이후라면
if (expiration.after(refreshTokenExpiration)) expiration = refreshTokenExpiration;

return jwtTokenizer.generateAccessToken(accessTokenClaims, subject, expiration, base64EncodedSecretKey);
}

private String recreateRefreshToken(Map<String, Object> refreshTokenClaims) {
refreshTokenClaims.remove("iat");
refreshTokenClaims.remove("exp");

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

return jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,14 @@ private void redirect(HttpServletRequest request, HttpServletResponse response,
// response.setStatus(HttpStatus.TEMPORARY_REDIRECT.value());
// response.setHeader("Location", "http://localhost:8888/v1/accounts/oauth/login");

// client growstory.com => server api.growstory.com => cookie.setDomain(".growstory.com")
// response = addCookies(response, account, accessToken, refreshToken);
// HttpSession httpSession = request.getSession(true);
// httpSession.setAttribute("accessToken", accessToken);
// httpSession.setAttribute("accountId", account.getAccountId());

// 만약 보안성을 추가하려면 토큰과 그 토큰을 가리키는 uuid를 하나 생성해서 account 테이블에 저장한 후
// 토큰의 key인 uuid만 queryparm으로 리다이렉트 그 후 client측에서 uuid를 입력으로 유저정보 get 요청
// 토큰의 key인 uuid만 queryparam으로 리다이렉트 그 후 client측에서 uuid를 입력으로 유저정보 get 요청

//SimpleUrlAuthenticationSuccessHandler에서 제공하는 sendRedirect() 메서드를 이용해 Frontend 애플리케이션 쪽으로 리다이렉트
getRedirectStrategy().sendRedirect(request, response, uri);
Expand Down Expand Up @@ -131,9 +132,9 @@ private String delegateRefreshToken(String username) {
private Object createURI(String accessToken, String refreshToken, Account account) {
return UriComponentsBuilder
.newInstance()
.scheme("http")
.host("localhost")
.port(3000)
.scheme("https")
.host("grow-story.vercel.app")
// .port(3000)
// .host("growstory.s3-website.ap-northeast-2.amazonaws.com")
// .port(80) //S3는 80포트
.path("/signin")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ public Jws<Claims> getClaims(String jws, String base64EncodedSecretKey) {
.setSigningKey(key) // 서명 검증을 위한 key 지정
.build()
.parseClaimsJws(jws); // 토큰의 유효성 검사

System.out.println("after:" + claims.getBody().getExpiration());

return claims;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public Account getAuthUser() {
// 인증된 사용자를 나타내는 인증 객체 반환
Map<String, Object> principal = (Map<String, Object>) authentication.getPrincipal();

return accountRepository.findById(Long.valueOf((Integer) principal.get("accountId"))).orElseThrow(() ->
return accountRepository.findById(Long.parseLong((String) principal.get("accountId"))).orElseThrow(() ->
new BusinessLogicException(ExceptionCode.ACCOUNT_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
import java.util.ArrayList;
import java.util.List;

import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willDoNothing;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
Expand Down
Loading