OAuth에서 가장 어려웠던 부분이 프로젝트 구현입니다. Spring Security를 완벽하게 알지 못해서 그랬습니다. 그래서 무작정 공책에 베껴썼습니다. 계속 베껴쓰다보니 이해가 됐습니다. 대신 어깨에 담이 왔습니다. 프로젝트 구현과 맞바꿈 셈입니다. 어깨에 걸린 담은 며칠 이내로 사라질 겁니다. 그러나 힘겹게 이해한 개념은 사라지지 않을 겁니다.
OAuth 프로젝트 구현을 하기 위해서는 준비물이 필요합니다. Spring Web Layer에 대해 알고 있다면 크게 어렵지 않습니다.
@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();
}
}
Security 사용 시 ROLE_xx
로 작성해야 합니다.
@Getter
@RequiredArgsConstructor
public enum Role {
GUEST("ROLE_GUEST", "손님"),
USER("ROLE_USER", "일반 사용자");
private final String key;
private final String title;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
SecurityConfig는 Spring Security 설정 파일입니다.
.csrf().disable().headers().frameOptions().disable
은 h2-console 화면을 사용하기 위해 작성했습니다.
antMatchers()
는 권한 관리 대상을 지정합니다.
antMatcher("/").permitAll()
로 하게 되면 "/" 경로에 대해 전체 열람 권한을 부여합니다.
antMatchers("/user/**").hasRole(Role.User.name())
은 "/user/**" 경로에 대해 Role이 user인 사용자만 열람할 수 있습니다.
anyRequest.authenticated()
는 .antMatchers()에서 설정된 값 외의 나머지 경로에 대해 인증된 사용자만 열람할 수 있습니다.
oauth2Login().userInfoEndpoint()
는 OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 사용합니다.
@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/**").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
@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();
}
}
@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();
}
}
registrationId
는 현재 로그인을 진행 중인 서비스(구글, 네이버 등)를 구분합니다.
userNameAttributeName
은 테이블의 PK와 같은 개념입니다.
OAuthAttributes
는 OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담은 클래스입니다.
@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);
}
}
스프링 부트와 AWS로 혼자 구현하는 웹 서비스
이동욱 지음ㅣ프리렉ㅣ2019ㅣ도서 정보