Google API Console 접속
1. 새 프로젝트 등록하여 선택
- 프로젝트 이름 : springboot-oauth-google
2. 좌측 메뉴 - OAuth 동의 화면
1) 외부에 체크하고 만들기
2) 만들기 세부 설정 후 저장
- 앱 이름 : springboot-oauth
- 문의 email 2개에 입력
3. 좌측 메뉴 - 사용자 인증 정보
1) 사용자 인증 정보 만들기 - OAuth 클라이언트 ID
2) 세부 설정 후 저장
- 애플리케이션 유형 : 웹 애플리케이션
- 이름 : springboot-oauth
- 승인된 리디렉션 URI 추가 : http://localhost:8080/login/oauth2/code/google
tip) oauth 라이브러리를 사용하면 login/oauth2/code/google 는 라이브러리에서 사용하는 고정 주소여서 따르 컨트롤러에 맵핑하지 않아 도 된다.
3) 저장하면 나오는 클라이언트ID, 클라이언트 보안 비밀번호는 따로 관리 해야 함
4. 스프링 시큐리티 OAuth-client 라이브러리 사용
- 스프링부트 프로젝트 만들 때 OAuth2 Client 체크 해서 만듬 ( pom.xml에서 해당 dependency 복사해서 사용)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
- application.yml에 OAuth google 설정 추가
security:
oauth2:
client:
registration:
google: # /oauth2/authorization/google 이 주소를 동작하게 한다.
client-id: 구글 API 콘솔에서 프로젝트의 클라이언트ID를 찾아 입력한다.
client-secret: 구글 API 콘솔에서 프로젝트의 클라이언트 보안 비밀번호를 찾아 입력한다.
scope: # scope("openid", "profile", "email", "address", "phone")
- email
- profile
5. 구글로그인페이지를 띄운다.
1) loginForm.html에 버튼 추가
<a href="/oauth2/authorization/google">Google</a>
2) seurityconfig에 oauth2 설정 추가
.and()
.oauth2Login() // OAuth2기반의 로그인인 경우
.loginPage("/loginForm") // 인증이 필요한 URL에 접근하면 /loginForm으로 이동
6. 일반로그인, OAuth2 로그인 2가지를 한 곳에서 후처리하기 위해 PrincipalDetails에서 UserDetails, OAuth2User를 implements
package com.jig.security1.config.auth;
import com.jig.security1.model.User;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
/**
* [로그인 처리 과정에서 사용되는 UserDetails]
*
* - Security Session -> Authentication -> UserDetails
* - 시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시킨다.
* - 로그인 진행이 완료되면 시큐리티 session을 만든다. (Security ContextHolder에 session정보를 저장한다.)
* - Authentication안에 user정보를 담는다. User 오브젝트는 UserDetails 타입이여야 한다.
* - UserDetails를 구현하여 사용하는데, user Entity를 컴포지션(변수로 사용)하고 각 메서드를 overrride 한다.
*/
/**
* [일반로그인, OAuth2 로그인 2가지를 한 곳에서 후처리하기 위한 방법]
*
* - 스프링 시큐리티에 session을 관리하는 security session이 있다.
* - 로그인 하면 security session에 Authentication이 들어간다.
* - Authentication은 각 메서드에서 DI해서 사용할 수 있다.
* - Authentication에는 UserDetails 타입, OAuth2User 타입이 들어갈 수 있다. 일반 로그인 한 경우에는 UserDetails, OAuth2로그인한 경우는 OAuth2User
* - 로그인 후처리를 위해 UserDetails 타입, OAuth2User 타입 2가지를 한 클래스에서 implements 해서 사용하면 된다.
*
* ===> public class PrincipalDetails implements UserDetails, OAuths2User{}
*/
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {
private User user;
private Map<String, Object> attributes;
/**
* 일반 로그인할 때 사용되는 생성자
* @param user
*/
public PrincipalDetails(User user) {
this.user = user;
}
/**
* OAuth 로그인할 때 사용되는 생성자
* @param user
* @param attributes
*/
public PrincipalDetails(User user, Map<String, Object> attributes) {
this.user = user;
this.attributes = attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// user.getRole() 을 타입에 맞게 만들어준다.
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return user.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
/**
* 계정이 만료된게 아닌지 ( true : 만료 안 됨)
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 계정이 잠긴게 아닌지 ( true : 안 잠김)
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 패스워드가 만료된게 아닌지 ( true : 만료 안 됨)
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 계정이 활성화 됐는지 (true : 활성화)
* @return
*/
@Override
public boolean isEnabled() {
/**
* 사용 예시
* 1년동안 로그인 안한 회원을 휴면계정으로 처리하기로 함
* 1. User객체에 '로그인 시간' 변수를 둠. ( user.getLoginTime() )
* 2. '현재시간 - 로그인시간' => 1년을 초과하면 return false;
*/
return true;
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public String getName() {
return null;
}
}
package com.jig.security1.config.oauth;
import com.jig.security1.config.auth.PrincipalDetails;
import com.jig.security1.model.User;
import com.jig.security1.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private UserRepository userRepository;
/**
* BCryptPasswordEncoder-loadUser, PrincipleService-loadUserByUsername
* 가 없어도 내부적으로 동작하는데, PrincipalDetails반환해주기 위해 overrride 함.
*
* [구글로부터 받은 userRequest 데이터에 대한 후처리를 하는 함수]
*
* 구글 로그인 동작 흐름
* 1. 구글 로그인 버튼 클릭
* 2. 구글 로그인 창
* 3. 로그인 완료
* 4. code를 반환 받는데, OAuth2 Client 라이브러리가 code를 이용해서 AccessToken을 요청
* 5. AccessToken과 userRequest 정보를 반환받는다. loadUser 함수에서 받환 받는다. ( userRequest에는 회원프로필 정보가 있다.)
*
* @param userRequest
* @return OAuth2User
* @throws OAuth2AuthenticationException
*/
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
/*
System.out.println("userRequest = " + userRequest);
System.out.println("userRequest.getClientRegistration() = " + userRequest.getClientRegistration());
System.out.println("userRequest.getAccessToken() = " + userRequest.getAccessToken());
System.out.println("userRequest.getAccessToken().getTokenValue() = " + userRequest.getAccessToken().getTokenValue());
System.out.println("super.loadUser(userRequest).getAttributes() = " + super.loadUser(userRequest).getAttributes());
*/
/*
[회원가입 정보]
username = "google_" + super.loadUser(userRequest).getAttributes().get("sub")
password = 암호화(겟인데어)
email = super.loadUser(userRequest).getAttributes().get("email")
role = ROLE_USER
provider = userRequest.getClientRegistration().get("registrationId")
providerId = super.loadUser(userRequest).getAttributes().get("sub")
*/
// 회원등록이 안됐으면 회원가입 진행
OAuth2User oAuth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerId = oAuth2User.getAttribute("sub");
String username = provider + "_" + providerId;
// TODO: OAuth2 회원가입인 경우 패스워드에 특정값을 넣어줘서 노출되는 경우 모든 OAuth 회원이 노출되게 되는 문제가 있음. 해결 필요함
String password = bCryptPasswordEncoder.encode("특정값");
String email = oAuth2User.getAttribute("email");
String role = "ROLE_USER";
User userEntity = userRepository.findByUsername(username);
if (null == userEntity) {
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());
}
}
8. 구글 로그인 후처리를 어디서 할 지 SecurityConfig에서 설정
/**
* OAuth2 과정
* 1. 토큰 받기 (인증)
* 2. 토큰으로 access토큰 받기 (정보에 대한 접근 권한)
* 3. access토큰으로 정보 요청
* 4. 받아온 정보를 이용해 바로 회원가입을 할 수 있음 or 회원에 추가정보가 필요한 경우 추가정보입력화면을 띄워서 회원가입 진행
*
* 그런데, OAuth2 Client 라이브러리 사용하면 바로 access토큰, 사용자프로필정보 를 받아온다.
*/
@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록됩니다.
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) // securedEnabled = true -> @Secured 어노테이션 활성화 // prePostEnabled = true -> @PreAuthorize, @PostAuthorize 어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PrincipalOauth2UserService principalOauth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // crsf 토큰 비활성화 ( 테스트 시 필요)
.authorizeRequests()
.antMatchers("/user/**").authenticated() // 이 경로는 인증이 필요하다. ( 별도 역할을 체크하지 않고 로그인만 되면 접속 가능)
.antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')") // 이 경로는 해당 역할이 필요하다.
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") // 이 경로는 해당 역할이 필요하다.
.anyRequest().permitAll() // 위 경로가 아닌 경우는 누구나 허용
.and()
.formLogin() // 위 경로가 아닌 요청은 로그인 페이지로 이동시키고
.loginPage("/loginForm") // 로그인 페이지는 이 경로로 하겠다.
.loginProcessingUrl("/login") // 이 경로가 호출되면 시큐리티가 낚아채서 대신 로그인을 진행한다.
.defaultSuccessUrl("/") // /loginForm 으로 들어와서 정상 로그인 후 이동되는 경로 ( 다른 경로로 들어와서 로그인 한 경우에는 들어온 경로로 callback 한다)
.and()
.oauth2Login() // OAuth2기반의 로그인인 경우
.loginPage("/loginForm") // 인증이 필요한 URL에 접근하면 /loginForm으로 이동
.userInfoEndpoint() // 로그인 성공 후 사용자정보를 가져온다
.userService(principalOauth2UserService) // 로그인 후 후처리하는 객체 등록 // 구글서버가 '엑세스 토큰', '사용자 프로필 정보'를 반환
;
// .authorizationEndpoint()
// .baseUri("/login/oauth2/authorization");
}
}
참고
'개발환경, 도구 > 오픈API' 카테고리의 다른 글
springboot OAuth2 Client - facebook, naver login (0) | 2023.01.11 |
---|---|
kakao developers - 로그인 (0) | 2022.12.29 |