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 컴포넌트 상호작용 흐름
- Resource Owner는 Client에게 OAuth2 인증을 요청합니다.
- Client는 Resource Owner가 Resource Owner의 계정 정보를 관리하고 있는 서드 파티 애플리케이션에 로그인하도록 서드 파티 애플리케이션 로그인 페이지로 redirect한다.
- Resource Owner는 로그인 인증 시도
- 인증 성공하면 Authorization Server가 Owner의 인증 성공을 증명하는 Access Token을 Client에게 전송
- Access Token을 받은 Client는 Resource Server에게 Owner 소유의 Resource 요청
- 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 유형
- 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에게 전달
- 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를 전달
- 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를 전달
- 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 2 샘플 애플리케이션 구현
의존성 추가
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())
- .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 인증 흐름
- Resource Owner가 웹 브라우저에서 Google 로그인 링크를 클릭
- frontend 애플리케이션에서 backend 애플리케이션에 http://localhost:8080/oauth2/authorization/google로 request 전송하고 OAuth2LoginAuthenticationFilter가 처리
- google의 로그인 화면을 요청하는 URI로 리다이렉트한다. 이때 Authorization server가 backend 쪽으로 Authorization code를 전송할 Redirect URI를 쿼리 파라미터로 전달
- google 로그인 화면 오픈
- Resource Owner가 Google 로그인 인증 정보를 입력해서 로그인 수행
- 로그인 성공하면 Backend Redirect URI로 Authorization Code를 요청
- Authorization server가 backend에게 Authorization code를 응답으로 전송
- Backend가 authorization server에게 Access Token 요청
- Authorization server가 backend에게 Access Token 응답으로 전송
- Backend가 Resource Server에게 User Info를 요청
- User Info는 Resource Owner에 대한 이메일 주소, 프로필 정보 등
- Resource server가 Backend에게 User Info 응답 전송
- 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 엔티티 클래스 수정