해당 내용은 이동욱님 저서 '스프링 부트와 AWS로 혼자 구현하는 웹 서비스'를 공부하며 정리한 내용입니다.
구글의 OAuth 클라이언트 ID 설정까지 마쳤으니 이제 프로젝트에서 이를 적용해보려고 한다.
우선 사용자 정보와 관련된 부분 먼저 구현해보도록 하자.
로그인한 사용자의 정보를 담당한 도메인인 User 클래스
package com.shawn.springboot.domain.user;
import com.shawn.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 = true)
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();
}
}
각 사용자의 권한을 관리할 Enum 클래스
package com.shawn.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;
}
User의 CRUD를 위한 DB Layer
package com.shawn.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);
}
사용자 정보와 관련된 부분의 구현은 모두 끝났다.
이제 본격적으로 시큐리티 관련 로직을 구현할 차례이다.
...
dependencies{
...
compile('org.springframework.boot:spring-boot-starter-oauth2-client')
...
}
...
시큐리티 설정 클래스
시큐리티 관련 클래스를 위치시킬 config.auth 패키지를 생성하고, 이 클래스 역시 config.auth 패키지에 생성한다.
package com.shawn.springboot.config.auth;
import com.shawn.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;
protected void configure(HttpSecurity http) throws Exception{
http
.csrf().disable()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
소셜 로그인 이후 가져온 사용자의 정보(email, name, picture 등)을 기반으로 가입 및 정보 수정, 세션 저장 등의 기능을 제공하는 클래스
package com.shawn.springboot.config.auth;
import com.shawn.springboot.config.auth.dto.OAuthAttributes;
import com.shawn.springboot.config.auth.dto.SessionUser;
import com.shawn.springboot.domain.user.User;
import com.shawn.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);
}
}
OAuth2UserService를 통해 가져온 OAuth2User의 속성을 담는 클래스
package com.shawn.springboot.config.auth.dto;
import com.shawn.springboot.domain.user.Role;
import com.shawn.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){
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();
}
}
인증된 사용자 정보를 담는 클래스
package com.shawn.springboot.config.auth.dto;
import com.shawn.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();
}
}
이렇게 서버의 설정은 모두 끝났다.
이제 화면에서 구글 로그인 버튼을 만들어서 정상적으로 로그인이 되는지 테스트를 해볼 차례이다.
"스프링 부트와 AWS로 혼자 구현하는 웹 서비스"에서 mustache로 구현한 화면에 로그인 버튼을 만들어본다.
<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>
index.mustache에서 userName을 사용할 수 있도록 IndexController에서 userName을 model에 추가해야 한다.
package com.shawn.springboot.web;
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";
}
}
서버를 구동시켜서 index.mustache에 구글 로그인을 추가한 화면을 보니 버튼이 제대로 추가되었다.
구글 로그인 버튼을 선택하니 익숙한 구글 로그인 화면이 나타났다.
화면에 보이는 계정은 구글에서 동의화면을 구성하고 클라이언트 ID를 생성할 때 테스트가 가능하도록 추가해놓은 구글 계정이다.
계정을 선택해서 로그인을 시도해보았다.
정상적으로 로그인이 된 것을 확인 할 수 있다.
DB의 유저 정보를 확인해보아도 제대로 회원가입이 된 것을 알 수 있다.
OAuthAttributes에서 구현한대로 처음 회원 가입시 권한이 GUEST로 설정 되어 있는 것을 확인할 수 있다.