5.3 구글 로그인 연동하기
사용자 정보를 담당할 도메인인 User 클래스를 생성한다. 패키지는 domain아래에 user 패키지를 생성한다.
User.java
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.*;
@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();
}
}
@Enumerated(Enum Type.STRING)
- JPA로 데이터 베이스로 저장할 때 Enum 값을 어떤 형태로 저장할지를 결정한다.
- 기본적으로는 int로 된 숫자가 저장된다.
- 숫자로 저장되면 데이터베이스로 확인할 때 그 값이 무슨 코드를 의미하는지 알 수가 없다
- 그래서 문자열 (EnumType.STRING)로 저장될 수 있도록 선언
Role.java
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;
}
스프링 시큐리티에서는 권한 코드에 항상 ROLE_이 앞에 있어야한다.
그래서 코드별 키 값을 ROLE_GUEST, ROLE_USER 등으로 지정한다.
UserRepository.java
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);
}
@findByEmail
- 소셜 로그인으로 반환되는 값 중 email을 통해 이미 생성된 사용자인지 처음 가입하는 사용자인지 판단하기 위한 메소드
UserRepository : User의 CRUD를 책임진다
스프링 시큐리티 설정
build.gradle
compile('org.springframework.boot:spring-boot-starter-oauth2-client')
spring-boot-starter-oauth2-client
- 소셜 로그인 등 클라이언트 입장에서 소셜 기능 구현 시 필요한 의존성
- spring-security-oauth2-client와 spring-security-oauth2-jose를 기본으로 관리해줌
config.auth 패키지를 생성. 앞으로 시큐리티 관련 클래스는 모두 이곳에 담는다.
SecurityConfig.java
package com.jojoldu.book.springboot.config.auth;
import com.jojoldu.book.springboot.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final CustomOAuth2UserService customOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**", "/profile").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
① @EnableWebSecurity
- Spring Security 설정들을 활성화시켜 준다
② csrf().disable() .headers().frameOptions().disable()
- h2-console 화면을 사용하기 위해 해당 옵션들을 disable해야함
③ authorizeRequests
- URL 별 권한 관리를 설정하는 옵션의 시작점
- authorizeRequests가 선언되어야만 antMatchers 옵션을 사용할 수 있다.
④ antMatchers
- 권한 관리 대상을 지정하는 옵션
- URL,HTTP 메소드별로 관리가 가능
- "/"등 지정된 URL들을 permitAll()옴션을 통해 전체 열람권한을 주었음
- "/api/v1/**"주소를 가진 API는 USER 권한을 가진 사람만 가능하도록 했다
⑤ anyRequest
- 설정된 값들 이외 나머지 URL들을 나타냄
- 여기서는 authenticated()을 추가하여 나머지 URL들은 모두 인증된 사용자들에게만 허용하게 함
- 인증된 사용자 즉, 로그인한 사용자들을 이야기함
⑥ logout().logoutSuccessUrl("/")
- 로그아웃 기능에 대한 여러 설정의 진입점
- 로그아웃 성공 시 /주소로 이동
⑦ oauth2Login
- OAuth2로그인 기능에 대한 여러 설정의 진입점
⑧ userInfoEndpoint
- OAuth2로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당
⑨ userService
- 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록
- 리소스 서버(소셜 서비스들)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시할 수 있음
CustomOAuth2UserService.java
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.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 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<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());
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의 attribute를 담을 클래스
- 이후 네이버 등 다른 소셜 로그인도 이 클래스를 사용
- 바로 아래에서 이 클래스의 코드가 나오니 차례대로 생성하면 됨
④ SessionUser
- 세션에서 사용자 정보를 저장하기 위한 Dto클래스
구글 사용자 정보가 업데이트 되었을 때를 대비해 update기능도 같이 구현.
사용자의 이름이나 프로필 사진이 변경되면 User 엔티티에도 반영된다
OAuthAttributes.java
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 {
public Map<String, Object> getAttributes;
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 User toEntity(){
return User.builder().name(name).email(email).picture(picture).role(Role.GUEST).build();
}
}
① of()
- OAuth2User에서 반환하는 사용자 정보는 Map이기 때문에 값 하나하나를 변환해야만 함
② toEntity()
- User 엔티티를 생성
- OAuthAttributes에서 엔티티를 생성하는 시점은 처음 기입할 때
- 가입할 때 기본권한을 GUEST로 주기 위해서 role 빌더값에는 Role.GUEST를 사용함
- OAuthAttributes 클래서 생성이 끝났으면 같은 패키지에 SessionUser클래스를 생성
SessionUser.java
package com.jojoldu.book.springboot.config.auth.dto;
import com.jojoldu.book.springboot.domain.user.User;
import lombok.Getter;
@Getter
public class SessionUser {
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();
}
}
SessionUser에는 인증된 사용자 정보만 필요하다.
-> name,email, picture
로그인 테스트
index.mustache
...
<h1>스프링 부트로 시작하는 웹 서비스 Ver.2 </h1>
<div class="col-md-12">
<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>
<br>
...
① {{#userName}}
- 머스테치는 다른 언어와 같은 if문(if userName !=null 등)을 제공하지 않음
- true/false 여부만 판단
- 그래서 머스테치에서는 항상 최종값을 넘겨줘야함
- 여기서도 역시 userName이 었다면 userName을 노출시키도록 구성
② a href="/logout"
- 스프링 시큐리티에서 기본적으로 제공하는 로그아웃 URL
- 즉, 개발자가 별도로 저 URL에 해당하는 컨트롤러 만들 필요 없음
- SecurityConfig 클래스에서 URL을 변경할 순 있지만 기본 URL을 사용해도 충분함
③ {{^userName}}
- 머스테치에서 해당 값이 존재하지 않는 경우에는 ^사용
- 여기서는 userName이 없다면 로그인 버튼을 노출시키도록 구성
④ a href="/oauth2/authorization/google"
- 스프링 시큐리티에서 기본적으로 제공하는 로그인 URL
- 로그아웃 URL과 마찬가지로 개발자가 별도의 컨트롤러를 생성할 필요가 없다.
index.mustache에서 userName을 사용할 수 있게
IndexController에서 userName을 model에 저장하는 코드를 추가
...
import javax.servlet.http.HttpSession;
@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) httpSession.getAttribute("user")
- 앞서 작성된 CustomOAuth2UserService에서 로그인 성공 시 세션에 SessionUser를 저장하도록 구성
- 즉, 로그인 성공 시 httpSession.getAttribute("user")에서 값을 가져올 수 있다
② if(user != null)
- 세션에 저장된 값이 있을 때만 model에 userName으로 등록
- 세션에 저장된 값이 없으면 model엔 아무런 값이 없는 상태이니 로그인 버튼이 보이게 됨
실행
회원 가입이 잘 되어있는지 확인하기
http://localhost:8080/h2-console 에 접속해서 user 테이블을 확인하기
권한이 GUEST인 것을 볼 수 있다. -> 이 상태에서는 posts 기능을 전혀 쓸 수 없다.
-> 이제는 글을 등록할 수 있음
출처 : 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 [이동욱 지음]
'Back-End > Springboot와 AWS로 혼자 구현하는 웹 서비스' 카테고리의 다른 글
Chapter 05. 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (5) (0) | 2022.03.27 |
---|---|
Chapter 05. 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (4) (0) | 2022.03.27 |
Chapter 05. 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (2) (0) | 2022.03.25 |
Chapter 05. 스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기 (1) (0) | 2022.03.25 |
Chapter 04. 머스테치로 화면 구성하기 (5) (0) | 2022.03.19 |