로그인 기능을 사용하기 위해서 oauth2 를 임포트 합니다.
compile('org.springframework.boot:spring-boot-starter-oauth2-client')
깃 이그노어 파일에 아래와같이 인증 키 정보가 담긴 application-oauth 파일을 제거합니다.
인증 키의 노출은 보안에 위험하기 때문입니다.
application-oauth.properties
application.properties에서 oauth 사용을 허락합니다.
spring.profiles.include=oauth
index.mutache에 아래에 해당되는 파일을 추가합니다.
{{#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}}
{{}} 문법은 머스테치 문법입니다. 컨트롤러에서 index.mustache 파일을 호출할때 userName 정보를 index.mustache파일에 넘겨주게 되면 로그인한 유저 정보와 logout 버튼을 나타내고, userName이 넘어오지않게 된다면 {{^userName}}이 실행되게 됩니다. ^표식이 들어가면 없을때 안의 값을 반환합니다.
package com.bell_bell.book.springboot.config.auth;
import com.bell_bell.book.springboot.config.auth.dto.OAuthAttributes;
import com.bell_bell.book.springboot.config.auth.dto.SessionUser;
import com.bell_bell.book.springboot.domain.user.User;
import com.bell_bell.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);
}
}
위 클래스 CustomOAuth2UserService에는 loadUser와 saveOrUpdate 메소드가 존재합니다.
loadUser 메소드는 구글 로그인 이후 호출되게 됩니다. 해당 클래스는SecurityConfig 에서 호출합니다. loadUser가 호출되면 loadUser 내부에는 saveOrUpdate가 실행되게 됩니다. userRequest에는 유저 이름과 아이디에 관한 정보가 포함되 있는 것으로 보이며 해당 정보를 saveOrUpdate 함수에게 전달합니다. saveOrUpdate 는 userRepository에게 해당 유저가 존재하는지 여부를 물어보고 존재 유무는 해당 유저의 이메일로 유저를 찾게 됩니다. orElse의 기능으로 인해서 해당 유저가 없다면 attributes.toEntity() 호출로 인해서 유저 클래스에 값을 넘기게 되고 해당 유저는 save 되는 것으로 보입니다.
그냥 간단하게 요약하자면 구글 로그인이 진행되면 해당 클래스에 있는 메소드를 통해서 httpSession에 유저 정보를 저장하고 User table에 유저 정보를 저장하는 거로 보면 됩니다.
import com.bell_bell.book.springboot.domain.user.Role;
import com.bell_bell.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) {
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();
}
}
위 클래스는 dto에 속하는 클래스로 각 계증간 유저 정보를 주고 받을 때 사용됩니다.
toEntity를 자세히 보면 .role(Role.GUEST)로 되어있습니다. 이는 가입할 때 기본 권한을 GUEST로 주기 위해서 role 빌더 값에는 Role.GEUSET를 사용합니다.
package com.bell_bell.book.springboot.config.auth.dto;
import com.bell_bell.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();
}
}
위 클래스가 사용되는 곳을 살펴보겠습니다.
httpSession.setAttribute("user", new SessionUser(user));
위와 같이 사용되고 있고 httpSession에 유저 정보를 저장 시키게 하기 위해서 사용 되는 것으로 보입니다.
httpSession에 유저 정보를 저장할 때 왜 User 클래스 대신 SessionUser를 사용하는 걸까요? httpSession에 User를 담기위해서는 Serialize(직렬화)를 사용해야 되는데 User를 직렬화 하게 되면 나중에 자식 엔티티가 생겨서 관계를 가지게 되면 직렬화 대상에 자식까지 포함되어서 성능 이슈, 부수 효가가 발생할 수 있기 때문입니다.
다음으로 아래 클래스 정보를 알아보겠습니다.
package com.bell_bell.book.springboot.config.auth;
import com.bell_bell.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/**").permitAll()
.antMatchers("/api/v1/**").hasRole(Role.USER.name())
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("/")
.and()
.oauth2Login()
.userInfoEndpoint()
.userService(customOAuth2UserService);
}
}
1. @EnableWebSecurity
- Spring Security 설정들을 활성화시켜 줍니다.
2. csrf().disable().headers().frameOptions().disable()
- h2-console 화면을 사용하기 위해 해당 옵션들을 disable 합니다.
3. authorizeRequests
- URL별 권한 관리를 설정하는 옵션의 시작지점입니다.
- authorizeRequests가 선언되어야만 antMatchers 옵션을 사용할 수 있습니다.
4. andMachers
- url을 지정하고 해당되는 url에게 전체 열람부터 특정 권한을 할당합니다.
- 권한 관리 대상을 지정하는 옵션입니다.
- URL, HTTP 메소드별로 관리가 가능합니다.
- "/"등 지정된 URL들은 permitAll()옵션을 통해 전체 열람 권한을 주었습니다.
- "/api/v1/**" 주소를 가진 api는 USER 권한을 가진 사람만 가능하도록 했습니다.
5. anyRequest
- 설정된 값들 이외 나머지 URL들을 나타냅니다.
- 여기서는 authenticated()을 추가하여 나머지 URL들은 모두 인증된 사용자들에게 만 허용하게 합니다.
- 인증된 사용자 즉, 로그인한 사용자들을 이야기합니다.
6. logout().logoutSuccessUrl("/")
- 로그아웃 기능에 대한 여러 설정의 진입점입니다.
- 로그아웃 성공 시 / 주소로 이동합니다.
7. oauth2Login
- OAuth 2 로그인 기능에 대한 여러 설정의 진입점입니다.
8. userInfoEndpoint
- OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정들을 담당합니다.
9. userService
- 소셜 로그인 성공 시 후속 조치를 진행할 UserService 인터페이스의 구현체를 등록합니다. 리소스 서버(즉, 소셜 서비스들)에서 사용자 정보를 가져온 상태에서 추가로 진행하고자 하는 기능을 명시할 수 있습니다.