1. 구글 로그인
1) 구글 로그인 설정
https://darrenlog.tistory.com/38
[OAuth 2.0] 개념과 Google OAuth2.0 세팅부터 스프링부트로 구현-1
안녕하세요. 이번 게시글에서는 OAuth 2.0에 대한 간략한 개념과 스프링부트 환경에서 직접 구현해보는 단계까지 다뤄보도록 하겠습니다. OAuth, Open Authorization 정의 OAuth는 인터넷 사용자들이 비밀
darrenlog.tistory.com
2) application-oauth.properties 등록
spring.security.oauth2.client.registration.google.client-id=클라이언트 ID
spring.security.oauth2.client.registration.google.client-secret=클라이언트 보안 비밀코드
spring.security.oauth2.client.registration.google.scope=profile,email
+) application.properties 에서 include 하도록 설정
# oauth 포함
spring.profiles.include=oauth
3) .gitignore 등록
.gitignore 파일에 application-oauth.properties 작성
💡 근데 .gitignore 파일에 작성해도 무시가 안되는 경우
cd C:\study\study-springboot-webservice (프로젝트 경로)
git rm -r --cached .
git add .
git commit -m "Fix .gitignore and untrack application-oauth.properties"
4) build.gradle - 스프링 시큐리티 설정
// spring security oauth2 client
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
5) User - 사용자 정보를 담는 객체
package com.example.studyspringbootwebservice.domain.user;
import com.example.studyspringbootwebservice.domain.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
@Table(name = "users")
@Entity
public class User extends BaseTimeEntity {
@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 User(String name, String email, String picture, Role role) {
this.name = name;
this.email = email;
this.picture = picture;
this.role = role;
}
public User update(String name, String picture) {
this.name = name;
this.picture = picture;
return this;
}
public String getRoleKey() {
return this.role.getKey(); // 'ROLE_GUEST', 'ROLE_USER'
}
}
- @Table
- 이름을 따로 users로 설정하지 않으면 자바의 예약어 user와 겹쳐서 sql 오류
- 이름을 따로 users로 설정하지 않으면 자바의 예약어 user와 겹쳐서 sql 오류
- @Enumerated(EnumType.STRING)
- JPA로 데이터베이스 저장할 때 Enum 값을 어떤 형태로 저장할지 결정
- 기본적으로는 int 저장 -> 그 값이 무슨 코드를 의미하는지 알 수 없음
6) Role - 사용자의 권한 관리 Enum 클래스
package com.example.studyspringbootwebservice.domain.user;
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;
}
7) UserRepository - User의 CRUD
package com.example.studyspringbootwebservice.domain.user;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
// 소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하기 위한 메소드
Optional<User> findByEmail(String email);
}
8) SecurityConfig
package com.example.studyspringbootwebservice.config.auth;
import com.example.studyspringbootwebservice.domain.user.Role;
import jakarta.servlet.DispatcherType;
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 // Spring Security 설정 활성화
@Configuration
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
protected SecurityFilterChain config(HttpSecurity http) throws Exception {
http
.csrf().disable() // 토큰 검증 비활성화
.headers().frameOptions().disable() // h2-console 화면을 사용하기 위해 해당 옵션들 disable
.and()
.authorizeHttpRequests( // http 요청에 대한 인증, 권한 설정 수행
request -> request
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() // Forwad 요청에 대해 모든 사용자의 접근 허용
.requestMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**").permitAll()
.requestMatchers("/api/v1/**").hasRole(Role.USER.name()) // 해당 경로에 대해서는 USER 권한을 가진 사용자만 접근 허용
.anyRequest().authenticated() // 이외의 모든 요청은 인증된 사용자만 접근 허용
)
.logout().logoutSuccessUrl("/")
.and()
.oauth2Login() // oauth2.0 로그인 설정
.userInfoEndpoint() // 로그인 성공한 사용자 정보를 가져올 때의 설정
.userService(customOAuth2UserService); // 로그인 성공 시 수행할 service 등록
return http.build();
}
}
WebSecurityConfigurerAdapter상속받아서 configure 메소드를 오버라이드 하는데 deprecated 됐음- WebSecurityConfigurerAdapter ---> SecurityFilterChain을 스프링 빈에 등록해서 사용
- antMatchers ---> requestMatchers
- authorizeHttpRequests() : URL별 권한 관리를 설정하는 옵션의 시작. 얘가 있어야 requestMatchers 옵션 쓸 수 있음
9) CustomOAuth2UserService - 로그인한 사용자 정보를 가지고 수행할 작업
package com.example.studyspringbootwebservice.config.auth;
import com.example.studyspringbootwebservice.config.auth.dto.OAuthAttributes;
import com.example.studyspringbootwebservice.config.auth.dto.SessionUser;
import com.example.studyspringbootwebservice.domain.user.User;
import com.example.studyspringbootwebservice.domain.user.UserRepository;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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 java.util.Collections;
@RequiredArgsConstructor
@Service
@Slf4j
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 기본적으로 제공되는 DefaultOAuth2UserSevice 사용 : OAuth 2.0 공급자로부터 받은 액세스 토큰을 사용하여 사용자 정보를 가져옴
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
// 사용자 정보
OAuth2User oAuth2User = delegate.loadUser(userRequest);
// 현재 로그인 진행 중인 OAuth 2.0 서비스를 구분하는 코드
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// OAuth 2.0 로그인 진행 시 키가 되는 필드값. 구글은 기본적으로 코드 지원(sub)
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
// 조회한 OAuth2User가 있을 경우 update, 없을 경우 save
User user = saveOrUpdate(attributes);
// 세션에 사용자 정보를 저장하기 위한 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(), attributes.getPicture()))
.orElse(attributes.toEntity());
return userRepository.save(user);
}
}
- oAuth2User의 attributes
- 사용자 정보
- 사용자 정보
// 구글의 attribute 구조
{
sub=12343941,
name=김준희,
given_name=준희,
family_name=김,
picture=https://~~~,
email=이메일주소,
email_verified=true,
locale=ko
}
- registrationId = google
- 현재 로그인 진행 중인 OAuth2.0 서비스 구분
- 현재 로그인 진행 중인 OAuth2.0 서비스 구분
- userNameAttributename = sub
- 로그인 진행 시 키가 되는 필드값
- 구글은 기본적으로 코드 지원 (sub)
- attribute = OAuthAttributes 객체
- OAuth2User의 attribute를 담을 OAuthAttributes 클래스
- OAuth2User의 attribute를 담을 OAuthAttributes 클래스
- user = User
- 로그인한 OAuth2User 정보가 이미 있을 경우 업데이트, 없을 경우 생성한 뒤 저장된 User 객체
- 이 객체를 HttpSession 세션의 attribute에 저장함
- new SessionUser(user)
- 객체의 상태를 일련의(serial) 바이트로 변환하여 저장
- 자바 시스템 간 데이터 교환하기 위함
- 로그인한 사용자의 정보를 세션에 저장하고 전달하기 위해 직렬화된 객체 사용
- 세션에 저장할 때, 직렬화하여 저장하면 사용자가 로그인 상태를 유지하는 동안 세션 데이터를 유지할 수 있음
10) OAuthAttributes - 로그인한 사용자의 attribute 정보를 담음
package com.example.studyspringbootwebservice.config.auth.dto;
import com.example.studyspringbootwebservice.domain.user.Role;
import com.example.studyspringbootwebservice.domain.user.User;
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();
}
// OAuthAttributes에서 엔티티 생성하는 시점은 처음 가입할 때-> ROLE_GUEST
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
11) SessionUser - 인증된 사용자 정보를 세션에 저장하기기 위한 직렬화된 객체
package com.example.studyspringbootwebservice.config.auth.dto;
import com.example.studyspringbootwebservice.domain.user.User;
import lombok.Getter;
import java.io.Serializable;
@Getter
public class SessionUser implements Serializable {
private String name;
private String email;
private String picture;
public SessionUser(User user) {
this.name = user.getName();
this.email = user.getEmail();
this.picture = user.getPicture();
}
}
12) IndexController - 사용자 이름을 뷰에서 사용할 수 있도록 모델에 담음
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
private final HttpSession httpSession;
@GetMapping("/")
public String index(Model model) {
model.addAttribute("posts", postsService.findAllDesc());
SessionUser user = (SessionUser) httpSession.getAttribute("user");
if (user != null) {
model.addAttribute("userName", user.getName());
}
return "index";
}
13) index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- header 조각 -->
<div th:replace="~{fragments/header :: main-header}"></div>
<body>
<h1>스프링 부트로 시작하는 웹 서비스</h1>
<div class="col-md-12">
<!-- 로그인 기능 영역 -->
<div class="row">
<div class="col">
<a href="/posts/save" role="button" class="btn btn-dark">글 등록</a>
</div>
<div th:if="${userName}" class="col">
Logged in as : <span id="user" th:text="${userName}"></span>
<a th:href="@{/logout}" class="btn btn-secondary active" role="button">Logout</a>
</div>
<div th:unless="${userName}" class="col">
<a th:href="@{/oauth2/authorization/google}" class="btn btn-primary active" role="button">Google Login</a>
</div>
</div>
<br/>
<!-- 목록 출력 영역 -->
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>게시글번호</th>
<th>제목</th>
<th>작성자</th>
<th>최종수정일</th>
</tr>
</thead>
<tbody id="tbody">
<tr th:each="post : ${posts}">
<td th:text="${post.id}">id</td>
<td><a th:href="@{/posts/update/{id}(id = ${post.id})}" th:text="${post.title}">title</a></td>
<td th:text="${post.author}">author</td>
<td th:text="${post.modifiedDate}">modifiedDate</td>
</tr>
</tbody>
</table>
</div>
<!-- footer 조각 -->
<div th:replace="~{fragments/footer :: main-footer}"></div>
</body>
</html>
게시글 등록 시 -> 403 (권한 거부)
테이블에서 role을 USER로 변경 -> 로그아웃 후 글 등록
2. 로그인 개선하기
1) @LoginUser 어노테이션 기반으로 개선
* IndexController
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
private final HttpSession httpSession;
@GetMapping("/")
public String index(Model model) {
model.addAttribute("posts", postsService.findAllDesc());
SessionUser user = (SessionUser) httpSession.getAttribute("user");
if (user != null) {
model.addAttribute("userName", user.getName());
}
return "index";
}
- SessionUser user = (SessionUser) httpSession.getAttribute("user");
- 세션 정보가 필요할 때마다 위처럼 세션 값을 가져와야 함 -> 같은 코드 반복
(1) @LoginUser 어노테이션 생성
package com.example.studyspringbootwebservice.config.auth;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER) // 이 어노테이션이 생성될 수 있는 위치 지정(메소드의 파라미터로 선언된 객체에서만 사용)
@Retention(RetentionPolicy.RUNTIME) // 런타임 시 클래스파일에서 유지
public @interface LoginUser {
}
(2) LoginUserArgumentResolver - 컨트롤러의 메소드로 로그인한 사용자의 세션 정보를 쉽게 주입
package com.example.studyspringbootwebservice.config.auth;
import com.example.studyspringbootwebservice.config.auth.dto.SessionUser;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@RequiredArgsConstructor
@Component
public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver {
private final HttpSession httpSession;
// 컨트롤러 메소드의 특정 파라미터를 지원하는지 판단
// 파라미터에 @LoginUser 어노테이션이 붙어있고, 파라미터 클래스 타입이 SessionUser.class인 경우 true
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null;
boolean isUserClass = SessionUser.class.equals(parameter.getParameterType());
return isLoginUserAnnotation && isUserClass;
}
// 파라미터에 전달할 객체 생성
// 세션에서 객체 가져옴
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
return httpSession.getAttribute("user");
}
}
- HandlerMethodArgumentResolver 구현
- 컨트롤러 메소드의 매개변수를 해결하는 역할
- 구현 클래스를 통해 컨트롤러 메소드의 매개변수에 값을 제공하는 사용자 지정 로직을 정의할 수 있음
- 주로 요청에 따라 컨트롤러 메소드에 전달되는 매개변수의 값을 추출, 변환하는 작업
- supportsParameter(MethodParameter parameter)
- 특정 파라미터에 리졸버 적용 가능한지 판단
- @LoginUser 어노테이션이 붙어있는지 + 파라미터 타입이 Session 클래스인지 확인
- resolveArgument()
- 실제로 매개변수 값 해결(추출, 변환, 생성 등 작업)하고 반환
- HttpSession에 들어있는 "user" 속성을 찾아서 반환
(3) WebConfig - 위 리졸버를 스프링이 인식할 수 있도록 등록
package com.example.studyspringbootwebservice.config;
import com.example.studyspringbootwebservice.config.auth.LoginUserArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@RequiredArgsConstructor
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginUserArgumentResolver loginUserArgumentResolver;
// HandlerMethodArgumentResolver는 항상 WebMvcConfigurer의 addArgumentResolvers()를 통해 추가해야 함
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserArgumentResolver);
}
}
- Argument Resolver 등록
(4) IndexController - @LoginUser 적용
@RequiredArgsConstructor
@Controller
public class IndexController {
private final PostsService postsService;
private final HttpSession httpSession;
@GetMapping("/")
public String index(Model model, @LoginUser SessionUser user) {
model.addAttribute("posts", postsService.findAllDesc());
if (user != null) {
model.addAttribute("userName", user.getName());
}
return "index";
}
- @LoginUser 어노테이션 확인 + SessionUser 클래스 타입 확인 -> HttpSession에 있는 "user" 속성 반환
2) 세션 저장소로 데이터베이스 사용하기
- 세션 저장소
- 톰캣 세션
- 기본적으로 선택되는 방식
- 톰캣(WAS)에 세션 저장 -> 애플리케이션 실행 시마다 항상 세션 초기화
- 2대 이상의 WAS 구동 환경 -> 톰캣 간 세션 공유를 위한 추가 설정 필요
- MySQL 같은 데이터베이스 (얘로 사용)
- 여러 WAS 간 공용 세션 사용할 수 있는 쉬운 방법
- 설정이 간단함. 로그인 요청이 많이 없는 백오피스, 사내 시스템 용도
- 로그인 요청마다 DB IO 발생 -> 성능 이슈
- Redis 같은 메모리 DB
- B2C에서 많이 사용
- 사용료 지불해야 함
- 실제 서비스로 사용하기 위해서는 임베디드 Redis가 아닌 외부 메모리 서버 필요
- 톰캣 세션
(1) build.gradle - 의존성 등록
// spring session jdbc
implementation 'org.springframework.session:spring-session-jdbc'
(2) application.properties
# 세션저장소를 jdbc로 선택
spring.session.store-type=jdbc
3. 네이버 로그인
1) 네이버 로그인 설정
https://developers.naver.com/apps/#/register
애플리케이션 - NAVER Developers
developers.naver.com
2) application-oauth.properties
# naver
spring.security.oauth2.client.registration.naver.client-id=클라이언트 id
spring.security.oauth2.client.registration.naver.client-secret=클라이언트 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,profile_image
spring.security.oauth2.client.registration.naver.client-name=Naver
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
- user-name-attribute=response
- 기준이 되는 user_name의 이름을 네이버에서는 response로 함
- 네이버의 회원 조회 시 반환되는 attribute 구조 중 최상위 필드는 resultCode, message, response
- 스프링 시큐리티에서는 하위 필드를 명시할 수 X, 최상위 필드만 user_name으로 지정 가능
- 위 3개의 필드 중 하나를 user_name으로 지정해야 함
- response의 id를 user_name으로 지정
// 네이버의 attributes 구조
{
"resultcode": "00",
"message": "success",
"response": {
"email": "openapinaver.com",
"nickname": "OpenAPI",
"profile_image": "https://~~",
"age": "40-49",
"gender": "F",
"id": "327489194",
"name": "오픈API",
"birthday": "10-01"
}
}
3) OAuthAttributes - 로그인한 사용자의 attribute 정보를 담은 객체
package com.example.studyspringbootwebservice.config.auth.dto;
import com.example.studyspringbootwebservice.domain.user.Role;
import com.example.studyspringbootwebservice.domain.user.User;
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) {
// 네이버인지 판단, response의 id를 user_name으로 지정
if ("naver".equals(registrationId)) {
return ofNaver("id", attributes);
}
return ofGoogle(userNameAttributeName, attributes);
}
private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return OAuthAttributes.builder()
.name((String) response.get("name"))
.email((String) response.get("email"))
.picture((String) response.get("profile_image"))
.attributes(response)
.nameAttributeKey(userNameAttributeName)
.build();
}
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();
}
// OAuthAttributes에서 엔티티 생성하는 시점은 처음 가입할 때
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
* CustomOAuth2UserService - 로그인한 OAuth2 서비스에 따라 OAuthAttributes 가져옴
@RequiredArgsConstructor
@Service
@Slf4j
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 기본적으로 제공되는 DefaultOAuth2UserSevice 사용 : OAuth 2.0 공급자로부터 받은 액세스 토큰을 사용하여 사용자 정보를 가져옴
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
// 사용자 정보
OAuth2User oAuth2User = delegate.loadUser(userRequest);
// 현재 로그인 진행 중인 OAuth 2.0 서비스를 구분하는 코드
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// OAuth 2.0 로그인 진행 시 키가 되는 필드값. 구글은 기본적으로 코드 지원(sub)
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());
// 조회한 OAuth2User가 있을 경우 update, 없을 경우 save
User user = saveOrUpdate(attributes);
// 세션에 사용자 정보를 저장하기 위한 Dto 클래스
httpSession.setAttribute("user", new SessionUser(user));
return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
attributes.getAttributes(),
attributes.getNameAttributeKey());
}
4) index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- header 조각 -->
<div th:replace="~{fragments/header :: main-header}"></div>
<body>
<h1>스프링 부트로 시작하는 웹 서비스</h1>
<div class="col-md-12">
<!-- 로그인 기능 영역 -->
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-dark">글 등록</a>
<div th:if="${userName}">
Logged in as : <span id="user" th:text="${userName}"></span>
<a th:href="@{/logout}" class="btn btn-secondary active" role="button">Logout</a>
</div>
<div th:unless="${userName}">
<a th:href="@{/oauth2/authorization/google}" class="btn btn-primary active" role="button">Google Login</a>
<a th:href="@{/oauth2/authorization/naver}" class="btn btn-success active" role="button">Naver Login</a>
</div>
</div>
</div>
<br/>
<!-- 목록 출력 영역 -->
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>게시글번호</th>
<th>제목</th>
<th>작성자</th>
<th>최종수정일</th>
</tr>
</thead>
<tbody id="tbody">
<tr th:each="post : ${posts}">
<td th:text="${post.id}">id</td>
<td><a th:href="@{/posts/update/{id}(id = ${post.id})}" th:text="${post.title}">title</a></td>
<td th:text="${post.author}">author</td>
<td th:text="${post.modifiedDate}">modifiedDate</td>
</tr>
</tbody>
</table>
</div>
<!-- footer 조각 -->
<div th:replace="~{fragments/footer :: main-footer}"></div>
</body>
</html>
5. 카카오 로그인
1) 카카오 로그인 설정
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
2) application-oauth.properties
# kakao
spring.security.oauth2.client.registration.kakao.client-id=
spring.security.oauth2.client.registration.kakao.client-secret=
spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.kakao.scope=profile_nickname,profile_image,account_email
spring.security.oauth2.client.registration.kakao.client-name=Kakao
spring.security.oauth2.client.registration.kakao.client-authentication-method=POST
spring.security.oauth2.client.provider.kakao.authorization-uri=https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri=https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri=https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute=id
3) OAuthAttributes
package com.example.studyspringbootwebservice.config.auth.dto;
import com.example.studyspringbootwebservice.domain.user.Role;
import com.example.studyspringbootwebservice.domain.user.User;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@Getter
@Slf4j
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) {
if ("naver".equals(registrationId)) {
return ofNaver("id", attributes);
} else if ("kakao".equals(registrationId)) {
return ofKakao(userNameAttributeName, attributes);
}
return ofGoogle(userNameAttributeName, attributes);
}
private static OAuthAttributes ofKakao(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
return OAuthAttributes.builder()
.name((String) profile.get("nickname"))
.email((String) kakaoAccount.get("email"))
.picture((String) profile.get("thumbnail_image_url"))
.attributes(attributes)
.nameAttributeKey(userNameAttributeName)
.build();
}
private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return OAuthAttributes.builder()
.name((String) response.get("name"))
.email((String) response.get("email"))
.picture((String) response.get("profile_image"))
.attributes(response)
.nameAttributeKey(userNameAttributeName)
.build();
}
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();
}
// OAuthAttributes에서 엔티티 생성하는 시점은 처음 가입할 때
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
* 카카오의 attribute 구조
{
id=12344656,
connected_at=2023-08-20T14:18:40Z,
properties=
{
nickname=김준희,
profile_image=http://k.kakaocdn.net/~~.jpg,
thumbnail_image=http://k.kakaocdn.net/~~.jpg
},
kakao_account=
{
profile_nickname_needs_agreement=false,
profile_image_needs_agreement=false,
profile=
{
nickname=김준희,
thumbnail_image_url=http://k.kakaocdn.net/~~.jpg,
profile_image_url=http://k.kakaocdn.net/~~.jpg,
is_default_image=false
},
has_email=true,
email_needs_agreement=false,
is_email_valid=true,
is_email_verified=true,
email=1234@naver.com
}
}
4) index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<!-- header 조각 -->
<div th:replace="~{fragments/header :: main-header}"></div>
<body>
<h1>스프링 부트로 시작하는 웹 서비스</h1>
<div class="col-md-12">
<!-- 로그인 기능 영역 -->
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-dark">글 등록</a>
<div th:if="${userName}">
Logged in as : <span id="user" th:text="${userName}"></span>
<a th:href="@{/logout}" class="btn btn-secondary active" role="button">Logout</a>
</div>
<div th:unless="${userName}">
<a th:href="@{/oauth2/authorization/google}" class="btn btn-primary active" role="button">Google Login</a>
<a th:href="@{/oauth2/authorization/naver}" class="btn btn-success active" role="button">Naver Login</a>
<a th:href="@{/oauth2/authorization/kakao}" class="btn btn-warning active" role="button">Kakao Login</a>
</div>
</div>
</div>
<br/>
<!-- 목록 출력 영역 -->
<table class="table table-horizontal table-bordered">
<thead class="thead-strong">
<tr>
<th>게시글번호</th>
<th>제목</th>
<th>작성자</th>
<th>최종수정일</th>
</tr>
</thead>
<tbody id="tbody">
<tr th:each="post : ${posts}">
<td th:text="${post.id}">id</td>
<td><a th:href="@{/posts/update/{id}(id = ${post.id})}" th:text="${post.title}">title</a></td>
<td th:text="${post.author}">author</td>
<td th:text="${post.modifiedDate}">modifiedDate</td>
</tr>
</tbody>
</table>
</div>
<!-- footer 조각 -->
<div th:replace="~{fragments/footer :: main-footer}"></div>
</body>
</html>
'Spring' 카테고리의 다른 글
Spring Boot 3 + Spring Security 6 + Swagger 3 으로 기본 Rest API CRUD 구현하기 (0) | 2023.08.27 |
---|---|
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 4. 타임리프로 화면 구성하기 (0) | 2023.08.18 |
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 3. 프로젝트에 Spring Data JPA 적용하기 (0) | 2023.08.11 |
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 2. 테스트코드 작성하기 (0) | 2023.08.01 |
[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 1. 인텔리제이로 스프링 부트 시작하기 (0) | 2023.07.31 |