2020년 8월 17일 월요일

멀티모듈 실습해보기

 멀티모듈 실습해보기

  • 멀티모듈 프로젝트 ??

    • 기존의 단일 프로젝트들을 프로젝트 안의 모듈로서 갖을 수 있게 하여 다중 모듈을 한 프로젝트에서 관리한다.

    • 독립적인 프로젝트들이 모여 동작하는 한 시스템을 개발하는 경우 독립적인 시스템들이 동일하게 사용하는 부분을 동일하게 각각의 프로젝트에서 구현해야 한다. (ex_ Domain, Repository 등)

    • 이 경우 공통부분에 변경이 생기면 모든 독립적인 프로젝트에도 동일하게 변경을 해줘야한다.

      • 상당히 비효율적이다. 실수발생 가능성도 높다.
    • 멀티모듈을 통해서 루트프로젝트 내 여러곳에서 공통적으로 사용되는 부분을 모듈로 분리해서 관리할 수있다.

  • 문제점

      1. 그래들 6.4.1 오류 발생
      • 생성 시 6.4.1로 설정되었는데 빌드가 안된다! 검색을 해보면 인털리제이 이슈로 나온다

      • 버전을 낮췄다.. 6.4.1 -> 5.6.1

      • 프로젝트 경로에서 다음을 실행하면 된다.

      ./gradlew wrapper --gradle-version 5.6.1

      • 나는 gradle/wrapper/gradle-wrapper.properties 에서 버전을 바꿔주었다. 위의 방법이 맞는 방법 같다.

        distributionBase=GRADLE_USER_HOME
        distributionPath=wrapper/dists
        distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-bin.zip
        zipStoreBase=GRADLE_USER_HOME
        zipStorePath=wrapper/dists
        
      1. common에 생성한 repository나 domain을 자꾸 인식을 못하여 다음과 같이 Application에 명시해줌
      @SpringBootApplication(scanBasePackages = "com.example")
      @EntityScan("com.example.domain")
      @EnableJpaRepositories(basePackages={"com.example.repository"})
      
      • 해결! : 생성한 repository 경로가 다른 모듈과 달랐음!

        • 기존 common 모듈 : com.example.domain.Member.java
        • 기존 api 모듈 : com.example.multimodulestestproject.ModuleApiApplication.java
        • 변경 common모듈 : com.example.multimodulestestproject .domain.Member.java


  • 프로젝트 생성 시 입력하는 groupId와 artifactId

  • 의문점

    • MSA랑 멀티모듈이랑 다른걸까??

      • MSA 는 기능기반으로 분리하여 아예 의존성 없는 모듈간 통신으로 서비스를 하는 것 같고 멀티모듈은 그냥 방식인 것 같음.
      • 모놀리스방식도 멀티모듈이 가능한듯?
    • 애초에 모두 묶어서 프로젝트를 만들면?

      • 모두 합친 프로젝트에 자주 변경되는 부분이 있을텐데 그 부분 때문에 전체 재배포가 필요해진다.

      • 해당 부분을 분리하여 각각 만듦으로써 필요한 부분만 변경(배포)이 가능해진다.

  • 변경 전 build.gradle

plugins {
    id 'org.springframework.boot' version '2.3.2.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    compile 'org.springframework.boot:spring-boot-starter-web'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

test {
    useJUnitPlatform()
}
  • 변경 후 build.gradle

buildscript {
    ext {
        springBootVersion = '2.3.2.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath "io.spring.gradle:dependency-management-plugin:1.0.9.RELEASE"
    }
}



subprojects {
    group = 'com.example'
    version = '0.0.1-SNAPSHOT'

    apply plugin: 'java'
    apply plugin: 'org.springframework.boot'
    apply plugin: 'io.spring.dependency-management'

    sourceCompatibility = '1.8'

    repositories {
        mavenCentral()
    }


    dependencies {
        testCompile group: 'junit', name: 'junit', version: '4.12'
    }
}

project(':module-api') {
    dependencies {
        compile project(':module-common')
    }
}

project(':module-web') {
    dependencies {
        compile project(':module-common')
    }
}

2020년 8월 4일 화요일

간단하게 OAuth 2.0 사용해보기

간단하게 OAuth 2.0 사용해보기

  • OAuth란?

    • 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준

    • ID와 비밀번호 대신 엑세스 토큰기반으로 사용자를 식별할 수 있다. 토큰은 리소스 서버만 제공한다.

  • 기본 구조

    • Resource Owner : ID와 비밀번호를 이용해 Resource Client에게 권한을 인가하여 엑세스 토큰을 획득하게 될 주체

    • Resource Client : ResourceOwner로부터 사용 인가를 받아, 소유자 대신 엑세스 토큰을 획득하고 해당 토큰 을 통해 Resource Server의 API를 사용하는 주체

    • Resource Server : 보호된 리소스를 관리하며 리소스 클라이언트가 사용할 API를 제공하는 주체. 엑세스 토큰이 유효한지 확인하기 위해 Authorization Server와 통신을 주고 받기도함

    • Authorization Server : 엑세스 토큰과 인가 코드를 관리하는 서버. 엑세스 토큰 검증, 폐기

  • 기본 동작 구조

    • ex) 리소스 오너 : 사용자 / 리소스 클라이언트 : 쇼핑몰 웹(앱) / 리소스 서버, 인가 서버 : 카카오
    • 사용자 -> 쇼핑몰 실행 -> 로그인 -> 카카오 로그인 버튼 클릭
    • 웹 -> 카카오로 인증페이지 요청 -> 카카오 로그인화면 이동
    • 사용자는 카카오 ID,PW 입력(토큰 요청) -> 카카오는 인증페이지를 요청했던 쇼핑몰에 엑세스 토큰 발급
    • 쇼핑몰에선 해당 엑세스 토큰을 가지고 다시 카카오API 호출 -> 정상적인 토큰일 경우 사용자 정보 리턴
  • 내가 적용한 예시

    • 아래의 두 가지 방법 중 하나로 가능하다. 특히 Spring Security가 제공하는 OAuth2ClientAuthenticationProcessingFilter 를 사용하면 토큰받기, 사용자정보 받기 과정을 자동으로 할 수 있다.

    • REST API 직접 호출을 통한 구현

      • 카카오로부터 AccessToken 받기

        public JsonNode getKakaoAccessToken(String code) {
        
        
          final String RequestUrl = "https://kauth.kakao.com/oauth/token";
          final List<NameValuePair> postParams = new ArrayList<NameValuePair>();
        
        
          //포스트 파라미터의 grant_type이라는 명칭에 authorization_code를 추가한다 아래도 동일
          postParams.add(new BasicNameValuePair("grant_type", "authorization_code"));
          postParams.add(new BasicNameValuePair("client_id", kakaoClientId));
          postParams.add(new BasicNameValuePair("redirect_uri", redirectUri));
          postParams.add(new BasicNameValuePair("code", code));
        
        
          final HttpClient client = HttpClientBuilder.create().build();
          final HttpPost post = new HttpPost(RequestUrl);
        
        
          JsonNode returnNode = null;
        
        
          try {
              post.setEntity(new UrlEncodedFormEntity(postParams));
              final HttpResponse response = client.execute(post);
              ObjectMapper mapper = new ObjectMapper();
              returnNode = mapper.readTree(response.getEntity().getContent());
          } catch (UnsupportedEncodingException e) {
              e.printStackTrace();
          } catch (ClientProtocolException e) {
              e.printStackTrace();
          } catch (IOException e) {
              e.printStackTrace();
          }
        
        
          return returnNode;
        }
        
      • 받은 토큰을 통해 사용자 프로필 받기

        public JsonNode getKakaoUserProfile(String access_token) {
        
        
            final String RequestUrl = "https://kapi.kakao.com/v2/user/me";
        
        
            final HttpClient client = HttpClientBuilder.create().build();
            final HttpPost post = new HttpPost(RequestUrl);
        
        
            post.addHeader("Authorization", "Bearer " + access_token);
        
        
            JsonNode returnNode = null;
        
        
            try {
                final HttpResponse response = client.execute(post);
        
        
                ObjectMapper mapper = new ObjectMapper();
                returnNode = mapper.readTree(response.getEntity().getContent());
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        
        
            return returnNode;
        }
        
    • Security 필터 사용

      • SecurityConfig

        @Configuration
        @EnableWebSecurity
        public class SecurityConfig extends WebSecurityConfigurerAdapter {
        
        
            private final Filter ssoFilter;
        
        
            public SecurityConfig(Filter ssoFilter) {
                this.ssoFilter = ssoFilter;
            }
        
        
            @Override
            public void configure(WebSecurity web) throws Exception {
                web.ignoring().antMatchers("/resources/**","resources/**");
                web.httpFirewall(new DefaultHttpFirewall());
            }
        
        
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.csrf().disable();
                http.authorizeRequests()
                        .antMatchers("/", "/course/admin/**").hasAuthority("ROLE_ADMIN")
                        .antMatchers("/", "/admin/**").hasAuthority("ROLE_ADMIN")
                        .and()
                            .formLogin()
                            .loginPage("/")
                            .loginProcessingUrl("/loginProcess.ajax")
                            .usernameParameter("id")
                            .passwordParameter("password")
                            .successForwardUrl("/loginCheck.ajax")
                            .permitAll()
                        .and()
                            .logout()
                            .logoutUrl("/logout")
                            .logoutSuccessUrl("/")
                            .deleteCookies("JSESSIONID")
                            .invalidateHttpSession(true)
                        .and()
                            .addFilterBefore(ssoFilter, BasicAuthenticationFilter.class)
                        .httpBasic();
            }
        
        
            @Bean
            public PasswordEncoder passwordEncoder(){
                // 버전차이로 PasswordEncoderFactories.createDelegatingPasswordEncoder(); 는 사용못하는 것 같음
                return new BCryptPasswordEncoder();
            }
        
        
        }
        
        
        
      • OAuthConfig

        @Configuration
        @EnableOAuth2Client
        @PropertySource(value = "classpath:socialInfo.properties")
        public class OAuthConfig {
        
        
            @Autowired
            @Qualifier("oauth2ClientContext")
            OAuth2ClientContext oauth2ClientContext;
        
        
            @Bean
            public Filter ssoFilter() {
                CompositeFilter filter = new CompositeFilter();
                List<Filter> filterList = new ArrayList<>();
        
        
                filterList.add(kakaoSsoFilter());
                filterList.add(googleSsoFilter());
                filter.setFilters(filterList);
        
        
                return filter;
            }
        
        
            public Filter kakaoSsoFilter() {
                OAuth2ClientAuthenticationProcessingFilter oauth2Filter = new OAuth2ClientAuthenticationProcessingFilter("/login/kakao/oauth");
                OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(kakaoClient(), oauth2ClientContext);
                oauth2Filter.setRestTemplate(oAuth2RestTemplate);
                oauth2Filter.setTokenServices(new UserInfoTokenServices(kakaoResource().getUserInfoUri(), kakaoClient().getClientId()));
                oauth2Filter.setAuthenticationSuccessHandler(kakaoSuccessHandler());
        
        
                return oauth2Filter;
            }
        
        
            public Filter googleSsoFilter() {
                OAuth2ClientAuthenticationProcessingFilter oauth2Filter = new OAuth2ClientAuthenticationProcessingFilter("/google/googleSignInCallback");
                OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(googleClient(), oauth2ClientContext);
                oauth2Filter.setRestTemplate(oAuth2RestTemplate);
                oauth2Filter.setTokenServices(new UserInfoTokenServices(googleResource().getUserInfoUri(), googleClient().getClientId()));
                oauth2Filter.setAuthenticationSuccessHandler(successHandler());
        
        
                return oauth2Filter;
            }
        
        
            @Bean
            public AuthenticationSuccessHandler successHandler(){
                return (request, response, authentication) -> {
        
        
                    response.sendRedirect("/login/googleSignIn");
                };
            }
        
        
            @Bean
            public AuthenticationSuccessHandler kakaoSuccessHandler(){
                return (request, response, authentication) -> {
        
        
                    response.sendRedirect("/login/kakaoSignin");
                };
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "google.client")
            public OAuth2ProtectedResourceDetails googleClient() {
                return new AuthorizationCodeResourceDetails();
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "google.resource")
            public ResourceServerProperties googleResource() {
                return new ResourceServerProperties();
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "kakao.client")
            public OAuth2ProtectedResourceDetails kakaoClient() {
                return new AuthorizationCodeResourceDetails();
            }
        
        
            @Bean
            @ConfigurationProperties(prefix = "kakao.resource")
            public ResourceServerProperties kakaoResource() {
                return new ResourceServerProperties();
            }
        
        
            @Bean
            public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
                FilterRegistrationBean registration = new FilterRegistrationBean();
                registration.setFilter(filter);
                registration.setOrder(-100);
                return registration;
            }
        }
        

참고 및 출처

2020년 8월 3일 월요일

.gitignore 사용하기

.gitignore 사용하기

  • 민감한 정보나, 업데이트하고 싶지 않은 정보는 gitignore를 통해 관리할 수 있다.

    # class 확장자
    *.class
    
    # build 하위 파일
    build/
    
    # 해당 파일 제외
    !gradle/wrapper/gradle-wrapper.jar
    
  • 내가 해놓은 .gitignore

    target/
    !.mvn/wrapper/maven-wrapper.jar
    
    ### GRADLE ###
    .gradle
    /build/
    !gradle/wrapper/gradle-wrapper.jar
    /out
    
    ### STS ###
    .apt_generated
    .classpath
    .factorypath
    .project
    .settings
    .springBeans
    
    ### IntelliJ IDEA ###
    .idea
    *.iws
    *.iml
    *.ipr
    
    ### NetBeans ###
    nbproject/private/
    build/
    nbbuild/
    dist/
    nbdist/
    .nb-gradle/
    /bin/
    
    ### querydsl
    generated
    
    
    
  • 여기서 .gitignore 예시도 제공해준다.

  • 개발 중간에 .gitignore에 추가하였지만, commit 목록에는 표시될 때 !!

    • cached 옵션을 통해 로컬에는 파일을 남기고 원격저장소에 파일을 지울 수 있다. (이미 파일이 원격저장소에 있는 경우 ignore가 안먹히는 것 같다!!)
    • git rm --cached 제외할파일명