OAuth는 프로토콜로 규약되어 있으며, 그와 관련된 자세한 내용은 아래에서 확인해볼 수 있습니다.
참고
OAuth("Open Authorization")는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다.
회원가입 & 로그인 로직을 구현할 때, 외부 플랫폼으로부터 리소스를 가져와서 해당 리소스를 이용하여 특정 기능을 수행하고 싶을 때 사용한다.
OAuth 인증을 통하면 신뢰할 수 있는 플랫폼이 인증과 리소스에 대한 권한을 외부 플랫폼에 부여하므로서 사용자는 추가적으로 개인정보를 입력하지 않고도 이전에 입력된 정보를 활용할 수 있다는 사용자 경험적 장점도 존재한다.
Authroiztion Code 방식
Client
가 Resource Owner
에게 특정 자원을 얻기 위한 권한을 얻기 위한 Request(간접적으로도 가능)
Resource Owner
는 Client
에게 자원을 가져올 권한을 부여한다. 이 과정에서 credential한 authorization grant
를 Client
가 수신한다.
Client
는 Authorization Server
에게 Access token
을 요청한다. 이 때, Request 메세지에 2번 과정에서 받은 Authorization grant가 포함된다.
Authorization Server
는 Client
에게 받은 Authorization grant
가 정당하다면 Access token
을 발행하여 전달한다.
Client
는 Access token
을 이용하여 Resource Server
에게 자원을 요청한다.
Resource Server
는 Access token
이 정당하다면, 요청된 자원을 전달한다.
client
와 resource owner
사이에서 중계를 하는 authorization server
를 이용하여 Authorization Grant
와 Access token
이 교환되는 방식.Access token
이 간접적으로 Client
에게 전달되기 때문에 다른 3가지 방식보다 보안 측면에서 좋다.Authorization code
없이 바로 access token
을 발급하여 사용하는 방식Authorization code validation
이 포함되지 xClient
에 Service provider(ex. 구글, 네이버, ...)의 ID와 PW를 저장해두고 사용하는 방식Client
와 Service Provider의 관계가 매우 밀접할 때, 사용하는 것이 권장됨Client
= Resource Owner
일 때 사용하는 인증 방식Authorization Request/Grant
과정이 불필요Authorization Server
로부터 access token
발급아래부터는 Authorization Code방식의 Authorization Grant에 대한 내용만 기술한다.
(추가 예정)
일반적으로 웹 애플리케이션은 다음과 같이 OAuth 2.0 프로토콜을 구현한다.
초기.
client-id
, client-secret
, redirect_url
client-id
, client-secret
1-1. 인증 요청
Client
는 사용자에게 OAuth 2.0 로그인을 지원하는 폼을 제공client
자신에게로 리다이렉트하는 url를 포함client-id
, 정보의 범위인 scope
가 query parameter에 포함https://server.authorization/?client-id=1&scope=email,name&redirect_url=https://client/callback
1-2. Resource Owner는 리다이렉트된 url로 접속하여 로그인
client-id, scope, redirect_url
등을 검증Authorization Code
를 발행하여 Resource Owner
와 Resource Server
에 임시저장Authorization Code
는 임시 비밀번호의 역할을 한다2-1. Resource Server는 발급된 Authorization Code를 Client에게 논리적으로 전달
client
에 대한redirect_url
을 Resource Owner
에게 전달2-2. Resource Owner는 리다이렉트 주소로 접속
Authorization Code
의 정보를 알게됨.3-1. Client가 Access token 발급을 위해 Authorization Server에게 Request
Authorization Code
, client-id
, client-secret
, redirect_url
4-1. Authorization Server가 Request에 대한 검증과정 수행
client-id
, client-secret
, Redirect-url
비교client-secret
은 절대 노출되선 안된다. Access token
발행 후 Authorization Code
폐기Authorization Code
를 폐기하는 이유는 Replay 공격을 막기 위함5-1. 이 후, 요청되는 자원들에 대해서는 Access token을 통해 인증을 수행할 수 있음
https://apis/v1/cal?access_token={}
Authorization: Bearer <access_token>
값 지정OAuth2.0 을 이용하기 위한 OAuth2.0 Client-ID 발행
application-oauth.yaml
파일 생성 후, 관련 property 정의OAuth2ClientProperties
라는 객체 생성 후 Bean 등록OAuth2ClientPropertiesRegistrationAdapter
를 통해 각 서버마다 ClientRegistration
생성client-id, client-secret
만 지정해주면 됨redirect-url
, authorization_grant
, scope
, client_name
등의 필드 값들도 지정해주어야함.application.yaml에 oauth 관련 profile 등록
spring:
(생략)
profiles:
include: oauth # application-oauth.yaml 관련 설정 사용 위함
* spring-boot에서는 `application-xxx.yaml` 로 profile을 지정할 경우, `xxx`만으로 값들을 가져올 수 있다.
# build.gradle
dependencies {
...
implementation('org.springframework.boot:spring-boot-starter-oauth2-client')
}
package com.fakedevelopers.bidderbidder.model.user;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import javax.validation.constraints.Email;
@Getter
@NoArgsConstructor
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Email
@Column(nullable = false)
private String email;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role; // TODO : OAuth2을 이용하여 Role별로 접근 권한 설정
@Builder
public User(String name, String email, Role role) {
this.name = name;
this.email = email;
this.role = role;
}
public User update(String name) {
// 추후 사용자의 요구에 따라 이름 변경 가능.
this.name = name;
return this;
}
public String getRoleKey() {
return this.role.getKey();
}
}
package com.fakedevelopers.bidderbidder.model.user;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
/* Spring Security는 Role 별로 권한 설정 가능 */
public enum Role {
CUSTOMER("ROLE_CUSTOMER", "구매자"),
SELLER("ROLE_SELLER", "판매자"),
ADMIN("ROLE_ADMIN","관리자"),
GUEST("ROLE_GUEST", "비회원");
private final String key;
private final String title;
}
package com.fakedevelopers.bidderbidder.config.oauth;
import com.fakedevelopers.bidderbidder.model.user.Role;
import lombok.RequiredArgsConstructor;
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;
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
// 필터링 규칙
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests() // URL 별 권한 관리를 설정하는 옵션의 시작점
.antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**").permitAll()
// .antMatchers("권한이 필요한 페이지").hasRole(Role.CUSTOMER.name()) { TODO }
// .anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()// 로그인 성공 후 사용자 정보 가져올 때 설정
.userService(customOAuth2UserService); // 이 후 호출할 Service 루틴
}
}
CustomOAuth2UserService
인터페이스에 맞게 서비스 로직 작성package com.fakedevelopers.bidderbidder.config.oauth;
import com.fakedevelopers.bidderbidder.config.oauth.dto.OAuthAttributes;
import com.fakedevelopers.bidderbidder.config.oauth.dto.SessionUser;
import com.fakedevelopers.bidderbidder.model.user.User;
import com.fakedevelopers.bidderbidder.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
import java.util.Collections;
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
// 현재 진행중인 서비스를 구분하기 위해 String으로 값을 가져옴 ex.) Naver, Google ....
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// OAuth2 로그인 성공시, 키 값이 된다. 인증 서버(ex. 네이버, 구글)에 따라 키 이름이 달라질 수 있으므로 변수로 가져옴
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// OAuth 로그인을 통해 가져온 OAuth2User의 attribute를 담아주는 of 메소드 (Dto로 취급)
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
// 사용자의 외부 프로필에 업데이트가 있을 경우, 어플리케이션에도 반영
// 만약, 새로운 사용자라면 DB에 저장
User user = saveOrUpdate(attributes);
// 사용자 정보를 Session에 저장.
// Session User는 Serializable한 Dto임
httpSession.setAttribute("user", new SessionUser(user));
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey()
);
}
private User saveOrUpdate(OAuthAttributes attributes) {
User user = userRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
OAuth2UserService<OAuth2UserRequest, OAuth2User>
스펙에 맞게 작성OAuth2UserRequest
에는 RegistrationID, accessToken, parameter
가 포함OAuth2User
에는 email, name, ...,
등의 사용자 정보가 포함OAuth2User
는 DefaultOAuth2UserService
의 loadUser(OAuth2UserRequest)
로 가져올 수 있음.OAuth2User
과 OAuth2UserRequest
를 통해 OAuth에 종속적이지 않은 dto(User
)를 정의할 수 있음saveOrUpdate
정의/oauth2/authorization/{RegistrationID}
이다.