Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

DOing

[Spring Security] 소셜 로그인 - 구글 본문

Spring

[Spring Security] 소셜 로그인 - 구글

mangdo 2021. 4. 10. 23:46

1. 구글 클라우드 플랫폼에서 인증정보 발급받기

 

구글 서비스에 신규 서비스를 생성하고

발급된 인증정보(clientId와 clientSecret)을 통해서 로그인 기능과 소셜 서비스 기능을 사용할 수 있으니 발급받아야한다.

OAuth 클라이언트 ID를 발급

OAuth 클라이언트 ID를 발급받는다. 그전에 동의화면을 만들어야함으로 동의화면 페이지로 넘어간다.

OAuth 동의화면을 만든다.

외부로 만들어주자!

동의화면을 모두 만들었다면 다시 OAuth 클라이언트 ID를 발급페이지로 간다.

OAuth 클라이언트 ID를 발급

애플리케이션유형을 웹으로 지정하고 승인된 리다이렉션 URL을 설정해준다.

승인된 리다이렉션 URL은 인증 성공시에 구글에서 리다이렉트할 URL이다. 스프링부트2 버전의 시큐리티에서는 기본적으로 {도메인}/login/oauth2/code/{소셜서비스코드}로 리다이렉트 URL을 지원하고 있다. 즉, 사용자가 별도로 리다이렉트 URL을 지원하는 Controller를 만들필요가 없다. 다 만들고 나면 다음과 같이 클라이언트 ID를 발급받을 수 있다. 아이디를 클릭하여 클라이언트 ID클라이언트 보안비밀 코드를 확인한다.

OAuth 클라이언트 ID 확인

2. application-oauth 등록

[applicaion-google.properties]

spring.security.oauth2.client.registration.google.client-id = (클라이언트ID)
spring.security.oauth2.client.registration.google.client-secret = (클라이언트 보안비밀)
spring.security.oauth2.client.registration.google.scope = profile, email
scope=profile,email

 scope의 기본값은 openid, profile, email이다.

 openid라는 scope가 있으면 Open Id Provider로 인식한다. OpenId Provider에는 구글, 깃허브, 페이스북, 옥타가 있다. 이렇게되면 OpenId Provider인 구글과 그렇지않은 서비스(네이버, 카카오)로 나눠 각각 OAuth2Service를 만들어야한다. 하나의 OAuth2Service를 사용하기 위해 scope를 명시하였다.

 

[applicaion.properties]

spring.profiles.include = google

 스프링 부트에서는 properties의 이름을 application-xxx.properties로 만들면 xxx라는 profile이 생성되어 이를 통해 관리를 할 수 있다. 즉, profile=xxx라는 식으로 호출하면 해당 properties의 설정들을 가져올 수 있다. 호출하는 방식이 여러가지 있지만 여기서는 application.properties에서 application-oauth.properties를 포함하도록 구성한다.

 

3. User와 Role 도메인 생성

@Getter
@RequiredArgsConstructor
public enum Role {
    GUEST("ROLE_GUEST", "손님"),
    USER("ROLE_USER","일반 사용자");
    
    private final String key;
    private final String title;
}

 

@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) //JPA에서 DB로 저장할때 Enum값을 문자열 형태로 저장
    @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();
    }
}

 

4. UserRepository 생성

UserRepository에서는 User의 CRUD를 책임진다.

public interface UserRepository extends JpaRepository<User, Long> {
    
    // email을 통해 등록된 사용자인지 신규 가입자인지 확인
    Optional<User> findByEmail(String email);
    
}

 

5. 스프링 시큐리티 의존성 추가

compile('org.springframework.boot:spring-boot-starter-oauth2-client')

6. SecurityConfig 생성

SecurityConfig 는 WebSecurityConfigurerAdapter 클래스를 상속받아서 시큐리티 관련 설정을 해줄 클래스이다.
@RequiredArgsConstructor
@EnableWebSecurity //spring security 설정 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomOAuth2UserService customOAuth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable().headers().frameOptions().disable() //h2-console화면을 사용하기위해 해당 옵션들 disable
                .and() // URL별 설정 관리
                .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);

    }
}

oauth2Login()

: OAuth2 로그인기능에 대한 여러 설정의 진입점

userInfoEndpoint()

: OAuth2 로그인 이후 사용자 정보를 가져올때의 설정을 담당

userService(customOAuth2UserService)

: 소셜로그인 성공시에 후속조치를 진행할 UserService인터페이스의 구현체를 등록한다. 소셜 서비스에서 가져온 사용자정보를 이용해서 추가적인 기능을 수행한다.

 

7. CustomOAuth2UserService 생성

CustomOAuth2UserService는 소셜 로그인 이후 가져온 사용자의 정보를 기반으로 가입 및 정보 수정, 세션 저장 등의 추가적인 기능을 수행한다.

OAuth2UserService인터페이스를 구현해서 만든다.

OAuth2User : A representation of a user Principal that is registered with an OAuth 2.0 Provider

OAuth2User란 OAuth 2.0 공급자에 등록된 접근주체이다.

userNameAttributeName이나 registrationId는 사실 구글 로그인에서는 필요없는 설정이지만 후에 OpenId Provider가 아닌 다른 소셜서비스(네이버, 카카오)를 고려하여 만들어 놓았다.

@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();

        // OAuth2 로그인 진행시 키가되는 필드값(PK 개념, 네이버는 id, 구글은 sub)
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
                .getUserInfoEndpoint().getUserNameAttributeName();

        // CustomOAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스
        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

        User user = saveOrUpdate(attributes);

        // 세션에 사용자 정보를 저장하기 위해 user를 직렬화 하는 sessionUser
        httpSession.setAttribute("user", new SessionUser(user));

        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey()))
                , attributes.getAttributes()
                , attributes.getNameAttributeKey());
    }

    private User saveOrUpdate(OAuthAttributes attributes){

        // 기존 유저라면 update, 신규 유저라면 User를 생성
        // 기존 유저가 소셜서비스에서 이름을 바꾸거나 사진을 바꾸면 이를 적용
        User user = userRepository.findByEmail(attributes.getEmail())
                    .map(entity->entity.update(attributes.getName(), attributes.getPicture()))
                    .orElse(attributes.toEntity());

        return userRepository.save(user);
    }

}

Collections.singleton?

더보기

Collections.singleton이란 단일 원소 컬렉션을 나타낼때 사용한다.

 

하나의 원소를 전달해야하지만 컬렉션 인터페이스를 사용해야할 경우에 사용된다.
Set의 경우에는 Collections.singleton(T o)를 사용하고

List와 Map의 경우에는 Collections.singletonList(T o)를 사용한다.

8. OAuthAttribute 생성

OAuthAttribute는 OAuth2User의 attribute를 담을 클래스이다.

OAuth2User에서 반환하는 사용자 정보를 하나씩 저장했다가 처음 가입하는 회원이라면 User를 생성해준다.

@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;
    }

    // OAuth2User에서 반환하는 사용자 정보는 Map 이기때문에 값을 하나씩 변환
    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();
    }
    
    // 처음 가입시에 User 엔터티 생성
    public User toEntity(){
        
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.GUEST) // 기본 권한은 GUEST
                .build();
    }
}

 

8. SessionUser생성

User를 세션에 저장하기위해 User를 직렬화하는 클래스

User클래스를 그대로 세션에 저장할 수는 없고 직렬화를 시켜서 세션에 저장해야한다.

그렇다고 엔티티의 User클래스에 직렬화를 구현하게되면 추후에 문제가 발생될 수 있다. User클래스가 자식 엔터티를 가지게 되면 직렬화 대상에 자식도 포함되어 성능이슈, 부수효과가 발생할 확률이 높기때문이다. 그래서 직렬화 기능을 가진 세션 DTO를 추가로 생성하는 것이 운영 및 유지 보수시에 효과적이다.

@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();
    }
}

 

9. controller 수정

model.addAttribute("username", user.getName());후에 화면단에 username을 불러오려고하면,

model에 넣어준 username이 나오는 것이 아닌 윈도우의 %userName% 결과가 나온다.

때문에 username이나 name이 아닌 아예 다른 이름을 사용해야한다!

    @GetMapping("/")
    public String index(Model model){
        model.addAttribute("posts", postsService.findAllDesc());

        SessionUser user = (SessionUser) httpSession.getAttribute("user");
        if (user!=null){
            model.addAttribute("socialName",user.getName());
        }
        return "index";
    }

10. 로그인 화면단

{{#socialName}}
	Logged in as : <span id="user">{{socialName}}</span>
	<a href="/logout" class="btn btn-info active" role="button">Logout</a>
{{/socialName}}
{{^socialName}}
	<a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
{{/socialName}}

 

로그인 전 화면
로그인 화면
로그인 후 화면

 

 

 

 

출처:

스프링 부트와 AWS로 혼자 구현하는 웹서비스(이동욱 저)

godekdls.github.io/Spring%20Security/oauth2/