Skip to content

Commit

Permalink
Feature: security 관련 Bean 추가 구현
Browse files Browse the repository at this point in the history
Feature: security 관련 Bean 추가 구현
  • Loading branch information
lcomment authored May 8, 2024
2 parents 6a5678d + e7e12db commit fa69753
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 1 deletion.
11 changes: 11 additions & 0 deletions cakk-api/src/main/java/com/cakk/api/annotation/SignInUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.cakk.api.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignInUser {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@ComponentScan(basePackages = {
"com.cakk.client",
"com.cakk.domain",
"com.cakk.external",
"com.cakk.api"
})
public class ComponentScanConfig {
Expand Down
12 changes: 12 additions & 0 deletions cakk-api/src/main/java/com/cakk/api/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import lombok.RequiredArgsConstructor;

import com.cakk.api.filter.JwtAuthenticationFilter;
import com.cakk.api.filter.JwtExceptionFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtExceptionFilter jwtExceptionFilter;

@Bean
public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
return http
Expand All @@ -25,6 +35,8 @@ public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws E
.headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.sessionManagement(setSessionManagement())
.authorizeHttpRequests(setAuthorizePath())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class)
.build();
}

Expand Down
18 changes: 18 additions & 0 deletions cakk-api/src/main/java/com/cakk/api/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.cakk.api.config;

import java.util.List;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.cakk.api.resolver.AuthorizedUserResolver;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthorizedUserResolver());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.cakk.api.factory;

import java.util.EnumMap;
import java.util.Map;

import org.springframework.stereotype.Component;

import com.cakk.api.provider.oauth.OidcProvider;
import com.cakk.api.provider.oauth.impl.AppleAuthProvider;
import com.cakk.api.provider.oauth.impl.GoogleAuthProvider;
import com.cakk.api.provider.oauth.impl.KakaoAuthProvider;
import com.cakk.common.enums.Provider;
import com.cakk.common.enums.ReturnCode;
import com.cakk.common.exception.CakkException;

@Component
public class OidcProviderFactory {

private final Map<Provider, OidcProvider> authProviderMap;
private final AppleAuthProvider appleAuthProvider;
private final KakaoAuthProvider kakaoAuthProvider;
private final GoogleAuthProvider googleAuthProvider;

public OidcProviderFactory(
AppleAuthProvider appleAuthProvider,
KakaoAuthProvider kakaoAuthProvider,
GoogleAuthProvider googleAuthProvider
) {
authProviderMap = new EnumMap<>(Provider.class);

this.appleAuthProvider = appleAuthProvider;
this.kakaoAuthProvider = kakaoAuthProvider;
this.googleAuthProvider = googleAuthProvider;

initialize();
}

private void initialize() {
authProviderMap.put(Provider.APPLE, appleAuthProvider);
authProviderMap.put(Provider.KAKAO, kakaoAuthProvider);
authProviderMap.put(Provider.GOOGLE, googleAuthProvider);
}

public String getProviderId(Provider provider, String idToken) {
return getProvider(provider).getProviderId(idToken);
}

private OidcProvider getProvider(Provider provider) {
OidcProvider oidcProvider = authProviderMap.get(provider);

if (oidcProvider == null) {
throw new CakkException(ReturnCode.WRONG_PROVIDER);
}

return oidcProvider;
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.cakk.api.filter;

import java.io.IOException;
import java.util.Optional;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.cakk.api.provider.jwt.JwtProvider;
import com.cakk.common.enums.ReturnCode;
import com.cakk.common.exception.CakkException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtProvider jwtProvider;
private final String accessHeader;
private final String grantType;

public JwtAuthenticationFilter(
JwtProvider jwtProvider,

@Value("${jwt.access-header}") String accessHeader,

@Value("${jwt.grant-type}") String grantType
) {
this.jwtProvider = jwtProvider;
this.accessHeader = accessHeader;
this.grantType = grantType;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws
ServletException,
IOException {
Optional<String> token = getTokensFromHeader(request, accessHeader);

token.ifPresent(it -> {
String accessToken = replaceBearerToBlank(it);

Authentication authentication = jwtProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
});

filterChain.doFilter(request, response);
}

private Optional<String> getTokensFromHeader(HttpServletRequest request, String header) {
return Optional.ofNullable(request.getHeader(header));
}

private String replaceBearerToBlank(String token) {
String suffix = grantType + " ";

if (!token.startsWith(suffix)) {
throw new CakkException(ReturnCode.NOT_EXIST_BEARER_SUFFIX);
}

return token.replace(suffix, "");
}
}
50 changes: 50 additions & 0 deletions cakk-api/src/main/java/com/cakk/api/filter/JwtExceptionFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.cakk.api.filter;

import java.io.IOException;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import com.cakk.common.enums.ReturnCode;
import com.cakk.common.exception.CakkException;
import com.cakk.common.response.ApiResponse;

@Component
public class JwtExceptionFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws
ServletException,
IOException {
try {
filterChain.doFilter(request, response);
} catch (CakkException exception) {
setErrorResponse(exception.getReturnCode(), response);
}
}

private void setErrorResponse(ReturnCode returnCode, HttpServletResponse response) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json; charset=UTF-8");
ApiResponse<String> result = ApiResponse.fail(returnCode);

try {
response.getWriter().write(toJson(result));
} catch (IOException e) {
// ignored
}
}

private String toJson(Object data) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.cakk.api.resolver;

import static java.util.Objects.*;

import org.springframework.core.MethodParameter;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import com.cakk.api.annotation.SignInUser;
import com.cakk.api.vo.OAuthUserDetails;
import com.cakk.domain.entity.user.User;

public class AuthorizedUserResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(SignInUser.class)
&& User.class.isAssignableFrom(parameter.getParameterType());
}

@Override
public User resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
OAuthUserDetails userDetails = (OAuthUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return isNull(userDetails) ? null : userDetails.getUser();
}
}
3 changes: 3 additions & 0 deletions cakk-api/src/main/java/com/cakk/api/vo/OAuthUserDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;

import lombok.Getter;

import com.cakk.domain.entity.user.User;

@Getter
public class OAuthUserDetails implements UserDetails, OidcUser, OAuth2User {

private final User user;
Expand Down
5 changes: 4 additions & 1 deletion cakk-api/src/test/java/com/cakk/api/ArchitectureTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -34,13 +35,15 @@ void response() {
rule.check(javaClasses);
}

@DisplayName("Provider 클래스는 Service, Provider 클래스에만 의존해야 한다.")
@Disabled
@DisplayName("Provider 클래스는 Service, Filter, Provider 클래스에만 의존해야 한다.")
@Test
void providerDependency() {
ArchRule rule = classes()
.that().haveNameMatching(".*Provider")
.and().areNotEnums()
.should().onlyHaveDependentClassesThat().haveSimpleNameEndingWith("Service")
.orShould().onlyHaveDependentClassesThat().haveSimpleNameEndingWith("Filter")
.orShould().onlyHaveDependentClassesThat().haveSimpleNameEndingWith("Provider");

rule.check(javaClasses);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public enum ReturnCode {
EMPTY_AUTH_JWT("1103", "인증 정보가 비어있는 jwt 토큰입니다."),
EMPTY_USER("1104", "비어있는 유저 정보로 jwt 토큰을 생성할 수 없습니다."),

// 공통 유저 관련 (1200 ~ 1250)
WRONG_PROVIDER("1200", "잘못된 인증 제공자 입니다."),

// 클라이언트 에러
WRONG_PARAMETER("9000", "잘못된 파라미터 입니다."),
METHOD_NOT_ALLOWED("9001", "허용되지 않은 메소드 입니다."),
Expand Down

0 comments on commit fa69753

Please sign in to comment.