본문 바로가기

IT Book Summary/스프링 마이크로 서비스 코딩공작소

Chapter 07 마이크로서비스의 보안

  1. 사용자를 적절히 통제해 사용자 본인 여부를 확인하고 작업에 대한 권한 여부를 검증.
  2. 서비스가 실행되는 인프라스트럭처를 꾸준히 패치하고 최신 상태로 유지해 취약점의 위험을 최소화
  3. 서비스는 명확히 정의된 포트로만 접근하고 소수의 인가된 서버만 접근할 수 있도록 네트워크 접근을 통제.

 

첫번째 항목

마이크로서비스를 호출하는 사용자가 본인인지 인증하는 방법.

특정 마이크로서비스에서 요청한 작업을 수행할 수 있는 권한을 부여받았는지 확인하는 방법.

 

OAuth2는 토큰 기반 보안 프레임워크

스프링 부트와 스프링 클라우드 모두 OAuth2 서비스의 기본구현을 제공


7.1 OAuth2 소개

 

OAuth2를 구성하는 4개의 컴포넌트

  1. 보호자원
    적절한 권한을 부여받은 인증된 사용자만 액세스 할 수 있는 자원(마이크로서비스)
  2. 자원 소유자
    서비스에 접근할 수 있는 사용자, 그리고 수행할 수 있는 작업을 정의.
  3. 애플리케이션
    사용자를 대신해 서비스를 호출 할 애플리케이션.
  4. OAuth2 인증서버
    애플리케이션과 소비되는 서비스 사이의 중개자.

서로 통신하는 4개의 컴포넌트

 

사용자는 자격증명만 제시 , 인증에 성공해 서비스간 전달되는 인증토큰 발급받는다.

 

사용자 애플리케이션이 이용하는 서비스가 보호자원에 접근하려고 할때마다 토큰 제공. 

 

보호자원은 OtAuth2 서버에 접속해 토큰 유효성을 확인, 사용자의 role을 조회

 

 

웹보안에서 꼭 이해해야 할 내용

- 서비스를 호출할 대상

- 서비스를 호출하는 방법

- 코드에서 수행할 작업

 

OAuth 명세의 네가지 그랜트 타입

- 패스워드 password

- 클라이언트 자격증명 client credential

- 인가 코드 authorization code

- 암시적 implicit

 

참고 https://auth0.com/docs/protocols/protocol-oauth2


7.2 작게 시작: 스프링과 OAuth2로 1개의 엔드포인트 보호

OAuth 패스워드 그랜트 타입을 구현한다

  • 스프링 클라우드 기반의 OAuth2 인증서비스 설정
  • OAuth2 서비스와 사용자 신원을 인증 및 인가할 수 있도록 인가된 애플리케이션 역할을 하는 가짜 EagleEye UI 애플리케이션 등록
  • OAuth2 패스워드 그랜트 타입을 사용해 EagleEye 서비스를 보호.
    Postman 으로 사용자 로그인 시뮬레이션
  • 인증된 사용자만 호출할 있도록 라이선싱과 조직 서비스를 보호

참고 https://github.com/carnellj/spmia-chapter7

 

1 - EagleEye OAuth2 인증 서비스 설정

두가지 설정

1. 부트스트랩 클래스에서 필요한 메이븐 빌드 의존성을 설정

2. 서비스 진입점이 될 부트스트랩 클래스를 설정.

 

스프링 부트2에 oauth2 기능이 시큐리티에 포팅되어 있으므로 클라우드 시큐리티만 추가한다.

 

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-security</artifactId>
</dependency>

 

부트스트랩 클래스를 수정

@SpringBootApplication
@RestController
@EnableResourceServer
@EnableAuthorizationServer //OAuth 서비스
public class Application {
    @RequestMapping(value = { "/user" }, produces = "application/json") //사용자정보 조회
    public Map<String, Object> user(OAuth2Authentication user) {
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("user", user.getUserAuthentication().getPrincipal());
        userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
        return userInfo;
    }


    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }


}

 

애너테이션으로 OAuth2 서비스 서버임을 설정

/user 엔드포인트 추가

 

2 - 클라이언트 애플리케이션을 OAuth2 서비스에 등록

 

OAuth2Config 클래스는 OAuth2 서비스에 등록된 애플리케이션과 사용자 자격증명을 정의한다.

authentication-service/src/main/java/com/thoughtmechanix/authentication/security/OAuth2Config.java

@Configuration //설정 클래스
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override //서비스에 등록될 클라이언트 정의
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("eagleeye") //애플리케이션 이름
                .secret("thisissecret") //서버호출시 제시할 시크릿
                .authorizedGrantTypes("refresh_token", "password", "client_credentials") //인가 그랜트 타입 전달
                .scopes("webclient", "mobileclient"); //동작 범위
    }

    @Override //기본인증 관리자와 사용자 상세 서비스 이용한다고 알려줌
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
      endpoints
        .authenticationManager(authenticationManager)
        .userDetailsService(userDetailsService);
    }
}

 

웹 애플리케이션은 webclient 스코프만 사용, 모바일 애플리케이션은 mobileclient 스코프 사용

민감한 정보를 다룰때 데이터 액서스 메커니즘을 기반으로 한 데이터 제한 방식이 흔히 사용.

 

 

3 - EagleEye 사용자 구성

 

authentication-service/src/main/java/com/thoughtmechanix/authentication/security/WebSecurityConfigurer.java

@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
        @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean(); //시큐리티 인증 처리하는데 사용
    }

   @Override
    @Bean
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean(); //반환될 사용자정보
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication() //사용자와 패스워드, 역할 정의
                .withUser("john.carnell").password("password1").roles("USER")
                .and()
                .withUser("william.woodward").password("password2").roles("USER", "ADMIN");
    }
}

 

OAuth2Config.java  configure 메서드에 삽입됨.

이 빈들은 /auth/oauth/token/ 과 /auth/user 엔드포인트 구성하는데 사용됨.

 

4 - 사용자 인증

 

http://localhost:8901/auth/oauth/token/ 

Postman 으로 HTTP 양식 매개변수로 다음의 정보 전달

grant_type: password

scope: webclient

username: john.carnell

password: password1

 

반환된 페이로드

access_token: 서비스를 호출할 때마다 제시할 토큰.

token_type: 일반적인 토큰 타입은 bearer 토큰. 

refresh_token: 만료된 토큰을 재발행하기 위한 토큰

expires_in: 만료시간. 기본값 12시간

scope: 토큰이 유효한 범위

 

이렇게 받은 access_token을 HTTP 헤더값에 추가하여 서비스 호출.


7.3 OAuth2를 사용한 조직 서비스 보호

사용자 역할과 작업 수행의 권한 여부는 각각의 서비스에서 정의한다.

 

1 - 스프링 시큐리티와 OAuth2 jar 파일을 각 서비스에 추가

organization-service/pom.xml 에 스프링 클라우드 시큐리티 의존성 추가

 

2 - OAuth2 인증 서비스 통신을 위한 서비스 구성

 

보호자원에서 토큰유효성을 확인해 주어야 한다.

 

application.yml 파일에서 콜백 URL 정의

security:
  oauth2:
    resource:
       userInfoUri: http://localhost:8901/auth/user

 

조직서비스가 보호자원이라고 지정해야 한다.

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
@EnableResourceServer //보호자원으로 지정하는 애너테이션
public class Application {
    @Bean
    public Filter userContextFilter() {
        UserContextFilter userContextFilter = new UserContextFilter();
        return userContextFilter;
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

 

3 - 서비스의 접근 대상 정의

 

인증되고 특정 역할을 가진 사용자만 서비스 URL에 접근할 수 있다.

 

인증된 사용자로 서비스 보호

Bearer access_token값과 함께 Authorization HTTP 헤더를 추가해야 한다.

ADMIN 역할을 가진 인증된 사용자에게만 접근을 제한.

@Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
    @Override 모든 접근 규칙을 재정의
    public void configure(HttpSecurity http) throws Exception{
        http
        .authorizeRequests()
          .antMatchers(HttpMethod.DELETE, "/v1/organizations/**") //접근
          .hasRole("ADMIN") //접근할수 있는 역할
          .anyRequest()
          .authenticated();
    }
}

 

4 - OAuth2 액세스 토큰 전파

 

한 서비스에서 다른 서비스로 OAuth 토큰을 어떻게 전달할 것인가?

 

  1. OAuth2 서버에서 인증받고 웹어플리케이션 호출, 토큰은 사용자 세션에 저장. 헤더에 토큰 추가한다.
    라이선싱 서비스는 주울 서비스 게이트웨이 뒤에서만 접근 가능.
  2. 주울은 서비스 엔드포인트 검색한 후 한 서버에 호출 전달.
    서비스 게이트웨이는 유입되는 호출의 Authorization헤더를 복사해 새 엔드포인트에 전달.
  3. 보호자원인 라이선싱 서비스는 토큰의 유효성 확인하고 권한 확인.
    사용자의 액세스 토큰을 조직서비스에 전달.
  4. 조직서비스는 헤더에서 토큰을 OAuth2서비스에 토큰의 유효성 검증.

 

 

OAuth2RestTemplate으로 OAuth 액서스 토큰 전파

@Component
public class OrganizationRestTemplateClient {
    @Autowired
    OAuth2RestTemplate restTemplate; //토큰전파 처리

    private static final Logger logger = LoggerFactory.getLogger(OrganizationRestTemplateClient.class);

    public Organization getOrganization(String organizationId){
        logger.debug("In Licensing Service.getOrganization: {}", UserContext.getCorrelationId());

        ResponseEntity<Organization> restExchange = // 조직서비스 호출
                restTemplate.exchange(
                        "http://zuulserver:5555/api/organization/v1/organizations/{organizationId}",
                        HttpMethod.GET,
                        null, Organization.class, organizationId);

        return restExchange.getBody();
    }
}

7.4 자바스크립트 웹 토큰과 OAuth2

JWT 장점과 특징

- 작다

-암호로 서명되어있다

-자체 완비형이다

-확장 가능하다

 

1 - JWT 발행을 위한 인증 서비스 수정

 

spring-security-jwt 의존성 추가.

 

JWT 토큰 저장소 설정

@Configuration
public class JWTTokenStoreConfig {
  @Autowired
  private ServiceConfig serviceConfig;
  @Bean
  public TokenStore tokenStore() {
    return new JwtTokenStore(jwtAccessTokenConverter());
  }
  @Bean
  @Primary //특정타입의 빈이 둘 이상일때 표시된 타입을 자동 주입하도록 스프링에 설정.
  public DefaultTokenServices tokenServices() { //전달된 토큰에서 데이터를 읽음.
    DefaultTokenServices defaultTokenServices
        = new DefaultTokenServices();
    defaultTokenServices.setTokenStore(tokenStore());
    defaultTokenServices.setSupportRefreshToken(true);
    return defaultTokenServices;
  }
  @Bean
  public JwtAccessTokenConverter jwtAccessTokenConverter() { 
  //JWT와 OAuth2 서버사이 변환기로 동작
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey(serviceConfig.getJwtSigningKey()); //토큰서명에 사용되는 서명키
    return converter;
  }
  @Bean
  public TokenEnhancer jwtTokenEnhancer() {
    return new JWTTokenEnhancer();
  }
}

 

OAuth2 서비스에 구성정보를 정의.

 

@Configuration
public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter {
  @Autowired
  private AuthenticationManager authenticationManager;
  @Autowired
  private UserDetailsService userDetailsService;
  @Autowired
  private TokenStore tokenStore;
  @Autowired
  private DefaultTokenServices tokenServices;
  @Autowired
  private JwtAccessTokenConverter jwtAccessTokenConverter;
  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 
    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); 
    tokenEnhancerChain.setTokenEnhancers(
     Arrays.asList(
       jwtTokenEnhancer,
       jwtAccessTokenConverter));
     endpoints
      .tokenStore(tokenStore) //토큰저장소 삽입
      .accessTokenConverter(jwtAccessTokenConverter) //JWT 연결
      .authenticationManager(authenticationManager)
      .userDetailsService(userDetailsService);
  }
...
}

 

2 - 마이크로서비스에서 JWT 사용

1. 메이븐 시큐리티 jwt 의존성 추가

2. 라이선싱 서비스와 조직 서비스에서 JWTTokenStoreConfig 클래스 설정. 인증 서비스와 거의 동일한 클래스.

3.라이선싱 서비스에서 토큰 전파 수행

 

사용자 정의 RestTemplate 클래스 생성해 JWT 토큰 삽입.

public class Application {
   
  @Primary
  @Bean
  public RestTemplate getCustomRestTemplate() {
    RestTemplate template = new RestTemplate();
    List interceptors = template.getInterceptors();
    if (interceptors == null) {
      template.setInterceptors(Collections.singletonList(
        new UserContextInterceptor())); //Authorization 헤더를 모든 REST 호출에 삽입
    } else {
      interceptors.add(new UserContextInterceptor()); //Authorization 헤더를 모든 REST 호출에 삽입
      template.setInterceptors(interceptors);
    }
      return template;
    }
}

 

Rest 호출에 JWT 토큰을 주입하는 UseContextInterceptor

public class UserContextInterceptor implements ClientHttpRequestInterceptor {
  @Override
  public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                             ClientHttpRequestExecution execution) throws IOException {
    headers.add(UserContext.CORRELATION_ID,UserContextHolder.getContext().getCorrelationId());
    headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());
    return execution.execute(request, body);
  }
}

 

3 - JWT 토큰 확장

JWT 토큰 안 organizationId 필드는 JWT 토큰을 생성할때 직접 추가한 것.

TokenEnhancer 클래스를 인증 서비스에 추가하며 JWT 토큰을 쉽게 확장할 수 있다.

public class JWTTokenEnhancer implements TokenEnhancer {
    @Autowired
    private OrgUserRepository orgUserRepo;

    private String getOrgId(String userName){ //사용자 조회
        UserOrganization orgUser = orgUserRepo.findByUserName( userName );
        return orgUser.getOrganizationId();
    }

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        
        Map<String, Object> additionalInfo = new HashMap<>();
        String orgId =  getOrgId(authentication.getName());

        additionalInfo.put("organizationId", orgId);
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); 
        //추가속성은 HashMap에 추가하고 메서드에 전달된 accessToken 변수 설정
        return accessToken;
    }
}

 

그리고 JWTTokenStoreConfig 클래스에 대한 빈 정의를 추가해준다.

 

JWTOAuth2Config에 tokenEnhancer 빈을 연결해 configure()를 호출할 때 전달된 endpoint매개변수에 tokenEnhancerChain 연결

 

4 - JWT 토큰에서 사용자 정의 필드 파싱

 

메이븐에 JJWT 라이브러리 추가

 

JWT 토큰에서 organizationId 파싱

 

@Component
public class TrackingFilter extends ZuulFilter{
  private String getOrganizationId(){
    String result="";
    if (filterUtils.getAuthToken()!=null){
      String authToken = filterUtils.getAuthToken().replace("Bearer ","");
      //Authorization 헤더에서 토큰을 파싱
      try {
        Claims claims = Jwts.parser() //서명키 전달하며, jwt클래스 사용해 토큰 파싱
                         .setSigningKey(serviceConfig.getJwtSigningKey().getBytes("UTF-8"))
                         .parseClaimsJws(authToken).getBody();
        result = (String) claims.get("organizationId"); //organizationId를 가져옴.
       } catch (Exception e){
         e.printStackTrace();
       }
    }
    return result;
  }
}

 


7.5 마이크로서비스 보안을 마치며

마이크로서비스 보안구축을 위한 지침

  1. 모든 서비스 통신에 HTTPS/SSL 사용
  2. 모든 서비스 호출은 API 게이트웨이를 통과
  3. 공개 API와 비공개 API 영역을 정함.
  4. 불필요한 네트워크 포트를 차단해 마이크로서비스의 공격 범위를 제한

마이크로서비스 보안 아키텍처

 

모든 서비스 통신에 HTTPS/SSL 사용

운영환경 마이크로서비스는 HTTPS 및 SSL로 제공되는 암호화된 채널로 통신해야한다.

DevOps 스크립트를 사용해 HTTP 구성 및 설정을 자동화

 

마이크로서비스에 접근하려면 서비스 게이트웨이 사용

서비스가 실행되는 각 서버와 서비스 엔드포인트, 포트가 클라이언트에서 직접 접근할 수 없어야 한다.

서비스 게이트웨이는 진입점과 게이트 키퍼 역할.

 

공개 API와 비공개 API 영역으로 서비스 분리

서비스를 2개의 별개 영역 (공개, 비공개영역) 으로 분리해 최소 권한을 구현

 

공개영역

- 클라이언트로 소비되는 공개 API 

- 서비스 게이트웨이 뒤에있어야 하며 자체 인증 서비스를 보유해야 함.

- 접근하려는 경로는 서비스 게이트웨이가 보호가는 단일경로.

 

비공개영역

- 핵심 애플리케이션 기능과 데이터 보호

- 단일 포트로만 접근. 서비스가 실행중인 네트워크 서브넷, 트래픽만 수용

- 자체 서비스 게이트웨이와 인증서비스 보유해야 함.

- 모든 애플리케이션 데이터는 비공개 영역의 네트워크 서브넷에 두고 비공개 영역의 마이크로서비스만 접근.

 

불필요한 네트워크 포크를 차단해 마이크로서비스에 대한 공격 지점 제한

노출하는 최소한의 포트수를 제한.

필요한 포트의 인바운드 및 아웃바운드 접근만 허용해야한다.

아웃바운드 포트를 차단하면 서비스가 공격받더라도 데이터 유출을 막을수 있다.


7.6 요약

  • OAuth2 는 사용자를 인증하는 토큰 기반의 인증 프레임워크
  • OAuth2를 이용하면 사용자 요청을 처리하는 각 마이크로서비스를 호출할 때마다 사용자 자격 증명을 제공할 필요 없음.
  • 그랜트라는 다양한 메커니즘을 제공해 웹서비스 호출을 보호
  • 스프링에서 사용하려면 OAuth2 기반의 인증 서비스를 설정
  • 서비스를 호출하려는 모든 애플리케이션은 OAuth2 인증 서비스에 등록되어야 함.
  • 애플리케이션마다 고유 애플리케이션 이름과 시크릿 키가 있다.
  • 사용자 자격 증명과 역할은 메모리나 데이터 저장소에 있고 스프링 보안으로 접근
  • 각 서비스는 역할이 수행할 행위를 정의
  • 스프링 클라우드 시큐리티는 JWT 명세를 지원
  • JWT는 OAuth2 토큰을 생성하기 위해 서명된 자바스크립트 표준을 정의
  • JWT를 사용하면 사용자 정의 필드를 명세에 삽입할수 있음.
  • 마이크로서비스 보안은 OAuth2를 사용하는 것 이상을 포함
  • HTTPS를 사용해 서비스 간 모든 호출을 암호화
  • 서비스 게이트웨이를 사용해 서비스에 접근 가능한 지점을 줄여야 함.
  • 서비스가 실행되는 운영 체제의 인바운드 및 아웃바운드 포트 수를 제한해 서비스 공격지점을 제한