본문 바로가기
Backend boot camp/Session4

[Spring Security] OAuth 2

by orioncsy 2022. 12. 9.

OAuth2

개념

OAuth 2

  • 전통적인 방식은 특정 애플리케이션에서 서비스를 제공하면서 사용자 크리덴셜을 직접 관리
  • 반면 OAuth 2 인증 프로토콜은 특정 애플리케이션에서 사용자 인증을 직접 처리하는 것이 아니라 신뢰할 만한 서드 파티 애플리케이션에 인증을 위임하고 resource에 대한 자격 증명 토큰을 발급 후 그 토큰을 사용하여 서드 파티 애플리케이션 서비스를 사용하게 해주는 방식
  • 외부 크리덴셜이 직접적으로 제공되지 않아 크리덴셜을 따로 관리할 필요가 없어 보안성 향상

OAuth 2 사용 애플리케이션 유형

  • 써드 파티 애플리케이션에서 제공하는 API
    • 써드 파티에서 제공하는 API를 직접적으로 사용하는 애플리케이션을 구현할 때 사용
  • 추가 인증 서비스 제공 용도
    • 추가적인 인증 서비스를 제공하기 위한 용도로 사용

OAuth2 동작 방식

OAuth 2 인증 컴포넌트 역할

  • Resource Owner
    • 사용하고자 하는 Resource의 소유자
    • 서비스를 이용하는 사용자
  • Client
    • Resource Owner를 대신해 보호된 Resource를 접근하는 애플리케이션
  • Resource Server
    • Client의 요청을 수락하고 Resource Owner에게 해당 Resource를 제공하는 서버
  • Authorization Server
    • Client가 Resource server에 접근할 수 있는 권한을 부여하는 서버
    • Resource Owner가 인증 성공하면 Client에게 Access Token 형태로 Resource Owner의 Resource에 접근할 수 있는 권한 부여

OAuth 2 컴포넌트 상호작용 흐름

  1. Resource Owner는 Client에게 OAuth2 인증을 요청합니다.
  2. Client는 Resource Owner가 Resource Owner의 계정 정보를 관리하고 있는 서드 파티 애플리케이션에 로그인하도록 서드 파티 애플리케이션 로그인 페이지로 redirect한다.
  3. Resource Owner는 로그인 인증 시도
  4. 인증 성공하면 Authorization Server가 Owner의 인증 성공을 증명하는 Access Token을 Client에게 전송
  5. Access Token을 받은 Client는 Resource Server에게 Owner 소유의 Resource 요청
  6. Resource Server는 Client가 전송한 Access Token을 검증하여 자격이 증명되면 Resource를 Client에게 전송

OAuth 2 인증 프로토콜에서 사용되는 용어

  • Authorization Grant
    • Client 애플리케이션이 Access Token을 얻기 위한 Resource Owner의 권한을 표현하는 Credential
    • Authorization Code, Implicit Grant Type, Resource Owner Password Credentials, Client Credentials 방식이 존재
  • Access Token
    • Client가 Resource Server에 resource에 접근하기 위해 사용하는 자격 증명 토큰
    • Authorization Code, Client Secret을 사용해 Access Token으로 자격 증명하여 resource 접근
  • Scope
    • Access Token을 사용하여 접근할 수 있는 Resource 범위

Authorization Grant 유형

  1. Authorization Code Grant : 권한 부여 승인 코드 방식
  • 자체 생성한 Authorization Code를 전달하는 방식으로, 기본적인 방식
  • Refresh Token을 사용할 수 있다.
  • 권한 부여 승인 요청 시 response_type을 code로 지정하여 요청
  • 인증 처리 흐름
    • Resource Owner가 서비스 요청을 Client에게 전송
    • Client가 Authorization Server에게 Authorization Code를 요청(미리 생성한 Client ID, Redirect URI, 응답 타입을 함께 전송)
    • Resource Owner가 로그인 페이지에서 로그인 진행
    • 로그인이 확인되면 Authorization Server에서 Authorization Code를 Client에게 전달(이전 요청과 함께 보낸 Redirect URI로 code 전달)
    • Client는 전달받은 Authorization Code를 이용해 Access Token 발급 요청(미리 생성한 Client secret, Redirect URI, 권한 부여 방식, Authorization Code를 함께 전송)
    • 요청 정보 확인 후 Redirect URI로 Access Token 발급
    • 발급받은 Access Token으로 Client가 Resource Server에 Resource 요청
    • Resource Server가 Access Token을 확인 후 resource를 Client에게 전달
  1. Implicit Grant : 암묵적 승인 방식
  • 별도의 Authorization code 없이 바로 Access Token을 발급하는 방식
  • 인증 처리 흐름
    • Resource Owner가 서비스 요청을 Client에게 전송
    • Client는 Authorization server에게 접근 권한 요청(Client ID, Redirect URI, 응답 타입 전송)
    • Resource Owner는 로그인 페이지를 통해 로그인
    • 로그인이 확인되면 Authorization Server는 Client에게 Access Token 전달
    • Client는 Access Token을 사용해 Resource Server에게 Resource 요청
    • Resource Server가 Access Toekn 확인 후 Resource를 전달
  1. Resource Owner Password Credential Grant : 자원 소유자 자격 증명 승인 방식
  • 간단하게 로그인 시 필요한 정보로 Access Token을 발급받는 방식
  • 자신의 서비스에서 제공하는 애플리케이션의 경우만 사용되는 인증 방식으로 Refresh Token 사용 가능
  • Client가 Authorization server, Resource server와 모두 같은 시스템에 속했을 경우 사용 가능
  • 구글 계정으로 구글 캘린더, 클라우드 등에 접근하는 경우
  • 인증 과정
    • Resource Owner가 서비스 요청을 Client에게 전송, 로그인에 필요한(Username/Password)를 이용해 요청
    • Client는 Resource Owner에게 전달받은 로그인 정보를 통해 Authorization Server에 Access Token 요청(Client ID, 권한 부여 방식, 로그인 정보 함께 전달)
    • 요청과 함께 들어온 정보를 확인하여 Client에게 Access Token 전달
    • Client는 Access Token을 사용해 Resource Server에게 Resource 요청
    • Resource Server가 Access Toekn 확인 후 Resource를 전달
  1. Client Credentials Grant : 클라이언트 자격 증명 승인 방식
  • Client 자신이 관리하는 resource 혹은 Authorization Server에 해당 Client를 위한 Resource 접근 권한이 설정되어 있는 경우 사용 가능한 방식
  • Refresh Token은 사용 불가하고 자격 증명을 안전하게 보관할 수 있는 Client에게만 사용해야 한다.
  • 인증 흐름
    • Client가 Authorization Server에 Access Token 요청
    • Authorization Server가 Client에게 액세스 토큰 전달
    • Client는 Access Token을 사용해 Resource Server에게 Resource 요청
    • Resource Server가 Access Toekn 확인 후 Resource를 전달

Spring Security에서 OAuth 2 인증

OAuth 2 인증 사전 작업

구글 API 콘솔에서 OAuth 2 설정

프로젝트 생성

  • 메인 화면에 프로젝트 만들기를 클릭
  • 프로젝트 이름을 설정하고 만들기 클릭
  • 사용 설정된 API 및 서비스 상단에 프로젝트 선택 메뉴를 클릭하고 생성된 프로젝트 선택

OAuth 동의 화면 만들기

  • 왼쪽 메뉴에서 OAuth 동의 화면 클릭
  • User Type을 외부로 선택 체크하고 만들기 클릭
  • 앱 이름, 사용자 지원 이메일, 개발자 연락처 정보 입력 후 저장 후 계속
  • 테스트 사용자 화면에서 저장 후 계속

사용자 인증 정보 생성

  • 왼쪽 메뉴에 사용자 인증 정보 클릭
  • 사용자 인증 정보 만들기를 클릭 후 OAuth 클라이언트 ID 클릭
  • 애플리케이션 유형은 웹 애플리케이션 선택/애플리케이션 이름 설정
  • 승인된 리디렉션 URI에 http://localhost:8080/login/oauth2/code/google 입력
  • 만들기 클릭하면 클라이언트 ID와 보안 비밀번호 확인

OAuth 2 샘플 애플리케이션 구현

의존성 추가

  • dependencies에 아래 추가
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
  • HTML 화면 구성을 위한 템플릿을 위해 타임리프(Thymeleaf) 추가, Spring Security 추가, OAuth 2클라이언트 추가

보호된 웹 페이지

  • SSR 방식의 웹 애플리케이션은 HTML로 렌더링되는 페이지 존재
  • HTML 코드를 간단하게 작성
  • Controller 클래스를 생성하여 @GetMapping()으로 연결
    • SSR 방식의 핸들러 메서드는 리턴 타입이 String 이고 뷰 이름을 리턴하여 뷰 이름.html을 웹브라우저로 전송

OAuth 2 인증을 위한 SecurityConfiguration

  • SecurityConfiguration 클래스 생성
    • @Configuration을 붙여 생성하고 @Bean을 붙인 filterChain 생성
    • HttpSecurity 객체인 http를 매개 변수로 받는다.
    • .csrf().disable().formLogin().disable().httpBasic().disable() 설정
    • .authorizeHttpRequests(a→a.anyRequest().authenticted())
      • 인증된 request에 대해서만 접근 허용
    • .oauth2Login(withDefaults())를 추가해 OAuth2 로그인 활성화
    • return http.build() 추가
  • application.yml에서 ClientID와 ClientSecret 설정
  • 민감정보는 ${}를 통해 환경 변수로 설정한다.
spring:
	security:
    oauth2:
      client:
        registration:
          google:
            clientId: ${CLIENT-ID}
            clientSecret: ${CLIENT-SECRET}

Configuration을 통한 OAuth 2 인증 설정

  • 자동 구성을 통한 OAuth2 인증 설정 외에 Configuration을 통해 Bean을 등록해 설정 가능
  • SecurityConfiguration 클래스에서
    • 필드 멤버로 clientId, clientSecret을 선언
    • 각자 @Value()를 붙여서 application.yml 경로를 “${}” 형태로 입력
    • @Bean을 붙인 clientRegistrationRepository() 메서드를 선언
      • 반환형은 ClientRegistrationRepository 를 가진다.
      • clientRegistration() 메서드를 통해 ClientRegistration 객체를 받는다.
      • ClientRegistrationRepository의 구현체인 InMemoryClientRegistrationRepository의 인스턴스를 위에서 받은 객체를 생성자로 넣어서 반환
    • clientRegistration() 메서드
      • CommonOAuth2Provider라는 enum은 내부적으로 Builder 패턴을 이용해서 ClientRegistration 인스턴스 제공
      • CommonOAuth2Provider.GOOGLE.getBuilder(”goole”).clientId(clientId).clientSecret(clientSecret).build();

인증된 Authentication 정보 확인

  • SecurityContext를 이용하여 확인
    • HomeController 에서 home() 핸들러 메서드는 인증이 된 후에 실행된다.
    • var oAuth2User = (OAuth2User)SecurityContextHolder.getContext().getAuthentication().getPrincipal()
    • 위와 같이 SecurityContext에서 정보를 가져온다.
    • System.out.println(oAuth2User.getAttributes().get("email")) 를 통해 email 정보를 가져와 출력한다.
  • Authentication 객체를 핸들러 메서드 파라미터로 전달받는 방법
    • home() 메서드에서 Authentication authentication으로 매개변수를 받아서
    • var oAuth2User = (OAuth2User)authentication.getPrincipal() 을 통해 정보를 가져온다.
    • 출력하여 정보 확인한다.
  • OAuth2User를 파라미터로 받는 방법
    • home() 메서드에서 @AuthenticationPrincipal 애너테이션을 붙여 OAuth2User oAuth2User를 매개 변수로 받는 것이 가능하다.

Authorization Server로부터 전달받은 Access Token 확인

  • OAuth2AuthorizedClientService를 DI 받는 방법
    • HomeController에서
      • 필드 멤버로 OAuth2AuthorizedClientService 객체를 선언하고 DI 받는다.
      • home() 메서드 매개변수로 Authentication을 받는다.
      • var authorizedClient = authorizedClientService.loadAuthorizedClient("google", authentication.getName()) 를 통해 OAuth2AuthorizedClient 객체 로드
      • OAuth2AccessToken accessToken = authorizedClient.getAccessToken();을 통해 토큰 가져와서 정보 출력
  • OAuth2AuthorizedClient를 핸들러 메서드의 파라미터로 받는 방법
    • home() 메서드의 매개 변수로 @RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient authorizedClient 를 선언

OAuth2와 JWT를 이용한 샘플 애플리케이션 구현

Frontend와 Backend 간 OAuth 2 인증 흐름

  1. Resource Owner가 웹 브라우저에서 Google 로그인 링크를 클릭
  2. frontend 애플리케이션에서 backend 애플리케이션에 http://localhost:8080/oauth2/authorization/google로 request 전송하고 OAuth2LoginAuthenticationFilter가 처리
  3. google의 로그인 화면을 요청하는 URI로 리다이렉트한다. 이때 Authorization server가 backend 쪽으로 Authorization code를 전송할 Redirect URI를 쿼리 파라미터로 전달
  4. google 로그인 화면 오픈
  5. Resource Owner가 Google 로그인 인증 정보를 입력해서 로그인 수행
  6. 로그인 성공하면 Backend Redirect URI로 Authorization Code를 요청
  7. Authorization server가 backend에게 Authorization code를 응답으로 전송
  8. Backend가 authorization server에게 Access Token 요청
  9. Authorization server가 backend에게 Access Token 응답으로 전송
  10. Backend가 Resource Server에게 User Info를 요청
    • User Info는 Resource Owner에 대한 이메일 주소, 프로필 정보 등
  11. Resource server가 Backend에게 User Info 응답 전송
  12. Backend는 JWT로 구성된 Access Token과 Refresh Token을 생성 후 Frontend에게 JWT를 전달하기 위해 Frontend 애플리케이션으로 Redirect
  • 6 - 11가지의 과정은 Spring Security 에서 내부적으로 처리

Frontend 애플리케이션 준비

  • 아파치 웹서버 설치
  • https://www.apachelounge.com/download 링크를 통해 아파치 웹서버 다운받고 Apache24를 C:\ 디렉터리로 이동
  • Apache24/conf/httpd.conf 파일을 메모장으로 오픈
    • ServerName localhost:80으로 주석 해제 후 변경
  • 명령 프롬프트를 열어서 C:\Apache24\bin으로 이동해서 httpd.exe -k install 명령어 실행
  • Apache24/bin/ApacheMonitor.exe를 실행하여 웹서버 실행 가능

Frontend 샘플 애플리케이션 아파치 웹서버에 배포

  • C:\Apache24\htdocs에 세 개의 HTML 파일 생성
    • body 태그에 <a href="<http://localhost:8080/oauth2/authorization/google>">Google로 로그인</a> 를 넣어 링크를 클릭하면 Backend 쪽에 request를 전송하고 구글 로그인 화면이 오픈
    • script 태그에 let accessToken = (new URL(location.href)).searchParams.get('access_token') 을 통해 토큰을 가져오고 같은 방식으로 refreshToken을 얻은 후 localStorage.setItem("accessToken", accessToken) 를 통해 localStorage에 저장한다. location.href = 'my-page.html' 을 통해 my-page로 이동한다.
    • my-page에서는 script 태그에서 let accessToken = localStorage.getItem('accessToken') 와 같이 localStorage에서 각각 토큰을 가져와 표시하는 페이지를 구성한다. 표시는 document.getElementById("accessToken").textContent = accessToken; 을 통해 Id를 지정한 곳에 표시한다.

Backend 애플리케이션에 OAuth 2 인증 기능 적용

  • JwtTokenizer 추가
    • 이전에 구현한 JwtTokenizer를 그대로 사용
  • application.yml
    • scope 값을 설정하여 해당 범위로 resource를 제한한다.
  • security: oauth2: client: registration: google: clientId: ${CLIENT_ID} clientSecret: ${CLIENT_SECRET} scope: - email - profile jwt: key: secret: ${JWT_SECRET_KEY} access-token-expiration-minutes: 30 refresh-token-expiration-minutes: 420
  • JwtVerificationFilter 추가
    • 이전에 구현한 JwtVerificationfilter를 통해 request를 전송할 때마다 Authorization header에 실어 보내는 Access Token에 대한 검증 수행
  • AuthenticationSuccessHandler 구현
    • OAuth 2 인증에 성공하면 호출되는 핸들러
    • JWT를 생성하고 Frontend 쪽으로 JWT를 전송하기 위해 Redirect하는 로직 구현
    • SimpleUrlAuthenticationSuccessHandler 를 extends하여 클래스 선언
    • JwtTokenizer, CustomAuthorityUtils, MemberService 를 필드 멤버로 선언하고 dI 받는다.
    • onAuthenticationSuccess 메서드를 오버라이딩 받아서 var oAuth2User = (OAuth2User)authentication.getPrincipal() 를 통해서 객체를 가져온다.
    • String email = String.valueOf(oAuth2User.getAttributes().get("email"))를 통해서 email을 가져온다.
    • List<String> authorities = authorityUtils.createRoles(email); 을 통해 권한을 가져온다.
    • saveMember(email)
      • member 객체를 email을 가지고 생성해 stamp를 생성해 설정하고 memberService.createMember() 메서드를 통해 생성하는 메서드를 작성
    • redirect(request, response, email, authorities)
      • redirect라는 메서드를 선언하여 request, response, username, authorities를 입력받아 delegateAccessToken, delegateRefreshToken 메서드를 각자 선언하여 각 토큰을 만들고 각 토큰을 입력받아 URI를 만드는 createURI 메서드를 선언하여 uri를 입력받아서
      • getRedirectStrategy().sendRedirect(request, response, uri) 를 통해 frontend쪽에 리디렉트한다.
    • delegateAccessToken, delegateRefreshToken 메서드는 이전에 구현한 내용과 거의 동일하다.
    • createURI 메서드
      • MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>() 객체 생성
      • queryParams.add("access_token", accessToken)를 통해 refresToken도 저장한다.
      • return으로 UriComponentBuilder를 사용한다.
        • .newInstance().scheme(”http”).host(”localhost”).port(80).path(”/receive-token.html”).queryParams(queryParams).build().toUri()를 통해 구현
  • SecurityConfiguration 설정
    • 이전에 구현한 것과 거의 흡사하다.
    • http.authorizeHttpRequests()에서
      • members 로 연결되는 URI는 OAuth 2인증을 사용하기 때문에 작성하지 않았다.
      • .oauth2Login(oauth2 -> oauth2 .successHandler(new OAuth2MemberSuccessHandler(jwtTokenizer, authorityUtils, memberService)) 를 통해 OAuth 2 인증 성공 후 실행되는 핸들러 추가
    • CustomFilterConfigurer 클래스 configure 메서드에서는 JwtVerificationFilter 객체를 생성하고 OAuth2LoginAuthenticationFilter 뒤에 실행될 수 있도록 코드 작성
      • builder.addFilterAfter(jwtVerificationFilter, OAuth2LoginAuthenticationFilter.class)
  • 기타 수정 코드
    • OAuth 2 인증 시스템을 사용하기 때문에 회원 정보를 등록하거나 수정할 필요가 없어 관련된 MemberController, MemberDto, MemberService, Member 엔티티 클래스 수정

'Backend boot camp > Session4' 카테고리의 다른 글

[Spring WebFlux] Reactor  (0) 2022.12.09
[Spring WebFlux] 리액티브 프로그래밍  (0) 2022.12.09
[Spring Security] JWT 인증  (0) 2022.12.09
[Spring Security] Spring Security 기본  (0) 2022.11.19
[인증/보안] 기초  (0) 2022.11.19