몇주째 하고 있는데 ^^ 즐거워요.. ^^
한 번 정복하고 나면 로그인구현이 즐거워진다는 OAuth-Client2!!
OAuth-Client2의 동작 원리와 사용하기 위한 방법들에 대해서 배워보자!
OAuth2는
1. 코드를 발급 받고..
2. 코드를 이용해서 토큰을 발급받고..
3. 토큰을 이용해서 사용자 정보를 받아오는 과정을
단순하게 만들어준다!!
즉, OAuth 서버에서 사용자 정보를 가져와준다
① OAuth 로그인을 사용할 수 있도록 OAuth 로그인을 활성화한다.
② OAuth2 로그인에 쓰이는 클라이언트 정보를 등록해야한다.
이 두번째를 담당해주는 객체가 바로 OAuth2ClientAutoConfiguration
!!
OAuth2ClientAutoConfiguration
이란??
- OAuth2ClientAutoConfiguration은 Spring Boot에서 OAuth 2.0 클라이언트와 관련된 설정을 자동으로 구성해준다.
- OAuth 2.0 클라이언트 정보를 기반으로 클라이언트를 등록하고, 토큰 관리 및 보안 설정을 자동으로 구성한다.
- 이를 통해 개발자는 별도의 복잡한 설정 없이도 OAuth 2.0 클라이언트 기능을 손쉽게 사용할 수 있습니다.
HttpSecurity 설정을 통해서 oauth2Login
을 활성화한다.
👉 이렇게 하면 ClientRegistrationRepository
빈을 등록해서 직접 클라이언트 정보를 등록해야한다.
① Java Config 클래스를 이용해 ClientRegistrationRepository
을 빈으로 등록해 동적으로 클라이언트 정보를 등록할 수 있다.
이외에도 ② application.yml 또는 application.properties 파일을 이용방법, ③ 환경 변수를 이용한 설정 방법이 있다.
나는 위에서 HttpSecurity 설정을 이용했기 때문에 첫번째 방법만 사용할 수 있다.
- clientId
- clientSecret
- redirectUri
- 인증 페이지로 리디렉션하기 위한 URL
- 액세스 토큰을 받을 수 있는 URL을 설정
- 사용자 이름(또는 ID)으로 사용할 속성의 이름을 설정
- 클라이언트의 이름 설정
어떤 방법을 이용하던 위의 정보를 모두 제공해야한다.
① 사용자 정보를 가져온다.
② 가져온 사용자 정보를 이용해서 후처리를 한다.
시큐리티 OAuth2을 이용하면 OAuth 서버에서 사용자 정보를 받아왔다고 해보자
① 사용자 정보를 리턴 받는다면 그 위치는 어디일까?
② 사용자 정보는 어떤 객체에 담겨올까?
③ 회원가입을 진행하고 세션에 넣을 객체가 필요하다!!
① 사용자 정보를 리턴 받는다면 그 위치는 DefaultOAuth2UserService 클래스의 loadUser 메서드
② 사용자 정보는 OAuth2UserRequest 타입에 담겨온다.
③ OAuth2 로그인을 하면 loadUser 메서드가 호출되기 때문에 이 메서드에서 회원가입을 진행하고, OAuth2User 객체를 반환하면 이 객체가 Authentication에 담긴다.
public OAuth2User loadUser(OAuth2UserRequest userRequest)
throws OAuth2AuthenticationException {
...
return "Authentication에 담길 OAuth2User타입"
}
Authentication
의 내부 객체로 올 수 있는 건 UserDetails 타입이라고 했잖아요..? @_@사실 아직은 배우지 않았지만
Authentication
의 내부 객체로는
1. 폼 로그인을 통해 생성되는UsernamePasswordAuthenticationToken
2. OAuth2 인증을 통해 생성된 인증 토큰OAuth2AuthenticationToken
3. JWT (JSON Web Token)를 사용하는 인증 토큰JwtAuthenticationToken
이다.
UserDetails를 이용해
UsernamePasswordAuthenticationToken`를 생성하는 과정이 있었던 것...!!하여튼
OAuth2User
도Authentication
의 내부 객체로 들어갈 수 있다.
① OAuth 로그인 활성화 하기
② 클라이언트 정보 제공하기
③ 전달받은 유저정보를 가지고 회원가입과 로그인 세션을 위한 OAuth2User
을 제공
을 해야한다.
MySecurityConfig 파일에서 SecurityFilterChain반환 메서드에서 설정
package com.jsh.securitystudy.config;
@Configuration
@EnableWebSecurity
public class MySecurityConfig {
private final PrincipalOauth2UserService principalOauth2UserService;
public MySecurityConfig(@Lazy PrincipalOauth2UserService principalOauth2UserService) {
this.principalOauth2UserService = principalOauth2UserService;
}
@Bean //빈으로 등록하기
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeRequests((authorizeRequests) -> authorizeRequests
.requestMatchers("/user/**").authenticated()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER"))
.formLogin(formLogin -> formLogin
.loginPage("/loginForm")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/"))
//설정 시작------------------------
.oauth2Login(oauth2Login -> oauth2Login
.loginPage("/loginForm")
.defaultSuccessUrl("/")
.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
.userService(principalOauth2UserService)));
//설정 완료------------------------
return http.build();
}
}
.oauth2Login(oauth2Login -> oauth2Login
: OAuth2 로그인 설정을 시작합니다..loginPage("/loginForm")
: 사용자 정의 로그인 페이지 URL을 지정합니다..defaultSuccessUrl("/")
: 로그인 성공 후 리디렉션될 기본 URL을 지정합니다. .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
: 사용자 정보 엔드포인트 설정을 시작합니다. 이는 OAuth2 제공자로부터 사용자 정보를 가져오는 데 사용한다. .userService(principalOauth2UserService)));
: 사용자 정보를 가져오기 위해 사용할 OAuth2UserService
를 지정합니다. 이 서비스 안에 loadUser
메서드가 있는 것이다. MySecurityConfig 파일에서 ClientRegistrationRepositor
타입 빈을 등록한다.
package com.jsh.securitystudy.config;
@Configuration
@EnableWebSecurity
public class MySecurityConfig {
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
ClientRegistration googleClientRegistration = ClientRegistration.withRegistrationId("google")
.clientId("760459109498-1dpf66s2nefpk02uvg07479hnakltacs.apps.googleusercontent.com")
.clientSecret("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")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("http://localhost:8000/security/login/oauth2/code/google") //"http://localhost:8000/security/login/oauth2/code/google"
.scope("profile", "email")
.authorizationUri("https://accounts.google.com/o/oauth2/auth")
.tokenUri("https://oauth2.googleapis.com/token")
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
.userNameAttributeName("sub")
.clientName("Google")
.build();
return new InMemoryClientRegistrationRepository(googleClientRegistration);
}
}
ClientRegistration.withRegistrationId("google")
ClientRegistration 객체를 생성하기 위한 withRegistrationId("google") 메서드를 호출, 여기서 registrationId는 이 클라이언트 등록 정보의 고유 식별자입니다. 보통 OAuth2 로그인 시 사용자가 선택할 수 있도록 식별하는 데 사용됩니다.
.clientId("your-client-id")
: 구글 OAuth2 클라이언트의 클라이언트 ID를 설정
.clientSecret("your-client-secret")
: 구글 OAuth2 클라이언트의 비밀번호 설정
.redirectUri("http://localhost:8000/security/login/oauth2/code/google")
: OAuth2 인증이 완료된 후 사용자가 리디렉션될 URI 설정
.authorizationUri("https://accounts.google.com/o/oauth2/auth")
" 로그인창을 띄우는 페이지 주소 설정
.tokenUri("https://oauth2.googleapis.com/token")
인증 코드 교환을 통해 액세스 토큰을 받을 수 있는 URL을 설정
.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
액세스 토큰을 사용하여 사용자 정보를 가져올 수 있는 URL을 설정
.userNameAttributeName("sub")
:
사용자 정보 응답에서 사용자 이름(또는 ID)으로 사용할 속성의 이름을 설정합니다. 구글의 경우, 일반적으로 sub
.clientName("Google")
: 클라이언트의 이름을 설정
.build();
: 빌드!
return new InMemoryClientRegistrationRepository(googleClientRegistration);
: 빈으로 등록하는 부분~!
위 OAuth 설정에서 보았듯이 PrincipalOauth2UserService
을 설정했다.
PrincipalOauth2UserService
는 DefaultOAuth2UserService
을 상속받는 클래스이고, 이 내부에 있는 loadUser
메서드가 OAuth2로그인 후에 사용자 유저정보를 넘겨받고 호출된다.
로그인을 하기 위해서는 OAuth2User
타입의 객체를 리턴해야한다.
PrincipalDetails
수정⭐⭐이게 무슨말이냐면 스프링 시큐리티를 사용해서 세션을 가지고 로그인을 진행하면
@AuthenticationPrincipal
어노테이션을 통해 현재 로그인한 사용자의 정보를 넘겨받을 수 있단말이에요??그런데 폼 로그인을 하면!
Authentication
의 내부 객체로 UserDetails를 사용하고.. 구글 로그인을 하면 내부 객체로 OAuth2User 타입을 가지고 있어서
결국 상황마다 전달받는 타입이 다르다.. 😨😨😨
따라서 UserDetails과 OAuth2User를 한 번에 다룰 수 있도록 PrincipalDetails
수정이 필요하다.
① 구글로그인을 통해서 세션을 넣으려면 OAuth2User
타입의 객체를 리턴해야한다.
② 그러나 폼로그인과, 구글 로그인 모두를 지원하는 나의 프로젝트에서는 통일된 타입이 필요하다.
③ 따라서 UserDetails
과 OAuth2User
다중상속한 PrincipalDetails
을 리턴한다.
④ 이후 @AuthenticationPrincipal
어노테이션을 통해 현재 로그인한 사용자의 정보를 넘겨받을 때 PrincipalDetails
타입으로 전달받아 어떤 로그인을 구현했던 동일하게 사용할 수 있도록 한다.
package com.jsh.securitystudy.config.auth;
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {
// 다중 상속
private User user;
private Map<String, Object> attrilbutes;
//일반 로그인
public PrincipalDetails(User user) {
this.user = user;
}
//OAuth 로그인 사용
public PrincipalDetails(User user,Map<String, Object> attrilbutes ) {
this.user = user;
this.attrilbutes = attrilbutes;
}
//OAuth2User 확장으로 인해 추가한 메서드
@Override
public Map<String, Object> getAttributes() {
return attrilbutes;
}
}
@GetMapping("/user")
public @ResponseBody String user(@AuthenticationPrincipal PrincipalDetails principalDetails) {
return "principalDetails.getUser() = " + principalDetails.getUser();
}
① 일반 폼 로그인
② 구글 폼 로그인
userRequest
① userRequest 를 통해 OAuth2User를 만들어준다.
② OAuth2User를 통해 프로젝트에서 쓰는 회원정보인 USer 객체를 만들어준다.
이때 어떤 서버를 사용했는지는 userRequest.getClientRegistration().getRegistrationId()
를 사용한다.
③ 해당 회원정보가 이미 존재하는지 검사하고 없다면 회원가입 시킨다.
package com.jsh.securitystudy.config.oauth;
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
@Autowired
private UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public PrincipalOauth2UserService(@Lazy BCryptPasswordEncoder bCryptPasswordEncoder) {
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
@Override // 구글로부터 받은 UserRequest 데이터에 대한 후처리되는 함수
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
//① userRequest 를 통해 OAuth2User를 만들어준다.
OAuth2User oAuth2User = super.loadUser(userRequest);
System.out.println("oAuth2User.getAttributes() = " + oAuth2User.getAttributes());
//이 정보를 사용해 회원 가입을 진행한다.
// [회원가입 진행]
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerId = oAuth2User.getAttribute("sub"); //
String username = oAuth2User.getAttribute("name") + "_" + providerId; //
String email = oAuth2User.getAttribute("email"); //
String password = bCryptPasswordEncoder.encode("비밀번호");
String role = "ROLE_USER";
//이미 존재하는 회원인지 검사
User userEntity = userRepository.findByUsername(username);
if (userEntity == null) {
//회원가입 진행하기
userEntity = User.builder()
.username(username)
.password(password)
.email(email)
.role(role)
.provider(provider)
.providerId(providerId)
.build();
userRepository.save(userEntity);
}
return new PrincipalDetails(userEntity,oAuth2User.getAttributes());
}
}
로그인은 세션에 넣는 것이다.
따라서 OAuth2 로그인 시 넣을 수 있는 OAuth2User 타입 객체를 리턴하면 정상로그인!!
return new PrincipalDetails(userEntity,oAuth2User.getAttributes());
폼 로그인, 구글로그인 모두 정상적으로 회원가입 완료!!
폼 로그인, 구글로그인 모두 정상적으로 현재 세선 정보 반환
💦💦 진짜 오래걸리구... 하고 나니까 너무 간단해서 웃기다..^^
글이 미친듯이 길어졌지만 누구라도 잘 이해ㅐ할 수 있도록 정말 자세히 써봤다.. ^^
미래의 나야 언제든지 구현해조.. ㅜㅜ
깃허브 주소 : https://github.com/jinseohyun1228/Spring_Security_Study