OAuth 란
개발 환경
- JDK 17
- Spring boot 버전 : 3.0.6
- PostgreSQL : 14.0
사용 플러그인
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
구글 OAuth 설정하기
구글 앱 등록하기
- 먼저 https://console.cloud.google.com/ 이 사이트로 이동을 해서 구글 앱을 등록합니다.
- 구글 Cloud 사이트로 이동 후 왼쪽 상단에 보면 Google Cloud 제목 오른쪽에 네모 버튼을 클릭하면 위와 같은 팝업창이 화면에 나타납니다.
- 팝업창이 나타났을 때 팝업창에서 오른쪽 위에 [ 새 프로젝트 ] 버튼을 클릭합니다.
- 새 프로젝트를 생성하는 화면으로 이동합니다.
- 프로젝트 이름은 사용자가 원하는 명칭으로 설정하면 됩니다. 명칭을 입력 후 아래의 [ 만들기 ] 버튼을 클릭하면 신규 프로젝트가 생성됩니다.
- 생성되는 데 몇 초 정도의 시간이 소요됩니다.
- 생성 후 위 화면에서 빨간 네모칸을 눌러줍니다. 그러면 생성한 프로젝트 목록이 표시되면서 생성한 프로젝트를 선택해줍니다.
- 선택 후 생성된 프로젝트로 이동합니다.
사용자 인증 정보 - OAuth 동의 하면 추가하기
- 왼쪽 상단의 Google Cloud 글자 왼쪽에 있는 버튼을 클릭 후 API 및 서비스 클릭 후 사용자 인증 정보를 클릭합니다.
- 사용자 인증 정보 화면으로 이동합니다.
- 오른쪽의 빨간 네모칸에 있는 [ 동의 하면 구성 ] 버튼을 클릭합니다.
- OAuth 동의 화면 이동합니다.
- User Type 을 선택을 하는데, [ 외부 ] 를 선택하고 만들기 버튼을 클릭합니다.
- 앱 이름, 사용자 지원 이메일, 개발자 연락처 정보를 입력 후 제일 하단의 [ 저장 후 계속 ] 버튼을 클릭합니다.
- 범위를 지정하는 화면으로 이동합니다.
- 왼쪽의 [ 범위 추가 또는 삭제 ] 버튼을 클릭하면 오른쪽에 범위를 선택할 수 있는 화면이 나타납니다.
- 오른쪽에 나타난 화면에서 제일 상단의 위에 3개만 선택 후 [ 저장 후 계속 ] 버튼을 클릭합니다.
- 테스트 사용자를 추가하는 화면으로 이동합니다.
- 추가할 테스트 사용자가 없기 때문에 별도의 추가는 하지 않고, [ 저장 후 계속 ] 버튼을 클릭합니다.
사용자 인증 정보 만들기
- 사용자 인증 정보 화면에서 오른쪽 상단에 있는 [ + 사용자 인증 정보 만들기 ] 버튼을 클릭하면 아래에 메뉴가 표시됩니다.
- 아래에 표시된 메뉴 중에서 [ OAuth 클라이언트 ID ] 클릭합니다.
- 애플리케이션 유형은 [ 웹 애플리케이션 ] 을 선택해줍니다.
- 이름과 승인된 리디렉션 URI 를 입력해줍니다. 저희는 로컬로 서버를 운영하기 때문에 주소에는 localhost 를 넣어줍니다.
- 입력 후 하단의 [ 만들기 ] 버튼을 클릭합니다.
- 생성 후 사용자 인증 정보 화면으로 이동되면서 만든 OAuth 정보가 팝업창에 표시됩니다.
- 표시된 화면에서 클라이언트 ID, 클라이언트 보안 비밀번호는 스프링부트에서 추가할 설정 파일에 있어야 하는 정보기 때문에 따로 복사를 해줍니다.
개발
프로젝트 구조
application-oauth.properties
spring.security.oauth2.client.registration.google.client-id={clientID}
spring.security.oauth2.client.registration.google.client-secret={client secret key}
spring.security.oauth2.client.registration.google.scope=profile,email
- Google Cloud 에서 추가한 OAuth 의 클라이언트 ID와 클라이언트 시크릿 키 값을 추가합니다.
- scope 의 기본값은 openid, profile, email이 포함되어 있습니다. 저희는 OAuth2 Service 직접 구현하기 때문에 open id 를 제외하고 profile, email 만 추가해줍니다.
application.properties
spring.profiles.include=oauth
spring.datasource.hikari.maximum-pool-size=4
# Database 설정
spring.datasource.url=jdbc:postgresql://localhost:5432/study
spring.datasource.username=testuser
spring.datasource.password=testpass
# JPA 설정
spring.jpa.hibernate.dialect = org.hibernate.dialect.PostgreSQL10Dialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql = true
spring.jpa.show-sql = true
- spring.profiles.include 는 oauth 설정 파일을 읽을 수 있게 추가하는 부분입니다.
Role.java
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
}
OAuthAttributes.java
package com.ina.oauth_test.common.auth.dto;
import com.ina.oauth_test.api.domain.Users;
import com.ina.oauth_test.common.status.Role;
import lombok.Builder;
import lombok.Getter;
import java.util.Map;
@Getter
public class OAuthAttributes {
private Map<String, Object> attributes;
private String nameAttributeKey;
private String name;
private String email;
private String picture;
@Builder
public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email, String picture) {
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
this.name = name;
this.email = email;
this.picture = picture;
}
public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
return ofGoogle(userNameAttributeName, attributes);
}
private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
return OAuthAttributes.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.picture((String) attributes.get("picture"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}
public Users toEntity() {
return Users.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
SessionUser.java
package com.ina.oauth_test.common.auth.dto;
import com.ina.oauth_test.api.domain.Users;
import lombok.Getter;
import java.io.Serializable;
@Getter
public class SessionUser implements Serializable {
private String name;
private String email;
private String picture;
public SessionUser(Users user) {
this.name = user.getName();
this.email = user.getEmail();
this.picture = user.getPicture();
}
}
CustomOAuth2UserService.java
package com.ina.oauth_test.common.auth;
import com.ina.oauth_test.api.domain.Users;
import com.ina.oauth_test.api.repository.UserRepository;
import com.ina.oauth_test.common.auth.dto.OAuthAttributes;
import com.ina.oauth_test.common.auth.dto.SessionUser;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
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 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 registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
Users user = saveOrUpdate(attributes);
httpSession.setAttribute("user", new SessionUser(user));
return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey());
}
private Users saveOrUpdate(OAuthAttributes attributes) {
Users user = userRepository.findByEmail(attributes.getEmail())
.map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
Users.java
package com.ina.oauth_test.api.domain;
import com.ina.oauth_test.common.status.Role;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@Entity
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String email;
@Column
private String picture;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
@Builder
public Users(String name, String email, String picture, Role role) {
this.name = name;
this.email = email;
this.picture = picture;
this.role = role;
}
public Users update(String name, String picture) {
this.name = name;
this.picture = picture;
return this;
}
public String getRoleKey() {
return this.role.getKey();
}
}
- 사용자 정보 Entity 입니다.
- PostgreSQL 사용 시 User 로 Entity 를 생성할 수 없기 때문에 Users 이름으로 Entity 객체를 생성해줘야 합니다.
UserRepository.java
package com.ina.oauth_test.api.repository;
import com.ina.oauth_test.api.domain.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<Users, Long> {
Optional<Users> findByEmail(String email);
}
OAuthController.java
package com.ina.oauth_test.api.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class OAuthController {
@GetMapping("/loginForm")
public String home() {
return "loginForm";
}
@GetMapping("/private")
public String privatePage() {
return "privatePage";
}
}
SecurityConfig.java
package com.ina.oauth_test.common.config;
import com.ina.oauth_test.common.auth.CustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.httpBasic().disable()
.csrf().disable()
.cors().and()
.authorizeRequests()
.requestMatchers("/private/**").authenticated() //private로 시작하는 uri는 로그인 필수
.anyRequest().permitAll() //나머지 uri는 모든 접근 허용
.and().oauth2Login()
.loginPage("/loginForm") //로그인이 필요한데 로그인을 하지 않았다면 이동할 uri 설정
.defaultSuccessUrl("/") //OAuth 구글 로그인이 성공하면 이동할 uri 설정
.userInfoEndpoint()//로그인 완료 후 회원 정보 받기
.userService(customOAuth2UserService).and().and().build(); //로그인 후 받아온 유저 정보
}
}
- oauth2Login() 설정을 추가를 해줌으로써 oauth 로 로그인할 수 있게 합니다.
- 로그인 성공 후 토큰을 발행이 필요한 경우 .successHandler 를 추가해주면 된다.
loginForm.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="/oauth2/authorization/google" class="btn btn-sm btn-success active" role="button">Google Login</a><br>
</body>
</html>
- index.html, logout.html, privatePage.html 은 기본 생성 html 로 생성해주시면 됩니다.
참고 사이트
Spring Security와 Oauth 2.0으로 로그인 구현하기(SpringBoot + React)
OAuth 2.0 + JWT + Spring Security로 회원 기능 개발하기 - 앱등록과 OAuth 2.0 기능구현
'Framework > Springboot' 카테고리의 다른 글
스프링부트로 파일 업로드 구현하기 (0) | 2024.02.07 |
---|---|
스프링부트로 이메일 인증 서비스 구현하기 (4) | 2024.02.04 |
스프링부트로 이메일 인증 서비스 구현하기 (0) | 2024.02.03 |
스프링부트로 이메일 인증 서비스 구현하기 (0) | 2024.01.31 |
스프링부트에서 html 템플릿 조회하기 (0) | 2024.01.30 |
댓글