RDS ( Relational Database Service )

AWS에서 DB의 종류와 템플릿을 설정할 수 있다.

 

 

설정

DB 인스턴스 식별자는 인스턴스를 여러 개 만들었을 때, 각 고유의 이름이다.
마스터 사용자 이름은 데이터베이스에 접속하려면 사용자와 비밀번호를 알양 한다.

 

RDS의 요금에 영향을 미치는 것은 인스턴스 유형, 저장장치의 용량이다.

 

가용성 및 내구성

다중 AZ 배포는 리전 지역에 2-3개의 가성 지역에 대기 인스턴스 생성을 해서 이중 저장을 하는 것이다.
안정성이 올라가지만 비용도 두배로 든다는 특성이 있다.

 

VPC

AWS 안에서 외부로부터 독립되고 안전한 네트워크를 구성해주는 서비스
VPC를 생성하고 그 안에다가 AWS RDS 인스턴스를 만들면, 외부에서 직접적으로 RDS에 접속하는 것을 막을 수 있다.

 

설정하는 부분에서 추가적으로, 데이터 베이스 인증, 옵션, 백업 설정, 모니터링 기능 활성화, 로그 내보내기 등의 설정들을 할 수 있다.

추가로, 유지관리(마이너 버전 자동 업그레이드 사용) 및 삭제 방지 활성화 기능도 사용할 수 있다.

 

 


데이터 베이스 서버 접속

접속 1 - 퍼블릭 방식으로 접속

기본적으로 RDS의 연결&보안 탭에 보안 > 퍼블릭 액세스 가능 여부를 확인해야 한다.
해당 센션에 추가 구성 > 퍼블릭 액세스 가능 설정을 해주면 된다.

수정 사항 요약에서 수정 예약항목을 즉시 적용으로 해주고 DB 인스턴수 수정을 해준다.

그리고, 엔드포인트로 접속을 하면 퍼블릭 방식으로 접속을 할 수 있게 된다.

 

But, 보안 그룹 때문에 안될 수도 있다.

VPC보안 그룹 에서 인 바운드 규칙, 아웃 바운드 규칙의 설정 값을 바꿔줘야 하는 경우가 있다.

인바운드 : 데이터베이스 서버 입장에서 안으로 들어오는 것
아웃바운드 : 응답하는 것

 

인바운드 규칙

유형 : MYSQL / Aurora
프로토콜 : TCP
PORT : 3306
소스 : 내 IP

> 규칙 저장

 

접속 2 - 비공개 상태로 접속

1단계 : 외부에서 접속할 수 없는 RDS 만들기

DB 인스턴스 수정 페이지에서 퍼블릭 액세스 불가능 설정 후 RDS 연결 & 보안 탭 > 인바운드 규칙 편집설정으로 이동한다.
모든 인바운드 규칙을 제거 한다.

 

2단계 : RDS와 같은 VPC에 소속된 EC2 인스턴스 만들기

새로운 EC2를 생성해준다.
세부 정보 > 퍼블릭 IP 자동 할당 (활성화) > 스토리지 추가 > 태그 추가 > 보안 그룹 구성 (22번 포트 활성화 확인) > 검토 및 시작
> 기존 키 페어 성택(기존 키 존재 시) > 인스턴스 시작

생성된 인스턴스의 설명 탭 > IPv4 퍼블릭 IP or 퍼블릭 DNS(IPv4) 접속 
SSH > MYSQL 설치

  • sudo apt update
  • sudo apt install mysql-client

 

추가로, EC2가 RDS에 접속하려면 보안그룹 정책을 업데이트 해야 한다.

생성된 EC2 인스턴스의 보안 그룹을 EC2-Security라 설정한다. 그 후, 보안 그룹 ID를 복사한다.
그 후, RDS 인스턴스에서 보안그룹을 들어가 AWS2-RDS-security를 설정한다.

RDS 보안 그룹의 인바운드 규칙 편집에 들어가서 소스 > 사용자 지정 란에 EC2 인스턴스 보안 그룹 ID를 등록한다.

 

이렇게 설정하게 되면 EC2가 속한 보안 그룹 B의 머신들은 RDS가 속한 보안 그룹 A에 접속할 수 있도록 허용된다.

S3 : Simple Storage Service

중요한 파일의 경우 내구성이 좋도록(리전을 N개)의 위치에 저장할 수 있다. 즉, 안전하게 파일을 보관하는 저장소이다.

 

 

버킷

버킷의 이름과 리전을 정하여 생성할 수 있다.
퍼블릭 액세스 여부도 설정할 수 있으며, 버전 관리도 할 수 있다.

버킷을 생성하고, 수정을 할 수 있으며 관리, 권한, 속성, 객체등을 수정할 수 있다.

 

 

폴더

버킷안에 폴더를 생성할 수 있다. 폴더도 수정, 삭제를 할 수 있다.

 

객체

실제 데이터를 담는 그릇인 객체를 생성할 수 있다.
폴더안에 생성될 수 있으며, 객체들에 대한 권한과 속성을 수정할 수 있다.

특히, 속성의 스토리지 클래스에서는 가격, 속도, 안정성에 따라 비용이 달라질 수 있으며 이를 설정 할 수 있다.

 

이 객체들에 대해서 객체 URL이 생성되어 링크로 다른 사람에게 객체를 공유할 수 있다. 또한, 권한에서 ACL(액세스 제어 목록) 리스트에 위쪽에 위치한 객체 소유자를 통해 권한을 설정할 수 있다.

 

 

 


스토리지 클래스

파일의 속성에 있는 스토리지 클래스에 대해 알아보자

  • 지능적 계층화 : 파일에 대한 접근을 예측할 수 없거나 접근이 변화하는 경우에 사용하는 클래스
  • Standard-IA : 자주 접속하지 않는다는 뜻으로 가격을 조절할 수 있다.
  • One Zone-IA : 잃어버려도 되는 정도의 파일
  • Glacier : 얼려 놓기만 해도 되는 정도의 파일들

 

 

요금 체계

  • 스토리지 : 저장 공간에 대한 요금
  • 요청 및 데이터 검색 : 얼마나 사용했느냐에 대한 내용. 얼마나 다운로드하고 업로드 했느냐?
  • 데이터 전송 요금 : S3에 있는 파일을 다운로드할 때는 돈이 든다.

 

이렇듯 각자의 장/단점에 따라 요금이 다르기 때문에 상활 별로 잘 선택해서 사용해야 한다.

 

 

웹 서버

S3에는 웹 서버 기능이 내장되어 있다. 속성에서 정적 웹 사이트 호스팅을 활성화 하면 S3에 보관한 파일로 전 세계 누구나 방문할 수 있는 웹사이트를 설정 할 수 있다.

 

CDN

AWS에서는 cloudFront라는 서비스를 제공하는데, 이를 CDN(Content Delivery Network)라고 부른다. 전 세계에 CDN서버를 분산시켜 놨다가 요청이 들어오면 가장 가까운 곳에 미리 저장하고 있었던 콘텐츠를 보내 주는 것이다.

 

버전 관리

S3에서는 또한 자체적으로 버전관 관리 기능을 가지고 있다. 버킷의 속성에 버킷 버전 관리 기능을 활성화 하면 시스템이력을 남기고 관리할 수 있다.

 

라이프 사이클

S3에서는 라이프 사이클이라는 기능을 통해 Standard에 있던 파일이 시간이 지나면 Standard-IA -> Glacier로 가게 하는 등의 작업을 자동화할 수 있다.

 

 


S3를 서버로 활용하기

 

정적 웹 호스팅

S3 버킷에 index.html 파일을 올려놓고, 속성 탭에서 정적 웹 사이트 호스팅 편집을 할 수 있다.
인덱스 문서에 html을 입력해주고, 버킷 웹 사이트 엔드포인트 UR로 접근하게 되면 인덱스 페이지에 접근 할 수 있다.

 

 

캐시

사용자에게 한번 컨텐츠를 노출하고 난 뒤에 다시 호출되었을 때는, 저장된 결과 응답을 바로 하는 것을 저장의 의미로 캐시라 한다. 이러한 역할을 전담하는 서버를 캐시 서버라 한다.
CloudFront가 이 캐시서버의 역할을 한다.

 

 

CDN

어떤 지역에 있든 위치의 한계를 극복하게 해주는 것을 CDN(Content Delivery Network)라 한다.

 

 

aws에서 cloudFront를 생성할 수 있다.

  1. 사용자가 cloudFront 접속
  2. cloudFront가 클라이언트 돼서 웹서버에게 요청
  3. 웹 서버가 자신의 정보를 cloudFront에 전송
  4. cloudFront는 웹 서버의 정보를 저장하고 사용자에게 응답
  5. 사용자가 cloudFront로 접속
  6. 웹 서버의 정보를 cloudFront가 저장하게 되면 그 다음의 요청은 cloudFront가 응답

 

 

배포 생성 내 원본 도메인에 주목해야 한다.

 

AWS의 서비스를 사용해도 되고, 자신이 직접 운영하고 있는 서버의 주소를 붙여넣기 해도 된다.

 

이렇게 CloudFront를 생성한 후, 해당 주소를 통해 인덱스 페이지에 반복접근하게 되면 페이지 로딩속도가 빨라진 것을 확인할 수 있다.
하지만 이를 이용하게 되면 동적페이지로 변경되는 데이터가 느린 시간동안 캐시를 먹어 변하지 않는 현상이 발생하게 된다.

 

이를 개선하기 위해서는 캐시 설정에 대한 부분을 변경해야 한다.

 


CloudFront를 이용해서 전 세계에 배포하기

CloudFront 동작 > 캐시 키 및 원본 요청 속성 값 변경란에서는 캐시를 얼마동안 살려둘 것인지를 설정하는 속성 값이 있다.

즉, max-age 만큼 웹서버가 클라이언트에게 제공한 정보를 유지하겠다는 의미이다.

 

캐시 설정에서는 무효화 설정, 캐시 미사용 설정, 쿼리 문자열 설정 등의 값들을 설정할 수 있다.

 

 

 

CDN

CloudFront 설정의 편집값에서 모든 엣지 로케이션 사용(최고의 성능)을 설정하게 되면, CDN이 설정된다.
요금이 과금될 수 있으니 요금표를 잘 보고 설정해야 한다.

 

컴퓨터 한대를 EC2에서는 인스턴스라고 한다. 즉, 5대의 컴퓨터가 필요하다고 하면 5개를 만들면 된다.

 

해당 인스턴스 시작 페이지에서 인스턴스를 프리티어로 만들 수 있다.
이렇게 만들어진 인스턴스를 원격제어 할 수 있다.

 

 

비용 관리

여기서 비용에 관련된 부분을 확인할 수 있다.

 

 

보안

MFA설정을 통해 보안을 강화할 수 있다.

 

 

S3

파일 저장소라고 생각하면 되고, 버킷을 만들 수 있다.

버킷에는 퍼블릭 액세스 차단 설정도 할 수 있다.

 

이렇게 만든 버킷에는 파일을 업로드하고, 그 저장소의 URL도 얻을 수 있다.
작업 메뉴의 ACL을 통해 퍼블릭으로 설정 할 수도 있다.

 

버킷의 커맨드라인으로도 제어가 가능하다.
또한, 프로그래밍을 통해서도 제어할 수 있다. 예제에서는 node.js를 통해 코드를 구현하여 S3에 파일을 업로드한다.

 

 

 

AWS 서비스 구성
. EC2 : AWS에서 제공하는 원격 서버
  . 오토 스케일링 그룹
  . 타킷 그룹
  . 로드 밸란서
 . RDS : AWS에서 제공하는 원격 데이터베이스

 

가상의 PC 또는 서버 EC2

EC2는 클라우드 컴퓨팅 서비스로 가상의 PC, 즉, 서버 한 대를 임대하는 서비스

 

유동적으로 EC2를 관리해주는 오토 스케일링 그룹

트래픽이 늘어남에 따라 EC2가 늘어날 수 있고, 이때 오토 스케일링 그룹이 사용자의 요청 횟수에 따라 EC2를 늘리거나 줄인다.

 

요청을 분산시켜주는 로드 밸런서

요청이 동시에 많이 들어오게 되면, 로드밸랜서가 분산을 시켜준다. 또한, 요청을 어디로 분산시킬지 그룹을 정해야하는데 이를 대상 그룹이라고한다.

 

데이터 저장소 RDS

범용 데이터베이스인 아마존 관계형 데이터 베이스로 RDS를 사용한다.

 

 

일래스틱 빈스토크(Elasitc Beanstalk)

서비스를 한 번에 설정하는 서비스.

  • 어플리케이션 생성
  • 소스를 번들의 형태, 예를 들면 java.war파일로 애플리케이션 버전을 일래스톡 빈스토크에 업로드
  • 일래스틱 빈스토크가 자동으로 환경을 실행하고 코드 실행에 필요한 AWS 리소스를 생성하고 구성
  • 환경 실행 후에는 환경을 직접 관리하고 필요한 경우 버전 업데이트를 하거나 새로운 앱 버전을 배포

 

 

 


AWS계정 생성 후 콘솔로 이동!

 

 

일래스틱 빈스토크로 서버 구축

리전을 서울로 설정해준다.

 

 

 

시작하기 > 애플레이케이션 생성을 해준다.

 

생성화면에서 플랫폼 및 애플리케이션 코드를 설정한다.

 

 

 

의존성 추가하기

OAuth2를 사용하기 위한 스타터 추가

 

 

쿠키 관리 클래스 추가

  • addCookie : 요청 값을 바탕으로 HTTP 응답에 쿠키를 추가합니다.
  • 쿠키 이름을 입력받아 쿠키를 삭제한다. 실제 삭제 방법은 없어서 파라미터로 넘어온 키의 쿠키를 빈 값으로 바꾸고 만료기간을 0으로 설정해서 쿠키가 재생성 되자마자 만료처리한다.

  • serialize : 객체를 직렬화해 쿠키의 값으로 들어갈 값으로 변환
  • deserialize : 쿠키를 역직렬화해 객체로 변환

 

 

OAuth2 서비스 구현

사용자 정보를 조회해 users 테이블에 사용자 정보가 있다면 리소스 서버에서 제공해주는 이름을 업데이트
정보가 없다면 새 사용자를 생성해 데이터베이스에 저장하는 서비스를 구현

domain 패키지의 User.java 파일에 사용자 이름과 OAuth 관련 키를 저장하는 코드 추가

 

 

config > oauth 패키지에 클래스를 생성한다.
loadUser()를 통해 사용자를 조회하고 사용자가 users 테이블에 사용자 정보가 있다면 이름을 업데이트하고 없다면 saveOrUpdate() 메서드를 실행해 users 테이블에 회원 데이터를 추가한다.

 

 

약간 복잡한 OAuth2 설정 파일 작성

OAuth2와 JWT를 함께 사용하려면 설정 파일들을 알맞게 수정 해야한다.

 

기존 WebSecurityConfig.java 내용을 모두 주석처리 한 후, 작업 시작

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package study.springbootdeveloper.config;
 
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import study.springbootdeveloper.BlogService.UserService;
import study.springbootdeveloper.config.jwt.TokenProvider;
import study.springbootdeveloper.config.oauth.OAuth2AuthorizationRequestBasedOnCookieRepository;
import study.springbootdeveloper.config.oauth.OAuth2SuccessHandler;
import study.springbootdeveloper.config.oauth.OAuth2UserCustomService;
import study.springbootdeveloper.repository.RefreshTokenRepository;
 
import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console;
 
@RequiredArgsConstructor
@Configuration
public class WebOAuthSecurityConfig {
    private final OAuth2UserCustomService oAuth2UserCustomService;
    private final TokenProvider tokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;
    private final UserService userService;
 
    @Bean
    public WebSecurityCustomizer configure() {
        //스프링 시큐리티 기능 비활성화
        return (web) -> web.ignoring()
                .requestMatchers(toH2Console())
                .requestMatchers("/img/**""/css/**""/js/**");
    }
 
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        // 토큰 방식으로 인증을 하기 때문에 기존에 사용하던 폼로그인, 세션 비활성화 .. (1)
        http.csrf().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .logout().disable();
 
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
 
        //헤더를 확인할 커스텀 필터 추가 .. (2)
        http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
 
        //토큰 재발급 URL은 인증 없이 접근 가능하도록 설정. 나머지 API URL은 인증 필요 .. (3)
        http.authorizeHttpRequests()
                .requestMatchers("/api/token").permitAll()
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll();
 
        http.oauth2Login()
                .loginPage("/login")
                .authorizationEndpoint()
                //Authorization 요청과 관련된 상태 저장 ..(4)
                .authorizationRequestRepository(oAuth2AuthorizationRequestBasedOnCookieRepository())
                .and()
                .successHandler(oAuth2SuccessHandler()) // 인증 성공 시 실행할 핸들러 ..(5)
                .userInfoEndpoint()
                .userService(oAuth2UserCustomService);
 
        http.logout()
                .logoutSuccessUrl("/login");
 
        // /api로 시작하는 url인 경우 401 상태 코드를 반환하도록 예외 처리 .. (60
        http.exceptionHandling()
                .defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
                        new AntPathRequestMatcher("/api/**"));
        return http.build();
    }
 
    @Bean
    public OAuth2SuccessHandler oAuth2SuccessHandler(){
        return new OAuth2SuccessHandler(tokenProvider, refreshTokenRepository,
                oAuth2AuthorizationRequestBasedOnCookieRepository(),
        userService);
    }
 
    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter(){
        return new TokenAuthenticationFilter(tokenProvider);
    }
 
    @Bean
    public OAuth2AuthorizationRequestBasedOnCookieRepository oAuth2AuthorizationRequestBasedOnCookieRepository(){
        return new OAuth2AuthorizationRequestBasedOnCookieRepository();
    }
 
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
 
cs

config > WebOAuthSecurityConfig.java 파일을 새롭게 생성한다.

  • filterChain() : 토큰 방식으로 인증 하므로 기존 폼 로그인, 세션 기능을 비활성화
  • addFilterBefore() : 헤더 값을 확인 할 커스텀 필드 추가
  • authorizationRequests() : 토큰 재발급 URL은 인증 없이 접근하도록 설정하고 나머지 API들은 모두 인증해야 접근하도록 설정
  • oauth2Login() : 필요한 정보를 쿠키에 저장해서 쓸 수 있도록 인증 요청과 관련된 상태를 저장할 저장소를 설정. 인증 성공 시 실행할 핸들러를 설정한다.
  • exceptionHandling() : /api로 시작하는 url인 경우 인증 실패 시 401 상태 코드 즉, Unathorized를 반환

 

 

OAuth2AuthorizationRequestBasedOnCookieRequestRepository.java

OAuth2에 필요한 정보를 세션이 아닌 쿠키에 저장해서 쓸 수 있도록 인증 요청과 관련된 상태를 저장할 저장소

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package study.springbootdeveloper.config.oauth;
 
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
import org.springframework.web.util.WebUtils;
import study.springbootdeveloper.util.CookieUtil;
 
public class OAuth2AuthorizationRequestBasedOnCookieRepository implements
        AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
 
    public final static String OAUTH2_AUTHORIZATION_REQEUST_COOKIE_NAME = "oauth2_auth_request";
    private final static int COOKIE_EXPIRE_SECONDS = 18000;
 
    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
                                                                 HttpServletResponse response) {
        return this.loadAuthorizationRequest(request);
    }
 
    @Override
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
        Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQEUST_COOKIE_NAME);
        return CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class);
    }
 
    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest,
                                         HttpServletRequest request, HttpServletResponse response){
        if(authorizationRequest == null){
            removeAuthorizationRequestCookies(request, response);
            return;
        }
        CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQEUST_COOKIE_NAME,
                CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
    }
 
    public void removeAuthorizationRequestCookies(HttpServletRequest request,
                                                  HttpServletResponse response){
        CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQEUST_COOKIE_NAME);
    }
}
 
cs

 

 

UserService

인증 성공 시 실행할 핸들러 구현을 위해 userService를 수정해야 한다.

 

 

 

OAuth2SuccessHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package study.springbootdeveloper.config.oauth;
 
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
import study.springbootdeveloper.BlogService.UserService;
import study.springbootdeveloper.config.jwt.TokenProvider;
import study.springbootdeveloper.domain.RefreshToken;
import study.springbootdeveloper.domain.User;
import study.springbootdeveloper.repository.RefreshTokenRepository;
import study.springbootdeveloper.util.CookieUtil;
 
import java.io.IOException;
import java.time.Duration;
 
@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    public static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token";
    public static final Duration REFRESH_TOKEN_DURATION = Duration.ofDays(14);
    public static final Duration ACCESS_TOKEN_DURATION = Duration.ofDays(1);
    public static final String REDIRECT_PATH = "/articles";
 
    private final TokenProvider tokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;
    private final OAuth2AuthorizationRequestBasedOnCookieRepository authorizationRequestRepository;
    private final UserService userService;
 
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
        User user = userService.findByEmail((String) oAuth2User.getAttributes().get("email"));
 
        //리프레시 토큰 생성 -> 저장 -> 쿠키에 저장 ..(1)
        String refreshToken = tokenProvider.generateToken(user, REFRESH_TOKEN_DURATION);
        saveRefreshToken(user.getId(), refreshToken);
        addRefreshTokenToCookie(request, response, refreshToken);
 
        //액세스 토큰 생성 -> 패스에 액세스 토큰을 추가 .. (2)
        String accessToken = tokenProvider.generateToken(user, ACCESS_TOKEN_DURATION);
        String targetUrl = getTargetUrl(accessToken);
 
        //인증 관련 설정 값, 쿠키 제거 .. (3)
        clearAuthenticationAttributes(request, response);
 
        //리다이렉트 .. (4)
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
 
    //생성된 리프레시 토큰을 전달받아 데이터베이스에 저장
    private void saveRefreshToken(Long userId, String newRefreshToken){
        RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId)
                .map(entity -> entity.update(newRefreshToken))
                .orElse(new RefreshToken(userId, newRefreshToken));
 
        refreshTokenRepository.save(refreshToken);
    }
 
    //생성된 리프레시 토큰을 쿠키에 저장
    private void addRefreshTokenToCookie(HttpServletRequest request,
                                         HttpServletResponse response, String refreshToekn){
        int cookieMaxAge = (int) REFRESH_TOKEN_DURATION.toSeconds();
        CookieUtil.deleteCookie(request, response, REFRESH_TOKEN_COOKIE_NAME);
        CookieUtil.addCookie(response, REFRESH_TOKEN_COOKIE_NAME, refreshToekn, cookieMaxAge);
    }
 
    // 인증 관련 설정 값, 쿠키 제거
    private void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response){
        super.clearAuthenticationAttributes(request);
        authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
    }
 
    //액세스 토큰을 패스에 추가
    private String getTargetUrl(String token){
        return UriComponentsBuilder.fromUriString(REDIRECT_PATH)
                .queryParam("token", token)
                .build()
                .toUriString();
    }
}
 
cs

스프링 시큐리티는 별도의 핸들러를 지정하지 않으면 로그인 성공 이후 SimpleUrlAuthenticationSuccessHandler을 사용한다.
일반적인 기능은 동일하고 토큰과 관련된 추가작업을 위해 오버라이드를 사용하여 함수를 정의한다.

  • (1) : 토큰 제공자를 사용해 리프레시 토큰을 만든 뒤, saveRefresthToken() 메서드를 호출해 리프레시 토큰을 데이터베이스에 유저 아이디와 저장. 그 후, 액세스 토큰이 만료되면 재발급 요청하도록 addRefreshTokenToCookie() 메서드를 호출해 쿠키에 리프레시 토큰을 저장
  • (2) : 토큰 제공자를 사용해 액세스 토큰을 만든 뒤, 리다이렉트 경로가 담긴 값을 가져와 쿼리 파라미터에 액세스 토큰을 추가한다.
  • (3) : 인증 프로세스를 진행하면서 세션과 쿠키에 임시로 저장해둔 인증 관련 데이터를 제거한다. 기본적으로 제공하는 clearAuthenticationAttributes()는 그대로 호출하고 removeAuthorizationReqeustCookies()를 추가 호출해 OAuth 인증을 위해 저장된 정보도 삭제한다.
  • (4) : (2)에서 만든 URL로 리다이렉트

 

 

 


글에 글쓴이 추가

article.java 도메인에 글쓴이 필드를 추가한다.

 

author 필드가 추가됨으로써 수정되어야 할 부분들 수정

AddArticleRequest DTO 에 해당 필드값을 추가해준다.
BlogService.java에서 글쓴이를 같이 리포지포리로 직렬화해서 넘겨준다.
현재 인증 정보를 가져오는 principal객체를 파라미터로 추가한다. 인증 객체에서 유저이름을 가져와서 서비스단으로 넘겨준다.
글 상세 페이지에서도 글쓴이가 보이도록 필드를 축해준다.

 

 

data.sql에도 데이터를 추가해준다.

 

 


뷰 구성하기

뷰 컨트롤러에 오어스 로그인 페이지를 리턴해준다.

 

 

resources/static/img 경로에 구글 아이콘을 생성해준다.

 

token.js / article.js 에 관련된 함수를 작성

article.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// 삭제 기능
const deleteButton = document.getElementById('delete-btn');
 
if (deleteButton) {
    deleteButton.addEventListener('click'event => {
        let id = document.getElementById('article-id').value;
        function success() {
            alert('삭제가 완료되었습니다.');
            location.replace('/articles');
        }
 
        function fail() {
            alert('삭제 실패했습니다.');
            location.replace('/articles');
        }
 
        httpRequest('DELETE',`/api/articles/${id}`, null, success, fail);
    });
}
 
// 수정 기능
const modifyButton = document.getElementById('modify-btn');
 
if (modifyButton) {
    modifyButton.addEventListener('click'event => {
        let params = new URLSearchParams(location.search);
        let id = params.get('id');
 
        body = JSON.stringify({
            title: document.getElementById('title').value,
            contentdocument.getElementById('content').value
        })
 
        function success() {
            alert('수정 완료되었습니다.');
            location.replace(`/articles/${id}`);
        }
 
        function fail() {
            alert('수정 실패했습니다.');
            location.replace(`/articles/${id}`);
        }
 
        httpRequest('PUT',`/api/articles/${id}`, body, success, fail);
    });
}
 
// 생성 기능
const createButton = document.getElementById('create-btn');
 
if (createButton) {
    // 등록 버튼을 클릭하면 /api/articles로 요청을 보낸다
    createButton.addEventListener('click'event => {
        body = JSON.stringify({
            title: document.getElementById('title').value,
            contentdocument.getElementById('content').value
        });
        function success() {
            alert('등록 완료되었습니다.');
            location.replace('/articles');
        };
        function fail() {
            alert('등록 실패했습니다.');
            location.replace('/articles');
        };
 
        httpRequest('POST','/api/articles', body, success, fail)
    });
}
 
 
// 쿠키를 가져오는 함수
function getCookie(key) {
    var result = null;
    var cookie = document.cookie.split(';');
    cookie.some(function (item) {
        item = item.replace(' ''');
 
        var dic = item.split('=');
 
        if (key === dic[0]) {
            result = dic[1];
            return true;
        }
    });
 
    return result;
}
 
// HTTP 요청을 보내는 함수
function httpRequest(method, url, body, success, fail) {
    fetch(url, {
        method: method,
        headers: { // 로컬 스토리지에서 액세스 토큰 값을 가져와 헤더에 추가
            Authorization: 'Bearer ' + localStorage.getItem('access_token'),
            'Content-Type''application/json',
        },
        body: body,
    }).then(response => {
        if (response.status === 200 || response.status === 201) {
            return success();
        }
        const refresh_token = getCookie('refresh_token');
        if (response.status === 401 && refresh_token) {
            fetch('/api/token', {
                method: 'POST',
                headers: {
                    Authorization: 'Bearer ' + localStorage.getItem('access_token'),
                    'Content-Type''application/json',
                },
                body: JSON.stringify({
                    refreshToken: getCookie('refresh_token'),
                }),
            })
                .then(res => {
                    if (res.ok) {
                        return res.json();
                    }
                })
                .then(result => { // 재발급이 성공하면 로컬 스토리지값을 새로운 액세스 토큰으로 교체
                    localStorage.setItem('access_token', result.accessToken);
                    httpRequest(method, url, body, success, fail);
                })
                .catch(error => fail());
        } else {
            return fail();
        }
    });
}
cs

 

 

token.js

 

 

BlogService.java 파일에서 본인 글이 아닌데 수정, 삭제를 시도하는 경우, 제외처리를 하도록 수정한다.

 

 

로컬호스트 접속시 !

 

 

oauth를 통해 로그인을 하게 되고, 개발자도구에서 application > Storage > local storage를 보게 되면 토큰이 발급된 걸 볼 수 있다.

 

쿠키에 리프레시 토큰도 잘 확인된다.

 

 

액세스 토큰이 없어서 실패하면, 다시 액세스 토큰을 발급받아 인증 요청

OAuth란?

제3의 서비스에 계정관리를 맡기는 것.

 

리소스 오너 정보를 취득하는 4가지 방법

  • 권한 부여 코드 승인 타입
  • 암시적 승인 타입
  • 리소스 소유자 암호 자격증명 승인 타입
  • 클라이언트 자격증명 승인 타입

 

권한 부여 코드 승인 타입

  • 권한 요청이란? 
    클라이언트, 즉, 스프링 부트 서버가 특정 사용자 데이터에 접근하기 위해 권한 서버, 즉, 카카오나 구글 권한 서버에 요청을 보내는 것
GET spring-authorization-server.example/authorize?
	client_id=66a36b4c2&
    redirect_uri=http://localhost:8080/myapp&
    response_type=code&
    scope=profile
  • client_id : 인증 서버가 클라이언트에 할당하나 고유 식별자. OAuth 서비스에 등록할 때 서비스에 생성하는 값
  • redirect_uri : 로그인 성공 시 이동해야 하는 URI
  • response_type : 인증 코드를 받을 때는 code값을 포함해야 한다. 클라이언트가 제공받길 원하는 응답 타입
  • scope : 제공받고자 하는 리소스 오너의 정보 목록

 

데이터 접근용 권한 부여
인증 서버에 처음 요청을 보낼 때에만 로그인 페이지로 변경하고 사용자의 데이터 접근 동의를 얻는다. 그 후부터는, 로그인만 진행한다. 로그인이 성공하게 되면 권한 부여 서버는 데이터에 접근할 수 있게 인증 및 권한 부여를 수신

 

인증 코드 제공
사용자가 로그인에 성공하면 권한 요청 시에 파라미터로 보낸 redirect_uri로 리다이렉션된다. 이때 파라미터에 인증 코드를 함께 제공한다.

GET http://localhost:8080/myapp?code=a1s2f3mcj2

 

액세스 토큰 응답이란?
인증 코드를 받으면 액세스 토큰으로 교환해야 한다. 이는 로그인 세션에 대한 보안 자격을 증명하는 식별 코드로 /token POST 요청을 보낸다.

POST spring-authrization-server.example.com/token
{
	"client_id" : "66a37v4b2",
    "client_secret" : "aabb11nn44",
    "redirect_uri" : "http://localhost:8080/myapp",
    "grant_type" : "authorization_code",
    "code" : "a1b2b3bcbqksd"
}
  • client_secret : OAuth 서비스에 등록할 때 제공받는 비밀키
  • grant_type : 권한 유형을 확인할 때 사용한다. 이때, authorization_code로 설정해야 한다. 권한 서버는 요청 값을 기반으로 유효한 정보인지 확인하고, 유효한 정보라면 액세스 토큰을 응답한다.
{
	"access_token" : "aasbffb",
    "token_type" : "Bearer",
    "expires_in" : 3600,
    "scope" : "openid profile",
    ...
}

 

 

액세스 토큰으로 API 응답 & 반환
제공받은 액세스 토큰을 통해 리소스 오너의 정보를 가져올 수 있다. 정보가 필요할 때마다 API호출을 통해 정보를 가져오고 서버는 토큰이 유효한지 검사한 뒤 응답

GET spring-authorization-resource-server.example.com/userinfo
Header : Authorization : Bearer aasbdffb

 

 

쿠키란 ?

웹사이트가 사용하는 서버에서 로컬 환경에 저장하는 작은 데이터
쿠키는 키와 값으로 이루어져 있으며 만료 기간, 도메인 등의 정보를 가지고 있다. HTTP 요청을 통해 쿠키의 특정 키에 값을 추가할 수 있다.

 

 

 


토큰 발급받기

 

1단계 )

https://cloud.google.com/cloud-console?hl=ko

 

Google Cloud 콘솔  |  Cloud Console - 웹 UI 관리

콘솔에서 데이터 분석, VM, 데이터 스토어, 데이터베이스, 네트워킹, 개발자 서비스 등 클라우드 애플리케이션의 모든 것을 관리하세요.

cloud.google.com

콘솔에서 새로운 프로젝트를 생성한다.

 

 

여기서 생성된 설정 값을 스프링 부트 애플리케이션 설정 파일에 등록해야 한다.

 

 

application.yml

토큰 기반 인증이란 무엇일까 ?

사용자 인증 확인 방법에는 서버 기반 인증과 토큰 기반 인증이 있다.
먼저, 지난 실습 (3) 글에서 정리한 내용이 세션 기반인증

토큰을 이용한 인증이 이번 장에서 설명한 토큰 기반 인증이다.

 

 

토큰 인증 과정

  • 클라이언트가 로그인요청을 서버로 보낸다
  • 서버는 유효성 검증을 하고, 통과하면 토큰을 생성하여 응답한다.
  • 클라이언트는 토큰을 저장한다.
  • 이후 인증이 필요한 API를 사용할 때 토큰을 함께 보낸다.
  • 서버는 토큰의 유효성을 검증한다.
  • 토큰이 유효하다면 클라이언트가 요청한 내용을 처리한다.

 

 

토큰 기반 인증의 특징

  • 무상태성 : 서버 입장에서 클라이언트의 인증 정보를 저장하거나 유지하지 않아도 되기 때문에 완전한 무상태으로 검증할 수 있다.
  • 확장성 : 하나의 토큰으로 결제 서버와 주문 서버에게 요청을 보낼 수 있는 다른 서비스에 권한을 공유할 수 있는 확정성이 있다.
  • 무결성 : 토큰 정보를 변경할 수 없는 무결성이 보장된다.

 

 


JWT

JWT는 "." 을 기준으로 헤더, 내용, 서명으로 이루어져 있다.

-> aaaaa(header) . bbbbb(payload) . cccccc(signature)

 

  1. 헤더 : 토큰의 타입과 해싱 알고리즘을 저장하는 정보를 답는다
    {
       "typ" : "JWT", ( 토큰의 타입을 지정한다. JWT라는 문자열이 들어간다. )
       "alg" : "HS256" ( 해싱 알고리즘을 지정 )
     }
  2. 내용 : 토큰과 관련된 정보를 담는다. 내용의 한 덩어리를 클레임이라 부르며, 이는 키값의 한 쌍으로 이루어져 있다.

이름 설명
iss 토큰 발급자(issuer)
sub 토큰 제목(subject)
aud 토큰 대상자(audience)
exp 토큰의 만료시간 , NumericDate 형식
nbf 활성 날짜와 비슷한 개념으로 Not Before를 의미하며 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.
iat 토큰이 발급된 시간
jti 고유 식별자로서 주로 일회용 토큰에 사용

 공개 클레임 : 공개 되어도 무방한 클레임. 보통 URI로 이름을 짓는다.
 비공개 클레임 : 공개되면 안되는 클레임.

 3. 서명 : 토큰이 조작/변경 되지 않았음을 확인하는 용도로, 헤더의 인코딩 값과 내용의 인코딩 값을 합친 후에 주어진 비밀키를 사용해 해시값을 생성한다.

 

 

토큰의 유효기간

사용자의 보안과 관련된 이슈에 대하여 토큰의 유효기간 때문에 리프레쉬 토큰이라는 것을 사용한다.
이는 액세스 토큰 발급 후, 추 후 발급되는 토큰이다.

  • 2 : 요청 검증 후 리프레시 토큰도 생성하여 클라이언트에 전달하고 디비에도 저장한다.
  • 그 후, 액세스 토큰이 만료되면 만료 응답을 보내고 클라이언트 에서는 리프레시 토큰과 함께 새로운 액세스 토큰을 요청한다.
  • 유효한 리프레시 토큰일 경우 새로운 액세스 토큰을 생성한 뒤 응답한다.

 

 

이론 끝


JWT 구현하기

 

의존성 추가

 

토큰 제공자 추가

이슈 발급자와 비밀키를 설정한다.

 

 

해당 값들을 변수로 접근하는 데 사용할 클래스를 만든다.

 

 

계속해서 토큰을 생성하고 올바른 토큰인지 유효성을 검사하고, 토큰에서 필요한 정보를 가져오는 클래스

 

 

validToken : 토큰이 유효한지 검증. 프러퍼티즈 파일에 선언한 비밀값과 함께 토큰 복호화를 진행
getAuthentication : 토큰을 받아 인증 정보를 담은 객체를 반환하는 메서드. 프로퍼티즈 파일에 저장한 비밀 값으로 토큰을 복호화한 뒤 클레임을 가져오는 getClaims()를 호출해서 클레임 정보를 반환받아 사용자 이메일이 들어 있는 토큰 제목 sub와 토큰 기반으로 인증 정보를 생성한다. 
getUserId : 프로퍼티즈 파일에 저장한 비밀값으로 토큰을 복호화한 다음 클레임을 가져오는 getClaims를 호출해서 클레임 정보를 반환받고 클레임에서 id 키로 저장된 값을 가져와 반환

 

빌더 패턴을 사용해 객체를 만들 때 테스트가 필요한 데이터만 선택. 필드 기본값을 사용.

 

 

테스트 코드

 

 

 

 

 

 

 

 

리프레시 토큰 도메인 구현

리프레시 토큰 도메인 생성

 

리포지토리 생성

 

 

토큰 필터 구현

토큰 필터는 요청 처리 전에 로직으로 전달되기 전후에 URL 패턴에 맞는 모든 요청을 처리하는 기능을 한다. 헤더값을 비교해서 토큰이 있는지 확인하고 유효 토큰이라면 시큐리티 콘텍스트 홀더(Security Text Holder)에 인증 정보를 저장한다.

시큐리티 컨텍스트는 인증 객체가 저장되는 보관소로, 인증 정보가 필요할 때 언제든지 인증 객체를 꺼내 사용한다. 이 클래스는 스레드마다 공간을 할당하는 스레드 로컬에 저장되므로 코드의 아무곳에서나 참조할 수 있고, 다른 스레드와 공유하지 않아서 독립적으로 사용할 수 있다.시큐리티 컨텍스트 객체를 저장하는 객체가 시큐리티 컨텍스트 홀더다.

 

 

토큰의 유효성 검사를 한 후, 유효하다면 인증 정보를 관리하는 시큐리티 컨텍스트에 인증정보를 설정
작성한 코드가 실행되며 인증 정보가 설정된 이후에 컨텍스트 홀더에서 getAuthentication() 메서드를 사용해 인증 정보를 가져오면 유저 객체가 반환
유저 객체에는 유저이름과 권한목록과 같은 인정 정보가 포함

 

 

토큰 API 구현하기

리프레시 토큰을 전달받아 검증하고, 유효한 리프레시 토큰이라면 새로운 액세스 토큰을 생성하는 토큰 API를 구현한다.

 

 

서비스 추가

UserService단에 유저 ID 검색 메서드를 추가해준다.

 

전달받은 리프레시 토큰으로 리프레시 토큰 객체를 검색해서 전달하는 메서드 구현

 

 

전달받은 리프레시 토큰으로 토큰 유효성 검사를 진행하고, 유효한 토큰일 때 리프레시 토큰으로 사용자 ID를 찾는 메서드
마지막으로, 사용자 ID로 사용자를 찾은 후 토큰 제공자의 generateToken()메서드를 호출해서 새로운 액세스 토큰을 생성한다.

 

 

토큰 생성 요청 및 응답을 담당할 DTO를 생성한다.

 

 

요청이 오면 토큰 서비스에서 리프레시 토큰을 기반으로 새로운 액세스 토큰을 만들어준다.

 

 

 

테스트 코드

 

 

토큰 기반 인증은 인증에 토큰을 사용하는 방식으로, 토큰은 클라이언트를 구분하는 데 사용하는 유일한 값으로서 서버에서 생성해서 클라이언트에게 제공한 뒤, 클라이언트는 서버에 요청할 때마다 요청 내용과 함께 토큰을 전송합니다. 서버에서는 토큰으로 유효한 사용자인지 검증합니다.

JWT는 토큰 기반 인증에서 주로 사용하는 토큰으로, JSON 형식으로 사용자의 정보를 저장한다. 헤더.내용.서명 구조로 이루어져 있으며, 헤더는 토큰의 타입과 해싱알고리즘, 정보에는 토큰을 담을 정보, 서명은 해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도

리프레시 토큰은 액세스 토큰이 만료되었을 때 새로운 토큰 액세스 토큰을 발급받는 용도

필터는 실제로 요청이 전달되기 전/후에 URL 패턴에 맞는 모든 요청을 처리하는 기능을 제공

시큐리티 콘텍스트는 인증 객체가 저장되는 보관소로, 인증 정보가 필요할 때 언제든지 인증 객체를 꺼내어 사용하도록 제공되는 클래스로 시큐리티 컨텍스트를 저장하는 객체가 시큐리티 컨텍스트 홀더다.

+ Recent posts