본문으로 바로가기
728x90

Chapter 05

스프링 시큐리티와 OAuth 2.0으로 로그인 기능 구현하기

 

 

구글 서비스 등록

 

- 여기서 발급된 인증정보를 통해서 로그인 기능과 소셜 서비스 기능을 사용할 수 있음

- 구글 클라우드 플랫폼 주소(https://console.cloud.google.com) 이동

- 프로젝트 선택 -> 새 프로젝트 : freelec-springboot2-webservice 생성

 

 

- 왼쪽 메뉴 탭을 클릭해 API 및 서비스 -> 사용자 인증 정보 -> OAuth 클라이언트 ID 클릭

-> 동의 화면 구성

 

 - 애플리케이션 이름을 아까와 같이 작성

 

 

- 다시 OAuth 클라이언트 ID 만들기 화면으로 이동

 

어플리케이션 유형 : 웹 어플리케이션

승인된 리디렉션 URI : http://localhost:8080/login/oauth2/code/google로 설정한 후 저장

 

 

클라이언트 ID와 클라이언트 보안비밀 코드 설정

 

- application-oauth 등록

 

src/main/resources/ 디렉토리에 application-oauth.properties 파일 생성

 

application.properties에 코드 추가

 

 

- .gitignore 등록

 

깃허브에 올라갈 수 있는 보안을 위해 아래 코드를 추가

 

application-oauth.properties

 

구글 로그인 연동하기

 

- domain 아래 user 패키지 -> 사용자 정보를 담당할 User 클래스 생성

 

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.qwon.springboot.domain.user;
 
import com.qwon.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();
    }
}
 
 
 

 

- 각 사용자의 권한을 관리할 Enum 클래스 Role 생성

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.qwon.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를 책임질 UserRepository 생성

 

1
2
3
4
5
6
7
8
9
10
11
12
package com.qwon.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);
}
 
 
 
 

 

 

스프링 시큐리티 설정

 

- build.gradle에 의존성 추가

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

 

- com.qwon.springboot 아래 config.auth 패키지 생성 -> SecurityConfig 클래스 생성

 

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
31
32
33
34
package com.qwon.springboot.config.auth;
 
import com.qwon.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);
    }
}
 
cs

 

 

- CustomOAuth2UserSerivce 클래스 생성 (구글 로그인 이후 가져온 사용자의 정보들을 기반으로 가입 및 정보수정, 세션 저장 등의 기능을 담당)

 

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.qwon.springboot.config.auth;
 
import com.qwon.springboot.config.auth.dto.OAuthAttributes;
import com.qwon.springboot.config.auth.dto.SessionUser;
import com.qwon.springboot.domain.user.User;
import com.qwon.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 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);
    }
}
 
 
 

 

- config/auth 아래 dto 패키지 생성 -> OAuthAttributes 클래스 생성

 

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.qwon.springboot.config.auth.dto;
 
import com.qwon.springboot.domain.user.Role;
import com.qwon.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) {
        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();
    }
}
 
 
 

 

- SessionUser 클래스 생성

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.qwon.springboot.config.auth.dto;
 
import com.qwon.springboot.domain.user.User;
 
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();
    }
 
}
 
 
 

 

로그인 테스트

 

- index.mustache에 로그인 버튼 / 성공 시 사용자 이름이 나오도록 코드 추가

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
....
<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>
    <!-- 목록 출력 영역 -->
    <table class="table table-horizontal table-bordered">
 
....
 

 

- IndexController에 userName을 model에 저장하는 코드 추가

 

 

- 프로젝트 실행해보기

 

여기서 Googole Login을 눌렀는데 에러가 났다.

 

이 에러는 추후 확인해 봐야겠다.

 

- 이 원인을 이틀이 되어서야 해결했다.

  정말 멍청한 짓을 하고 있었다.

 

application-oauth.properties 에서 위와 같이 클라이언트 ID, 보안비밀은 내 개인정보에 관한 내용을 담는 것이었다.

 

정보를 입력한 후, 로그인을 하면 아래와 같이 뜬다.

 

 

 

728x90