Published 2022. 4. 29. 17:06
05장 (3) 구글로그인 - 구글로그인 연동하기
1) 기능 구현하기
- User 클래스 생성
- Role 클래스 생성
- UserRepository 클래스 생성
package com.jojoldu.book.springboot.domain.user;
import com.jojoldu.book.springboot.domain.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Getter
@NoArgsConstructor
@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();
}
}
package com.jojoldu.book.springboot.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;
}
package com.jojoldu.book.springboot.domain.user;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
2) 스프링 시큐리티 설정
- build.gradle 스프링 시큐리티 관련 의존성 하나 추가
- com/jojoldu/book/springboot/config/auth 디렉토릭 생성 후 SecurityConfig클래스 생성
- CustomOAuth2UserService 클래스 생성
- com/jojoldu/book/springboot/config/auth/dto 디렉토리 생성 후 OAuthAttributes 클래스 생성
- SessionUser 클래스 생성
- 소셜로그인 등 클라이언트 입장에서 소셜기능 구현시 필요한 의존성입니다.
implementation('org.springframework.boot:spring-boot-starter-oauth2-client')
- @EnableWebSecurity - Spring Security 설정들을 활성화시켜줌
- csrf().disable().headers().frameOptions().disable() - h2-console 화면을 사용하기 위해 해당 옵션들을 disable
- authorizeRequests - URL별 권한 관리를 설정하는 옵션의 시작점 -> 이것이 선언되어야 andMatchers 옵션 사용 가능 권한 관리 대상을 지정하는 옵션 URL, HTTP 메소드별로 관리가 가능 "/" 등 지정된 URL들은 permitAll() 옵션을 통해 전체 열람 권한을 줌 "api/v1/**" 주소를 가진 API는 USER 권한을 가진 사람만 가능하도록 함
- anyRequest - 설정된 값들 이외 나머지 URL을 나타냄 여기서는 authenticated()를 추가하여 나머지 URL들은 모두 인증된 사용자들에게만 허용 인증된 사용자 즉, 로그인 사용자들만 허용
- logout().logoutSuccessUrl("/") - 로그아웃 기능에 대한 여러 설정의 진입점 로그아웃 성공 시 "/" 주소로 이동
- oauth2Login : OAuth2 로그인 기능에 대한 여러 설정의 진입점
- userInfoEndpoint : OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당
- userService : 소셜 로그인 성공 시 후속 조치를 진행할 UserService
package com.jojoldu.book.springboot.config.auth;
import com.jojoldu.book.springboot.config.auth.dto.OAuthAttributes;
import com.jojoldu.book.springboot.config.auth.dto.SessionUser;
import com.jojoldu.book.springboot.domain.user.User;
import com.jojoldu.book.springboot.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.*;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.*;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
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 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());
User user = saveOrUpdate(attributes);
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);
}
}
- registrationId : 현재 로그인 진행 중인 서비스를 구분하는 코드 이 후 네이버 로그인 연동 시에 네이버 로그인인지, 구글 로그인인지 구분하기 위해 사용
- userNameAttributeName : OAuth2 로그인 진행 시 키가 되는 필드값 Primary Key와 같은 의미 구글의 경우 기본적으로 코드를 지원 ("sub") 하지만, 네이버 카카오 등은 기본 지원하지 않음 이후 네이버 로그인과 구글 로그인을 동시 지원할 때 사용
- OAuthAttributes : OAuth2UserService를 통해 가져온 OAuth2User의 attibutes를 담을 클래스 이후 네이버 등 다른 소셜 로그인도 이 클래스를 사용
- SessionUser : 세션에 사용자 정보를 저장하기 위한 Dto 클래스
package com.jojoldu.book.springboot.config.auth;
import com.jojoldu.book.springboot.config.auth.dto.OAuthAttributes;
import com.jojoldu.book.springboot.config.auth.dto.SessionUser;
import com.jojoldu.book.springboot.domain.user.User;
import com.jojoldu.book.springboot.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.*;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.*;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpSession;
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 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());
User user = saveOrUpdate(attributes);
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);
}
}
- of : OAuth2User에서 반환되는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야 함
- toEntity() : User 엔티티를 생성 OAuthAttributes에서 엔티티를 생성하는 시점은 처음 가입했을 때 가입할 때의 기본 권한을 GUEST로 주기 위해서 role 빌더값에는 Role.GUEST를 사용
package com.jojoldu.book.springboot.config.auth.dto;
import com.jojoldu.book.springboot.domain.user.Role;
import com.jojoldu.book.springboot.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) {
if("naver".equals(registrationId)) {
return ofNaver("id", 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();
}
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();
}
public User toEntity() {
return User.builder()
.name(name)
.email(email)
.picture(picture)
.role(Role.GUEST)
.build();
}
}
package com.jojoldu.book.springboot.config.auth.dto;
import com.jojoldu.book.springboot.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();
}
}
3) 로그인 테스트
- IndexController 의 main메소드를 수정합니다.
- index.mustache 를 수정합니다.
- (SessionUser) httpSession.getAttribute("user") : CustomOAuth2UserService에서 로그인 성공 시 세션에 SessionUser를 저장하도록 구성 즉, 로그인 성공 시 httpSession.getAttibute("user")에서 값을 가져올 수 있음
- if(user != null) : 세션에 저장된 값이 있을 때만 model에 userName으로 등록 세션에 저장된 값이 없으면 model엔 아무런 값이 없는 상태니 로그인 버튼이 보이게 됨
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";
}
- {{#userName}} : 머스테치는 다른 언어와 같은 if문을 제공하지 않고 true/false 여부만 판단 그래서 머스테치에서는 항상 최종값을 넘겨줘야 함 여기서는 userName이 있다면 userName을 노출시키도록 구성
- a href="/logout" : 스프링 시큐리티에서 기본적으로 제공하는 로그아웃 URL 이로인해 개발자가 별도로 이 URL에 해당하는 컨트롤러를 만들 필요가 없음 SecurityConfig 클래스에서 URL을 변경할 수 있지만 여기선 기본 URL을 사용
- {{^userName}} : 머스테치에서 해당 값이 존재하지 않는 경우에는 ^을 사용 userName이 없다면 로그인 버튼을 노출시키도록 구성
- a href="/oauth2/authorization/google" : 스프링 시큐리티에서 기본적으로 제공하는 로그인 URL 로그아웃 URL과 마찬가지로 개발자가 별도의 컨트롤러를 생성할 필요가 없음
<div class="row">
<div class="col-md-6">
<a href="/posts/save" role="button" class="btn btn-primary">글 등록</a>
{{#userName}}
Logged in as: <span id="user">{{userName}}</span>
<a href="/logout" class="btn btn-info active" role="button">Logout</a>
{{/userName}}
{{^userName}}
<a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
{{/userName}}
</div>
</div>
'무조건 따라하기 > Spring Boot 기반 Web Service' 카테고리의 다른 글
스프링부트와 AWS로 혼자 구현하는 웹 서비스 - 05장 (2) 구글로그인 - 구글크라우드 설정 (0) | 2022.04.29 |
---|---|
스프링부트와 AWS로 혼자 구현하는 웹 서비스 - 05장 (1) 스프링부트 1.5 vs 스프링부트 2.0 (0) | 2022.04.29 |
스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 4장 (2) (0) | 2022.04.21 |
스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 4장 (1) (0) | 2022.04.21 |
스프링 부트와 AWS로 혼자 구현하는 웹 서비스 - 3장 (2) (0) | 2022.04.20 |