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

main 병합 #190

Merged
merged 12 commits into from
Aug 7, 2024
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
36 changes: 36 additions & 0 deletions .github/workflows/branch-protection-rules.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Branch Protection Rules

on:
pull_request:
types: [ opened, synchronize, reopened ]

jobs:
check-branch-rules:
runs-on: ubuntu-latest
steps:
- name: "develop 브랜치 머지 룰 확인"
if: github.event.pull_request.base.ref == 'develop'
run: |
HEAD_BRANCH="${{ github.event.pull_request.head.ref }}"
if [[ "$HEAD_BRANCH" != issue/* && "$HEAD_BRANCH" != bugfix/* && "$HEAD_BRANCH" != hotfix/* ]]; then
echo "'issue/', 'bugfix/' 또는 'hotfix/'로 시작하는 브랜치만 'develop' 브랜치로 머지할 수 있습니다."
exit 1
fi
- name: "release 브랜치 머지 룰 확인"
if: github.event.pull_request.base.ref == 'release'
run: |
HEAD_BRANCH="${{ github.event.pull_request.head.ref }}"
if [[ "$HEAD_BRANCH" != 'develop' && "$HEAD_BRANCH" != bugfix/* ]]; then
echo "'develop' 또는 'bugfix/'로 시작하는 브랜치만 'release' 브랜치로 머지할 수 있습니다."
exit 1
fi
- name: "main 브랜치 머지 룰 확인"
if: github.event.pull_request.base.ref == 'main'
run: |
HEAD_BRANCH="${{ github.event.pull_request.head.ref }}"
if [[ "$HEAD_BRANCH" != 'release' && "$HEAD_BRANCH" != hotfix/* ]]; then
echo "'release' 또는 'hotfix/'로 시작하는 브랜치만 'main' 브랜치로 머지할 수 있습니다."
exit 1
fi
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;
import org.springframework.security.web.firewall.RequestRejectedHandler;
import vook.server.api.web.common.auth.app.TokenService;
import vook.server.api.web.common.auth.jwt.JWTFilter;
import vook.server.api.web.common.auth.oauth2.LoginPolicyChecker;
import vook.server.api.web.common.auth.oauth2.LoginSuccessHandler;
import vook.server.api.web.common.auth.oauth2.VookOAuth2UserService;

Expand Down Expand Up @@ -94,4 +97,16 @@ private static OAuth2AuthorizationRequestResolver authorizationRequestResolver(C
}));
return authorizationRequestResolver;
}

@Bean
public RequestRejectedHandler requestRejectedHandler() {
HttpStatusRequestRejectedHandler defaultHandler = new HttpStatusRequestRejectedHandler();
return (request, response, requestRejectedException) -> {
if (requestRejectedException instanceof LoginPolicyChecker.VookRequestRejectedException) {
response.sendRedirect(loginFailUrl);
} else {
defaultHandler.handle(request, response, requestRejectedException);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class User {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true)
private String uid;

@Column(unique = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class Term extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true)
private String uid;

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

@Getter
@Entity
@Table(name = "vocabulary")
@Table(name = "vocabulary", indexes = {
@Index(name = "idx_vocabulary_user_uid", columnList = "user_uid")
})
@Builder(access = AccessLevel.PACKAGE)
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PACKAGE)
Expand All @@ -21,6 +23,7 @@ public class Vocabulary extends BaseEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true)
private String uid;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package vook.server.api.globalcommon.helper.format;

import org.slf4j.helpers.MessageFormatter;

public class FormatHelper {
/**
* Slf4J 스타일로 문자열 포멧팅을 해주는 함수
*/
public static String slf4j(String pattern, Object... params) {
return MessageFormatter.arrayFormat(pattern, params).getMessage();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package vook.server.api.globalcommon.helper.jwt;

import lombok.Getter;

@Getter
public class JWTException extends RuntimeException {

private final String useData;

public JWTException(Throwable cause) {
super(cause);
this.useData = "";
}

public JWTException(Throwable cause, String useData) {
super(cause);
this.useData = useData;
}

@Override
public String getMessage() {
if (useData.isEmpty()) {
return super.getMessage();
} else {
return super.getMessage() + "; useData(" + useData + ")";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ protected static <T> T run(CheckedSupplier<T> supplier) {
try {
return supplier.get();
} catch (Exception e) {
throw new RuntimeException(e);
throw new JWTException(e);
}
}

protected static <T> T run(CheckedSupplier<T> supplier, String useData) {
try {
return supplier.get();
} catch (Exception e) {
throw new JWTException(e, useData);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jwt.SignedJWT;
import vook.server.api.globalcommon.helper.format.FormatHelper;

import java.nio.charset.StandardCharsets;
import java.util.Date;
Expand All @@ -19,26 +20,29 @@ static JWTReader of(String secret, String token) {
reader.verifier = new MACVerifier(secretBytes);
reader.signedJWT = SignedJWT.parse(token);
return reader;
});
}, FormatHelper.slf4j("token: {}", token));
}

public void validate() {
run(() -> {
if (!signedJWT.verify(verifier)) {
throw new IllegalArgumentException("JWT의 서명이 올바르지 않습니다.");
throw new JWTException(new IllegalArgumentException("JWT의 서명이 올바르지 않습니다."));
}

Date expirationTime = signedJWT.getJWTClaimsSet().getExpirationTime();
if (expirationTime.before(new Date())) {
throw new IllegalArgumentException("JWT가 만료되었습니다.");
throw new JWTException(new IllegalArgumentException("JWT가 만료되었습니다."));
}

return null;
});
}, FormatHelper.slf4j("signedJWT: {}, verifier: {}", signedJWT.serialize(), verifier));
}

public String getClaim(String claimName) {
return run(() -> signedJWT.getJWTClaimsSet().getStringClaim(claimName));
return run(
() -> signedJWT.getJWTClaimsSet().getStringClaim(claimName),
FormatHelper.slf4j("claimName: {}, signedJWT: {}", claimName, signedJWT.serialize())
);
}

public static class Builder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.nimbusds.jwt.SignedJWT;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import vook.server.api.globalcommon.helper.format.FormatHelper;

import java.nio.charset.StandardCharsets;
import java.util.Date;
Expand Down Expand Up @@ -58,7 +59,7 @@ public String jwtString() {
signedJWT.sign(signer);

return signedJWT.serialize();
});
}, FormatHelper.slf4j("claims: {}", claims));
}

public static class Builder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import vook.server.api.globalcommon.helper.jwt.JWTException;
import vook.server.api.web.common.auth.app.TokenService;
import vook.server.api.web.common.auth.data.AuthValues;
import vook.server.api.web.common.auth.data.VookLoginUser;
Expand All @@ -34,8 +35,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
OAuth2User oAuth2User;
try {
oAuth2User = VookLoginUser.of(tokenService.validateAndGetUid(token));
} catch (Exception e) {
log.debug("JWT validation failed", e);
} catch (JWTException e) {
log.warn(e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package vook.server.api.web.common.auth.oauth2;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.stereotype.Component;

@Component
public class LoginPolicyChecker {

private final Boolean loginRestrictionEnable;
private final String[] allowedEmails;

public LoginPolicyChecker(
@Value("${service.loginPolicy.loginRestriction.enable:false}")
Boolean loginRestrictionEnable,
@Value("${service.loginPolicy.loginRestriction.allowedEmails:}")
String[] allowedEmails
) {
this.loginRestrictionEnable = loginRestrictionEnable;
this.allowedEmails = allowedEmails;
}

public void check(OAuth2Response response) {
if (!loginRestrictionEnable) {
return;
}

// allowedEmails가 비어 있으면 모든 이메일을 허용
if (allowedEmails.length != 0 && !isAllowedEmail(response)) {
throw new VookRequestRejectedException("Not allowed email: " + response.getEmail());
}
}

private boolean isAllowedEmail(OAuth2Response response) {
for (String allowedEmail : this.allowedEmails) {
if (response.getEmail().equals(allowedEmail)) {
return true;
}
}
return false;
}

public static class VookRequestRejectedException extends RequestRejectedException {
public VookRequestRejectedException(String message) {
super(message);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
public class VookOAuth2UserService extends DefaultOAuth2UserService {

private final UserLogic userLogic;
private final LoginPolicyChecker loginPolicyChecker;

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
Expand All @@ -33,6 +34,8 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
return null;
}

loginPolicyChecker.check(oAuth2Response);

return userLogic.findByProvider(oAuth2Response.getProvider(), oAuth2Response.getProviderId())
.map(VookLoginUser::from)
.orElseGet(() -> signUpUser(oAuth2Response));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package vook.server.api.web.common.response;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import vook.server.api.globalcommon.exception.AppException;
import vook.server.api.globalcommon.helper.jwt.JWTException;

@Slf4j
@RestControllerAdvice
Expand All @@ -20,6 +23,13 @@ public ResponseEntity<?> handleAppException(AppException e) {
return ResponseEntity.status(badRequest.statusCode()).body(badRequest.response());
}

@ExceptionHandler(JWTException.class)
public ResponseEntity<?> handleJWTException(JWTException e) {
log.debug(e.getMessage(), e);
CommonApiException serverError = CommonApiException.serverError(ApiResponseCode.ServerError.UNHANDLED_ERROR, e);
return ResponseEntity.status(serverError.statusCode()).body(serverError.response());
}

@ExceptionHandler(CommonApiException.class)
public ResponseEntity<?> handleCommonApiException(CommonApiException e) {
log.debug(e.getMessage(), e);
Expand All @@ -40,6 +50,12 @@ public ResponseEntity<?> handleHttpMessageConversionException(HttpMessageConvers
return ResponseEntity.status(badRequest.statusCode()).body(badRequest.response());
}

@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<?> handleNoResourceFoundException(NoResourceFoundException e) {
log.debug(e.getMessage(), e);
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}

@ExceptionHandler(Exception.class)
public ResponseEntity<?> handleException(Exception e) {
log.error(e.getMessage(), e);
Expand Down
4 changes: 4 additions & 0 deletions server/api/src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,9 @@ service:
loginFailUrl: http://localhost:3000/auth?error
accessTokenExpiredMinute: 30 # 30 minutes
refreshTokenExpiredMinute: 10080 # 60 * 24 * 7 == 1 week
loginPolicy:
loginRestriction:
enable: false
allowedEmails: ""
logging:
config: classpath:logback.xml