Page tree
Skip to end of metadata
Go to start of metadata

개발환경 설정

  • Intellij 환경을 기준으로 작성한다. (Gatling,  Cucumber, Protractor - 너무 많은걸 쓰는것 같다. ㅠ.ㅠ)

    아래 Gatling,  Cucumber, Protractor 에 대한 환경설정은 가급적 따로 설치 하지 말것을 추천 드립니다.

    개인적으로 해당 모듈들에 관심이 있다면 환경을 설정하고 plugin을 설치 하셔도 되지만 이번주제에서는 깊이 있게 다루지는 않고 대략적인 설명으로 진행할 예정입니다.(이후 4,5추차 주제에서 상세하게 다뤄질 것으로 예상됩니다.)

Gatling

  • plugins : IDEA에서 'STB' plugin을 검색한다. 
  • guide document : https://gist.github.com/groovybayo/4691670
  • IDEA설정 : https://github.com/orfjackal/idea-sbt-plugin/wiki
  • 개요 : 프로젝트 성능테스트 툴
    • 특징
      • 테스트 시나리오를 Scala DSL 로 작성
      • 부하 테스트 결과를 그래프를 포함한 깔끔한 보고서로 생성
      • 시나리오 자동 생성을 위한 레코드 도구 제공
    • 접속: admin으로 로그인 > Administration > Metrics
  • 성능테스트 툴 비교 : https://www.blazemeter.com/blog/open-source-load-testing-tools-which-one-should-you-use
  • 통계 페이지(admin으로 접속 후 metrics 메뉴를 클릭 하면 아래와 같은 화면이 보인다.)
  • 위와 같은 성능이나 속도를 보여주는 부분을 처리하는 것은 아래 소스코드에 들어있다.
  • MetricsConfiguration.java
    @PostConstruct
    public void init() {
        log.debug("Registering JVM gauges");
        metricRegistry.register(PROP_METRIC_REG_JVM_MEMORY, new MemoryUsageGaugeSet());
        metricRegistry.register(PROP_METRIC_REG_JVM_GARBAGE, new GarbageCollectorMetricSet());
        metricRegistry.register(PROP_METRIC_REG_JVM_THREADS, new ThreadStatesGaugeSet());
        metricRegistry.register(PROP_METRIC_REG_JVM_FILES, new FileDescriptorRatioGauge());
        metricRegistry.register(PROP_METRIC_REG_JVM_BUFFERS, new BufferPoolMetricSet(ManagementFactory.getPlatformMBeanServer()));
    
        metricRegistry.register(PROP_METRIC_REG_JCACHE_STATISTICS, new JCacheGaugeSet());
        if (hikariDataSource != null) {
            log.debug("Monitoring the datasource");
            hikariDataSource.setMetricRegistry(metricRegistry);
        }
        if (jHipsterProperties.getMetrics().getJmx().isEnabled()) {
            log.debug("Initializing Metrics JMX reporting");
            JmxReporter jmxReporter = JmxReporter.forRegistry(metricRegistry).build();
            jmxReporter.start();
        }
        if (jHipsterProperties.getMetrics().getLogs().isEnabled()) {
            log.info("Initializing Metrics Log reporting");
            final Slf4jReporter reporter = Slf4jReporter.forRegistry(metricRegistry)
                .outputTo(LoggerFactory.getLogger("metrics"))
                .convertRatesTo(TimeUnit.SECONDS)
                .convertDurationsTo(TimeUnit.MILLISECONDS)
                .build();
            reporter.start(jHipsterProperties.getMetrics().getLogs().getReportFrequency(), TimeUnit.SECONDS);
        }
    }
  • P50, P75... P99 : 속도에 대한 percentile 을 나타내는 것으로 전체 사용자중 PXX 의 뒤에 숫자가 분포에 따른 응답속도이다. (참고: https://ko.wikipedia.org/wiki/%EB%B0%B1%EB%B6%84%EC%9C%84%EC%88%98)
    • 즉, P50라면 전체 요청이 100 이라고 하였을 때 응답속도로 정렬을 하였을 때 50%의 응답속도를 보이는 사용자들의 평균 수치이다. 
    • raw data가 만일 Hive같은 data에 들어있다고 했을 경우 아래와 같은 쿼리로 데이터를 추출 해 낼 수 있다.
    •  select 
      round(percentile(cast(response_time AS BIGINT), 0.50),2) AS p50,
      round(percentile(cast(response_time AS BIGINT), 0.90),2) AS p90,
      round(percentile(cast(response_time AS BIGINT), 0.95),2) AS p95,
      count(1) AS request_count
      from  request_log
      where date >= '20170802' and reg_date< '20170803' and service_name='net.slipp.jhipster.web.rest.AccountResource.getAccount';

       

Cucumber

Protractor

 

JWT(JSON Web Token)

  • 웹 표준인 RFC 7519 로서 OAuth같이 인증을 위한 정보를 단순한 토큰 형태가 아닌 JSON자체를 토큰으로 활용하는 방식으로 인증이나 인가에 대한 추가정보를 다른 저장소에서 가져올 필요가 없이 토큰 자체에(일반적으로 Claim이라 부른다.) 저장을 하여 해당정보를 암호화(BASE64)처리하여 HTTP header같은 곳에 넣어 사용 할 수 있다. 
  • 그렇기 때문에 JWT를 가지고 서비스의 인증이나 인가를 처리 할 시 별도의 서버 작업이 없이 간단하게 처리가 가능해진다.
  • 이러한 토큰은 위변조 방지를 위하여 HMAC나 공개키 암호화 방식인 RSA으로 암호화를 하고 해당 정보를 Token에 같이 넣어서 처리가 가능하도록 구현되어있다. 
  • 사용하는 곳 
    • 인증
    • 정보 교환
  • JWT Structure
    • Header :암호화 알고리즘에 대한 설명을 가지고 있다. 
      • {
          "alg": "HS256",
          "typ": "JWT"
        }
    • Payload :Claim을 가지며 reserved, public and private 3가지의 형식이 있다. 
      • 예 

        {
          "sub": "1234567890",
          "name": "John Doe",
          "admin": true
        }
    • Signature : 아래 예를 보면 이해 할 수 있듯이 해당 토큰이 유효한지를 검사할 수 있도록 각 part부분을 암호화처리 하여 전달한다.
      • HMACSHA256(
          base64UrlEncode(header) + "." +
          base64UrlEncode(payload),
          secret)
  • 최종적으로 아래 그림과 같은 형태의 암화된 값이 나온다.
  • JWT에 대한 좀더 자세한 내용을 알고싶다면 http://bcho.tistory.com/999 를 참조하면된다. 

JHipster JWT

  • SecurityConfiguration.java 에서 처음 Project설정시 선택한 JWT Adapter를 추가하여 사용하고 있다.
    •  private JWTConfigurer securityConfigurerAdapter() {
          return new JWTConfigurer(tokenProvider);
      }

       

       

    • JWTConfigurer.java 에서 JWT에서 사용할 token Provider를 처리하고 설정을 추가한다.

    •  @Override
      public void configure(HttpSecurity http) throws Exception {
          JWTFilter customFilter = new JWTFilter(tokenProvider);
          http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
      }
    • JWTFiltre.java 에서 Filter chain형식으로 Bean을 등록 처리 한다.(java.servlet.filter를 구현한 Spring의 GenericFilterBean을 상속받아서 Filter chain으로 동작하도록 구현되어있다.)

    • public class JWTFilter extends GenericFilterBean {
    • 설치한 Jhipster에서 로그인시 network을 확인하면 아래와 같이 JWT token이 전달되는 것을 알 수 있다.

spring security

Spring Security 개요

  • Ben Alex의 Acegi security에서 Spring진영으로 넘어와 Spring security가 되었음.
  • Web application에서 기본적인 인증, 인가를 쉽게 수정하고 변경할 수 있도록 제공하는 Spring기반의 프레임워크. 
  • 아주 오래된 자료 : http://www.javajigi.net/pages/viewpage.action?pageId=205

Authentication 

  • 인증 : 네트워크 세큐리티에 관한 기술의 한 가지로, 어떤 것이 정말로 그것이 주장하고 있는 것이라는 것을 증명하는 일. 암호기술을 써서 실현된다. 메시지(통신문)에 대한 확증과, 사용자에 대한 확증이 있다. 전자는 전송되어 오는 메시지가 송신자가 보낸대로의 것.(컴퓨터 정보용어 대사전)
  • 인증방법
    • Cridential base

Authorization

Spring Security customizing

Jhipster Sprint Security 

  • Main class : SecurityConfiguration.java
    • SecurityConfiguration.java
      package net.slipp.jhipster.config;
      
      import net.slipp.jhipster.security.*;
      import net.slipp.jhipster.security.jwt.*;
      
      import io.github.jhipster.security.*;
      
      import org.springframework.beans.factory.BeanInitializationException;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.http.HttpMethod;
      import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
      import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
      import org.springframework.security.config.annotation.web.builders.HttpSecurity;
      import org.springframework.security.config.annotation.web.builders.WebSecurity;
      import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
      import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
      import org.springframework.security.config.http.SessionCreationPolicy;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
      import org.springframework.security.crypto.password.PasswordEncoder;
      import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension;
      import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
      import org.springframework.web.filter.CorsFilter;
      
      import javax.annotation.PostConstruct;
      
      @Configuration
      @EnableWebSecurity
      @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
      public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
      
          private final AuthenticationManagerBuilder authenticationManagerBuilder;
      
          private final UserDetailsService userDetailsService;
      
          private final TokenProvider tokenProvider;
      
          private final CorsFilter corsFilter;
      
          public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService,
                  TokenProvider tokenProvider,
              CorsFilter corsFilter) {
      
              this.authenticationManagerBuilder = authenticationManagerBuilder;
              this.userDetailsService = userDetailsService;
              this.tokenProvider = tokenProvider;
              this.corsFilter = corsFilter;
          }
      
          @PostConstruct
          public void init() {
              try {
                  authenticationManagerBuilder
                      .userDetailsService(userDetailsService)
                          .passwordEncoder(passwordEncoder());
              } catch (Exception e) {
                  throw new BeanInitializationException("Security configuration failed", e);
              }
          }
      
          @Bean
          public Http401UnauthorizedEntryPoint http401UnauthorizedEntryPoint() {
              return new Http401UnauthorizedEntryPoint();
          }
      
          @Bean
          public PasswordEncoder passwordEncoder() {
              return new BCryptPasswordEncoder();
          }
      
          @Override
          public void configure(WebSecurity web) throws Exception {
              web.ignoring()
                  .antMatchers(HttpMethod.OPTIONS, "/**")
                  .antMatchers("/app/**/*.{js,html}")
                  .antMatchers("/i18n/**")
                  .antMatchers("/content/**")
                  .antMatchers("/swagger-ui/index.html")
                  .antMatchers("/test/**");
          }
      
          @Override
          protected void configure(HttpSecurity http) throws Exception {
              http
                  .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
                  .exceptionHandling()
                  .authenticationEntryPoint(http401UnauthorizedEntryPoint())
              .and()
                  .csrf()
                  .disable()
                  .headers()
                  .frameOptions()
                  .disable()
              .and()
                  .sessionManagement()
                  .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
              .and()
                  .authorizeRequests()
                  .antMatchers("/api/register").permitAll()
                  .antMatchers("/api/activate").permitAll()
                  .antMatchers("/api/authenticate").permitAll()
                  .antMatchers("/api/account/reset_password/init").permitAll()
                  .antMatchers("/api/account/reset_password/finish").permitAll()
                  .antMatchers("/api/profile-info").permitAll()
                  .antMatchers("/api/**").authenticated()
                  .antMatchers("/websocket/tracker").hasAuthority(AuthoritiesConstants.ADMIN)
                  .antMatchers("/websocket/**").permitAll()
                  .antMatchers("/management/health").permitAll()
                  .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
                  .antMatchers("/v2/api-docs/**").permitAll()
                  .antMatchers("/swagger-resources/configuration/ui").permitAll()
                  .antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN)
              .and()
                  .apply(securityConfigurerAdapter());
      
          }
      
          private JWTConfigurer securityConfigurerAdapter() {
              return new JWTConfigurer(tokenProvider);
          }
      
          @Bean
          public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
              return new SecurityEvaluationContextExtension();
          }
      }
      
      
  • User detail : User.java
    • User.java
      package net.slipp.jhipster.domain;
      
      import net.slipp.jhipster.config.Constants;
      
      import com.fasterxml.jackson.annotation.JsonIgnore;
      import org.apache.commons.lang3.StringUtils;
      import org.hibernate.annotations.BatchSize;
      import org.hibernate.annotations.Cache;
      import org.hibernate.annotations.CacheConcurrencyStrategy;
      import org.hibernate.validator.constraints.Email;
      import org.springframework.data.elasticsearch.annotations.Document;
      
      import javax.persistence.*;
      import javax.validation.constraints.NotNull;
      import javax.validation.constraints.Pattern;
      import javax.validation.constraints.Size;
      import java.io.Serializable;
      import java.util.HashSet;
      import java.util.Locale;
      import java.util.Objects;
      import java.util.Set;
      import java.time.Instant;
      
      /**
       * A user.
       */
      @Entity
      @Table(name = "jhi_user")
      @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
      @Document(indexName = "user")
      public class User extends AbstractAuditingEntity implements Serializable {
      
          private static final long serialVersionUID = 1L;
      
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
      
          @NotNull
          @Pattern(regexp = Constants.LOGIN_REGEX)
          @Size(min = 1, max = 100)
          @Column(length = 100, unique = true, nullable = false)
          private String login;
      
          @JsonIgnore
          @NotNull
          @Size(min = 60, max = 60)
          @Column(name = "password_hash",length = 60)
          private String password;
      
          @Size(max = 50)
          @Column(name = "first_name", length = 50)
          private String firstName;
      
          @Size(max = 50)
          @Column(name = "last_name", length = 50)
          private String lastName;
      
          @Email
          @Size(min = 5, max = 100)
          @Column(length = 100, unique = true)
          private String email;
      
          @NotNull
          @Column(nullable = false)
          private boolean activated = false;
      
          @Size(min = 2, max = 5)
          @Column(name = "lang_key", length = 5)
          private String langKey;
      
          @Size(max = 256)
          @Column(name = "image_url", length = 256)
          private String imageUrl;
      
          @Size(max = 20)
          @Column(name = "activation_key", length = 20)
          @JsonIgnore
          private String activationKey;
      
          @Size(max = 20)
          @Column(name = "reset_key", length = 20)
          @JsonIgnore
          private String resetKey;
      
          @Column(name = "reset_date")
          private Instant resetDate = null;
      
          @JsonIgnore
          @ManyToMany
          @JoinTable(
              name = "jhi_user_authority",
              joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
              inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")})
          @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
          @BatchSize(size = 20)
          private Set<Authority> authorities = new HashSet<>();
      
          public Long getId() {
              return id;
          }
      
          public void setId(Long id) {
              this.id = id;
          }
      
          public String getLogin() {
              return login;
          }
      
          //Lowercase the login before saving it in database
          public void setLogin(String login) {
              this.login = StringUtils.lowerCase(login, Locale.ENGLISH);
          }
      
          public String getPassword() {
              return password;
          }
      
          public void setPassword(String password) {
              this.password = password;
          }
      
          public String getFirstName() {
              return firstName;
          }
      
          public void setFirstName(String firstName) {
              this.firstName = firstName;
          }
      
          public String getLastName() {
              return lastName;
          }
      
          public void setLastName(String lastName) {
              this.lastName = lastName;
          }
      
          public String getEmail() {
              return email;
          }
      
          public void setEmail(String email) {
              this.email = email;
          }
      
          public String getImageUrl() {
              return imageUrl;
          }
      
          public void setImageUrl(String imageUrl) {
              this.imageUrl = imageUrl;
          }
      
          public boolean getActivated() {
              return activated;
          }
      
          public void setActivated(boolean activated) {
              this.activated = activated;
          }
      
          public String getActivationKey() {
              return activationKey;
          }
      
          public void setActivationKey(String activationKey) {
              this.activationKey = activationKey;
          }
      
          public String getResetKey() {
              return resetKey;
          }
      
          public void setResetKey(String resetKey) {
              this.resetKey = resetKey;
          }
      
          public Instant getResetDate() {
             return resetDate;
          }
      
          public void setResetDate(Instant resetDate) {
             this.resetDate = resetDate;
          }
          public String getLangKey() {
              return langKey;
          }
      
          public void setLangKey(String langKey) {
              this.langKey = langKey;
          }
      
          public Set<Authority> getAuthorities() {
              return authorities;
          }
      
          public void setAuthorities(Set<Authority> authorities) {
              this.authorities = authorities;
          }
      
          @Override
          public boolean equals(Object o) {
              if (this == o) {
                  return true;
              }
              if (o == null || getClass() != o.getClass()) {
                  return false;
              }
      
              User user = (User) o;
              return !(user.getId() == null || getId() == null) && Objects.equals(getId(), user.getId());
          }
      
          @Override
          public int hashCode() {
              return Objects.hashCode(getId());
          }
      
          @Override
          public String toString() {
              return "User{" +
                  "login='" + login + '\'' +
                  ", firstName='" + firstName + '\'' +
                  ", lastName='" + lastName + '\'' +
                  ", email='" + email + '\'' +
                  ", imageUrl='" + imageUrl + '\'' +
                  ", activated='" + activated + '\'' +
                  ", langKey='" + langKey + '\'' +
                  ", activationKey='" + activationKey + '\'' +
                  "}";
          }
      }
      
      
  • UserDetailService : DomainUserDetailsService.java
    • DomainUserDetailsService.java
      package net.slipp.jhipster.security;
      
      import net.slipp.jhipster.domain.User;
      import net.slipp.jhipster.repository.UserRepository;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.security.core.GrantedAuthority;
      import org.springframework.security.core.authority.SimpleGrantedAuthority;
      import org.springframework.security.core.userdetails.UserDetails;
      import org.springframework.security.core.userdetails.UserDetailsService;
      import org.springframework.security.core.userdetails.UsernameNotFoundException;
      import org.springframework.stereotype.Component;
      import org.springframework.transaction.annotation.Transactional;
      
      import java.util.*;
      import java.util.stream.Collectors;
      
      /**
       * Authenticate a user from the database.
       */
      @Component("userDetailsService")
      public class DomainUserDetailsService implements UserDetailsService {
      
          private final Logger log = LoggerFactory.getLogger(DomainUserDetailsService.class);
      
          private final UserRepository userRepository;
      
          public DomainUserDetailsService(UserRepository userRepository) {
              this.userRepository = userRepository;
          }
      
          @Override
          @Transactional
          public UserDetails loadUserByUsername(final String login) {
              log.debug("Authenticating {}", login);
              String lowercaseLogin = login.toLowerCase(Locale.ENGLISH);
              Optional<User> userFromDatabase = userRepository.findOneWithAuthoritiesByLogin(lowercaseLogin);
              return userFromDatabase.map(user -> {
                  if (!user.getActivated()) {
                      throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated");
                  }
                  List<GrantedAuthority> grantedAuthorities = user.getAuthorities().stream()
                          .map(authority -> new SimpleGrantedAuthority(authority.getName()))
                      .collect(Collectors.toList());
                  return new org.springframework.security.core.userdetails.User(lowercaseLogin,
                      user.getPassword(),
                      grantedAuthorities);
              }).orElseThrow(() -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the " +
              "database"));
          }
      }
      
      

database

  • Liquibase를 사용하여 DB 변경사항을 관리 할 수 있도록 한다. 

LiquiBase (Home)

  • DB 스키마에 대한 버젼관리를 하여주고 자동화 하여 통합 빌드관리를 제공하여 DB변경 사항에 대한 협업및 관리를 쉽게 할 수 있도록 지원한다. (유사한 것으로 flyway가 있다.)
  • Liquibase vs flyway 
    • Liquibase
      • 이기종 DB간의 diff를 보여줄 수 있으며 Hibernate를 이용한 Diff도 제공한다.
      • 변경사항을 문서로 자동으로 만들어준다.
      • Rollback이 가능하다. (flyway에서는 신규 version을 만들어야 하지 않는가?)

 Hikari

  • Jhipster의 경우에는 Connection Pool을 BoneCP나 DBCP같은 것을 사용하지 않고 최근 상승세를 타고 있는 Hikari CP를 Connection Pool로 사용하고 있다.
  • 상세한 소계는 간단히 한글로 소개된 http://netframework.tistory.com/entry/HikariCP-%EC%86%8C%EA%B0%9C 를 참조하는 정도로 언급하고 넘어간다.

 

참고 : 

  • No labels