Ouath란 Open Authorization 으로 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹 사이트 상의 자신들의 정보에 대해 웹 사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는 접근 위임을 위한 개방형 표준이다.
개방형 표준 ? 누구나 자유록ㅂ게 수정할 수 있는 기술 표준. 특정 기업이나 단체에 의해 독점되지 않고
공개적으로 문서화 되어있다.
신뢰성 있는 서비스에서 로그인 하여 인증을 받고, 그 권한을 읻가 받아 세션을 생성하는 스프링 시큐리티 OAuth2 소셜 로그인 프로젝트 입니다.
OAuth2.0 서비스를 ㅈ공하고 Resource를 관리하는 서버
클라이언트는 이 서버로 하여급 인증 서버에서 발급받은 Token을 넘겨 개인 정보를 받을 수 있다.
어플리케이션을 이용하려는 Resource Server의 계정을 소유하고 있는 사용자
Resource Server API를 사용하여 정보를 가져오려는 애플리케이션 서버
Client 가 Resource Server 서비스를 사용할 수 있게 인증하고, 토큰을 발행 해주는 서버
사용자 : 서버로 ID PW 를 넘기면 Authorization Code 를 발급 받음
클라이언트 : 사용자가 발급 받은 AUthorization Code를Token
을 발급 받음
JWT 의
AcessToken
해당 토큰으로Resource Server
에 요청해서 개인정보를 받을 수 있다.
JWT
RefreshToken
해당 토큰으로AcessToken
을 재발급 받을 수 있다.
실습 예제 디렉토리 구조
실습 목표
OAuth2.0 클라이언트와 스프링 시큐리티6 프레임워크를 활용하여 신뢰할 수 있는 외부 사이트 ( 구글, 네이버 ) 부터 인증을 받고 전달 받은 유저 데이터를 활용하여 세션을 만들고, 인가를 진행하는 방법.
인증 받은 데이터는 MySQL 데이터베이스르 활용하여 저장하고 관리 한다.
구현
아래의 네모가 스프링 서버고, 위의 네모가 신뢰할 수 있는 서버 ( 구글, 네이버 ) 서비스다.
1. 처음에 사용자로부터 로그인 요청이 오면 스프링 부트 서버에서 로그인 요청을 진행하게 된다.
2. 로그인 페이지를 리디렉션하는 필터가 요청을 받아 인증 서버 ( 구글, 네이버 ) 리디렉션 창을 띄운다.
3. 로그인이 성공되면, 인증 서버에서 시큐리티 인증 필터 쪽으로 코드를 발급 해준다.
4. 발급받게된 코드를 가지고 시큐리티 인증 필터를 가지고 있고
5. 해당 코드를 다시 네이버 인증 서버로 보내게 되는데 액세스 토큰 을 발급받기 위해서다.
6. 전송을 해서, 코드와 시큐리티 키, 아이디를 모두 검증을 마치면 다시 액세스 토큰 을 시큐리티 인증 필터로 보내준다.
7. 시큐리티 인증 필터는 액세스 토큰을 가지고 서비스 리소스 서버에 액세스 토큰을 보내면, 그것에 대해서 검증을 마친 이후에 다시 로그인한 유저 데이터 를 발급 해준다.
8. 발급받은 유저 데이터를OAuth2
라는UserDetailService
가 있는데 세세션 및 DB를 저장한다.
기타
OAuth 동작 모식도
/oauth2/authorization/서비스명
클라이언트가 서버로 로그인 요청OAuth2AuthorizationRequestRedirectFilter
가 동작 /login/oauth2/code/서비스명
OAuth2LoginAuthenticationFilter
에서 해당 요청을 받게 되는데OAuth2LoginAuthenticationProvider
로 코드를 넘겨주게 된다. OUath2UserDetailService
에 OAuth2UserDetails
에 보낼 정보를 담아 넘겨준다. 기능 하는 필터에 따라 반드시 관례에 따라 주소를 기입 해야 한다.
OAuth2AuthorizationRequestRedirectFilter
처음 로그인 할때 리디렉션 하는 필터
/oauth2/authorization/서비스명
/oauth2/authorization/naver
/oauth2/authorization/google
OAuth2LoginAuthenticationFilter
로그인 성공 후 코드 및 액세스 토큰 발급해주는 필터
/login/oauth2/code/서비스명
/login/oauth2/code/naver
/login/oauth2/code/goggle
위에 나온 모식도를 전부 직접 구현 해야 할까?
변수 처리만 해주면
OAuth2.0
이 내부적으로 처리를 해준다. 전부 다 직접 구현 할 필요 없다.
변수 설정만 진행 하면
OAuth2AuthorizationRequestRedirectFilter
->OAuth2LoginAuthenticationFilter
->OAuth2LoginAuthenticationProvider
-> 과정을 추가 하지 않아도 자동으로 진행 한다. 따라서 사용자는
UserDetailService
와 UserDetails
만 구현하면 된다.
application.properties
#registration
spring.security.oauth2.client.registration.서비스명.client-name=서비스명
spring.security.oauth2.client.registration.서비스명.client-id=서비스에서 발급 받은 아이디
spring.security.oauth2.client.registration.서비스명.client-secret=서비스에서 발급 받은 비밀번호
spring.security.oauth2.client.registration.서비스명.redirect-uri=서비스에 등록한 우리쪽 로그인 성공 URI
spring.security.oauth2.client.registration.서비스명.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.서비스명.scope=리소스 서버에서 가져올 데이터 범위
#provider
spring.security.oauth2.client.provider.서비스명.authorization-uri=서비스 로그인 창 주소
spring.security.oauth2.client.provider.서비스명.token-uri=토큰 발급 서버 주소
spring.security.oauth2.client.provider.서비스명.user-info-uri=사용자 정보 획득 주소
spring.security.oauth2.client.provider.서비스명.user-name-attribute=응답 데이터 변수
예시
#registration
spring.security.oauth2.client.registration.naver.client-name=naver
spring.security.oauth2.client.registration.naver.client-id=발급아이디
spring.security.oauth2.client.registration.naver.client-secret=발급비밀번호
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/login/oauth2/code/naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email
#provider
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response
```
**registration 과 provider**
>registration 은 외부 서비스에서 우리 서비스를 특정하기 위해 등록하는 정보여서 등록이 필수적이다.
>하지만 provider의 경우 서비스별로 정해진 값이 존재하며 OAuth2 클라이언트 의존성이 유명한 서비스의 경우 내부적으로 데이터를 이미 가지고 있다. (구글, 깃허브, 페이스북 등)
>*네이버 등은 등록을 해주어야 한다는 말*
***
# 4.SecurityConfig 등록
```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf((csrf) -> csrf.disable())
.formLogin((login) -> login.disable())
.httpBasic((basic) -> basic.disable())
.oauth2Login((oauth2)->oauth2
.userInfoEndPoint(
userInfoEndPointConfig ->
userInfoEndpointConifg.userService(customOAuth2UserService)
))
//oauth2Client를 추가 하게 되면 세부 로그인 로직들을 구현 해야 한다.
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/").permitAll()
.anyRequest().authenticated());
return http.build();
}
}
http://localhost:8080/login
에 접속
UserDetailService
와 UserDetail
을 구현해놓지 않아서 이 다음 진행은 불가하다.
UserDetailService
와 UserDetail
을 구현하지 않아서 이 다음의 진행은 불가하다.
#registration
spring.security.oauth2.client.registration.google.client-name=google
spring.security.oauth2.client.registration.google.client-id=
spring.security.oauth2.client.registration.google.client-secret=
spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google
spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.google.scope=profile,email
CustomOAuth2UserService
@Service
public CustomOAuth2UserService extends DefaultOAuth2UserService{
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{
// OAuth2 제공 업체로부터 사용자 정보를 얻어오는 과정
OAuth2USer oAuth2User = super.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response =null;
if(registrationId.equals("naver")){{
oAuth2Response = new NaverResponse(oAuth2User.getAttribute());
}
else if ( registrationId.equals("google")){
oAuth2Response =
new GoogleResponse(oAuth2User.getAttribute());
}else{
return null;
}
return super.loadUser(userRequest);
}
}
}
public interface OAuth2Response{
String getProvider();
String getProviderId();
String getEmail();
String getName();
}
public class GoogleResponse implements OAuth2Response{
private final Map<String, Object> attribute;
public GoogleResponse(Map<String,Object>attribute){
this.attribute=attribute;
}
@Override
public String getProvider(){
return "google";
}
@Override
public String getEmail(){
return attribute.get("email").toString();
}
@Override
public String getName(){
return attribute.get("name").toString();
}
}
public class NaverResponse implemnets OAuth2Response{
private final Map<String,Object> attribute;
public NaverResponse(Map<String,Object> attribute){
this.attribute=attribute;
}
@Override
public String getProviderId() {
return attribute.get("id").toString();
}
@Override
public String getEmail() {
return attribute.get("email").toString();
}
@Override
public String getName() {
return attribute.get("name").toString();
}
}
CustomOAuth2User
@RequiredArgsConstructor
public class CustomOAuth2User implements OAuth2User {
private final OAuth2Response oAuth2Response;
private final String role;
@Override
public <A> A getAttribute(String name) {
return OAuth2User.super.getAttribute(name);
}
@Override
public Map<String, Object> getAttributes() {
return null;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return role;
}
});
return collection;
}
@Override
public String getName() {
return oAuth2Response.getName();
}
public String username() {
return oAuth2Response.getProvider() + " " + oAuth2Response.getProviderId();
}
}
권한을 가져오는 부분은
GrantedAuthority
를 컬렉션 리스트로 만들어서 add하면 되는데
GrantedAuthority
를 새로 초기화 하면서Override
해줘야 한다. return 값은role
로 준다
username
메서드는 OAuth2User
인터페이스에서 기본 제공 하는 메서드가 아닌데 이유는 프로바이더와 프로바이더 아이디를 구분하기 위함이다. 가령 네이버 + 아이디, 구글 + 아이디 이런 식으로.
어떤 사람이 로그인 했는지, 저장하기 위해 DB 사용이 필수적이다.
모식도를 간단히 살펴 보면 리디렉션 후 로그인에 성공하면 로그인 필터를 거쳐
OAuth2LoginAuthenticationProvider
에 도달 하면 다시 인증 서버에 먼저 발급받은 코드 및 등록 정보를 전달하고 인증 서버는 엑세스 토큰을 발급 해준다. 이 액세스 토큰을 리소스 서버에 전달 하면 리소스 서버는 유저 정보를 전달 해주는데OAuth2UserDetails
에 담아OAuth2UserDetailService
에 전달한다. 이제 이 서비스에서 세션 저장과 같은 나머지 시큐리티 로직이 동작한다.
이제 세션,DB 저장 로직을 구현해볼 차례이다.
먼저 소셜 로그인 서비스에서 보내준 Provider를 가지고 이미 DB에 저장된 회원인지 조회를 해야 한다.
DB 저장을 위해 유저 래퍼지 토리, 유저 엔티티, 커스텀 오쓰 유저서비스 단 하단에 저장하는 로직 추가가 필요하다.
유저 레퍼지토리와, 유저 엔티티는 생략
CustomOAuth2UserService
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService{
// 타 블로그 글을 보면 OAuth2UserService를 상속 받거나 직접 구현 하는 경우가 있는데
// DefaultOAuth2UserService는 구현체이기에 이대로 진행 해도무관하다
//DB 저장을 진행 하기 위해 유저 래퍼지토리 주입
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
//부모 클래스 loadUser로 부터 유저 정보를 가지고 오는 메서드 ( OAuth2 공급업체로 부터 사용자 정보를 가져오는 것 ) OAuth2User oAuth2User = super.loadUser(userRequest);
System.out.println(oAuth2User.getAttributes());
String registrationId = userRequest.getClientRegistration().getRegistrationId();
OAuth2Response oAuth2Response = null;
if (registrationId.equals("naver")) {
oAuth2Response = new NaverResponse(oAuth2User.getAttributes());
} else if (registrationId.equals("google")) {
oAuth2Response = new GoggleResponse(oAuth2User.getAttributes());
}else{
return null;
}
// 구글과 네이버 서비스마다 인증 규격이 상이하기 때문에 서로 다른 DTO로 담아야 한다.
// 따라서 OAuth2 DTO 객체 격인 OAuth2Response 객체를 인터페이스로 만든다.
// 네이버로 인터페이스를 구현, 구글 타입으로 인터페이스를 구현하는 식으로 진행한다.
String username = oAuth2Response.getProvider()+ " "+oAuth2Response.getProviderId();
UserEntity existData = userRepository.findByUsername(username);
String role =null;
if(existData ==null){
UserEntity userEntity = new UserEntity();
userEntity.setUsername(username);
userEntity.setRole(oAuth2Response.getEmail());
userEntity.setEmail("ROLE_USER");
userRepository.save(userEneity);
}else{
role = existData.getRole();
existData.setEmail(oAuth2Response.getEmail());
userRepository.save(existData);
}
reutrn new CustomerOAuth2User(oAuth2Response, role);
}
}
인증 코드 발급, 액세스 코드 발급, JWT에 비해 JWTUtil이나 TokenUtils 등을 구현하지 않고 변수 설정만 해주면 알아서 데이터를 넘겨 주고 받아오는데 생소한 메서드 들이 많아 더 연습이 필요해 보인다.. 서버사이드 렌더링을 써서 세션 기반을 썼지만 리액트를 쓸 경우 클라이언트 사이드 렌더링을 쓸 경우 토큰 기반의 로그인 방법도 추가 학습이 필요 해보인다.