본문 바로가기
Backend/Spring

[Spring] Spring Security + OAuth2.0으로 소셜 로그인 구현 ver 1. SSR (OAuth2.0 / 구글 로그인 / 네이버 로그인 / 카카오 로그인 / 회원가입)

by 비전공자 기록광 2023. 1. 20.
반응형

2023.01.09 - [Backend/Spring] - [Spring] Spring Security 기본 개념 (JWT / OAuth2.0 / 동작 방식 / 구성 요소)

 

[Spring] Spring Security 기본 개념 (JWT / OAuth2.0 / 동작 방식 / 구성 요소)

JWT (Jason Web Token) 유저 인증, 식별하기 위한 토큰 기반의 인증 구조 헤더 (Header) 타입 (type) : 항상 JWT 알고리즘 (alg) 페이로드 (Payload) : 사용자 정보 담김 서명 (Verify Signature) 동작 방식 1. 클라이언

datamoney.tistory.com

 

개인 프로젝트를 진행하며 개념도 정리도 다시하고 있다.

스프링 시큐리티는 거의 3번째 공부하고 있지만 여전히 복잡하다.

 

이번에는 SSR에서 OAuth2.0 로그인하는 방법을 블로깅해보겠다.

SSR로는 Thymleaf를 쓰겠다.

 

프로젝트 구성은 Spring Boot + Thymleaf가 되겠다.

스프링 시큐리티와 OAuth를 쓰기 위해 라이브러리를 추가해줬다.

 

Maven

<dependency>
    <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-oauth2-client</artifactId>
   <version>5.7.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>3.0.1</version>
</dependency>

 

Gradle

implementation 'org.springframework.security:spring-security-oauth2-client:5.7.5'
compileOnly "org.springframework.boot:spring-boot-starter-security"

 

이 외에  Spring Boot Starter web, JPA와 lombok을 추가해줬다.

 

 

Google Login API 생성

https://console.cloud.google.com/projectselector2/apis/dashboard?supportedpurview=project 

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

구글 개발자 콘솔 중 API 및 서비스에서 프로젝트를 하나 만들어준다.

 

사용자 인증정보를 만들어준다.

애플리케이션 유형을 '웹 애플리케이션'으로,

이름은 그냥 내 프로젝트 이름으로 했다.

 

redirection url은 http://localhost:8080/login/oauth2/code/google

 

 

메뉴의 OAuth 동의 화면을 외부로 만들어준다.

일단 필수 입력 *표시된 정보를 입력하고 넘어가준다.

 

그 다음 scope는 우리가 인증할때 받아오는 정보의 범위를 말한다. 미리 정해준다.

email, profile을 골랐다.

 

client-id, client-secret key 를 복사해둔다.

이따 application.yaml에 적어줄 것임

 

 

Naver Login API 생성

https://developers.naver.com/products/login/api/api.md

 

네이버 로그인 - INTRO

환영합니다 네이버 로그인의 올바른 적용방법을 알아볼까요? 네이버 로그인을 통해 신규 회원을 늘리고, 기존 회원은 간편하게 로그인하게 하려면 제대로 적용하는 것이 중요합니다! 이에 올바

developers.naver.com

scope를 선택한다.

 

 

서비스 url은 http://localhost:8080/

redirection url은 http://localhost:8080/login/ouath2/code/naver

로 설정해줬다.

 

내 애플리케이션에서 client-id와 client secret을 복사해둔다.

 

 

Kakao Login API 생성

https://developers.kakao.com/product/kakaoLogin

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

카카오는 애플리케이션을 먼저 추가해주고 나머지를 설정해준다.

 

카카오 로그인을 사용할 것이니 켜주고 가져올 scope도 정해준다.

 

보안에서 카카오 로그인도 활성화해주고 코드도 복사해둔다. client secret으로 사용될 것이다.

client-id는 요약 정보의 rest-api 키가 될 것임

 

redirect url도 설정해준다.

 

 

이제 이 정보들을 application.yaml에 저장해준다.

 

application.yaml

 

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
spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: [client-id]
            client-secret: [client-secret]
            scope:
              - profile
              - email
          naver:
            client-id: [client-id]
            client-secret: [client-secret]
            client-name: Naver
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            authorization-grant-type: authorization_code
            scope:
              - name
              - email
          kakao:
            client-id: [client-id]
            client-secret: [client-secret]
            client-name: Kakao
            client-authentication-method: POST
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            authorization-grant-type: authorization_code
            scope:
              - profile_nickname
              - account_email
 
        provider:
          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize
            token-uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user-name-attribute: response
          kakao:
            authorizationUri: https://kauth.kakao.com/oauth/authorize
            tokenUri: https://kauth.kakao.com/oauth/token
            userInfoUri: https://kapi.kakao.com/v2/user/me
            userNameAttribute: id
cs
 

줄바꿈, 띄어쓰기에 주의한다.

 

kakao에서 authentication-method: POST 설정을 안해주면 에러가 난다.

[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: 401 Unauthorized: [no body]

 

이제 본격적으로 OAuth 설정에 들어간다.

기본 화면부터 만들어준다.

resource > templates에 index.html 생성해준다.

 

 

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>Hello Spring Boot</h1>
    <a href="/oauth2/authorization/google" class="btn btn-success active" role="button">Google Login</a>
    <a href="/oauth2/authorization/naver" class="btn btn-secondary active" role="button">Naver Login</a>
    <a href="/oauth2/authorization/kakao" class="btn btn-secondary active" role="button">Kakao Login</a>
</body>
</html>
cs

 

 

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
@Configuration
@EnableWebSecurity
public class SecurityConfig { //WebSecurityConfigurerAdapter was deprecated
 
    private final CustomOAuth2UserService customOAuth2UserService;
 
    public SecurityConfig(CustomOAuth2UserService customOAuth2UserService) {
        this.customOAuth2UserService = customOAuth2UserService;
    }
 
    @Bean
    public BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .formLogin().disable()
                .httpBasic().disable()
                .authorizeRequests()
                .antMatchers("/api/user").permitAll()
                .and()
                .oauth2Login().userInfoEndpoint().userService(customOAuth2UserService);
        return http.build();
    }
}
 
cs

oauth2Login 요청이 들어오면 

userInfoEndpoint로 접근해 구현해낼 구현체 (CustomOAuth2UserService)를 등록해준다.

 

 

⭐️⭐️⭐️ 2023.12.16 추가

Spring Boot 3.x 에서는 설정이 바뀌었다. 아래 포스팅에서 확인하세요...

 

[Spring] Spring Boot 3.x SecurityConfig 설정 (Spring Security + OAuth2.0)

업무를 하며 오랜만에 spring security를 다시 만지게 됐다. 신규 프로젝트라 내 마음대로 셋팅을 할 수 있어 기존 많이 쓰던 Java 11 + Spring Boot 2.x 대신 Java 17 + Spring Boot 3.x로 셋팅했다. 그리고 별생각

datamoney.tistory.com

 

 

CustomOAuth2UserService

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
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
 
    private final UserRepository userRepository;
    private final HttpSession httpSession;
 
    public CustomOAuth2UserService(UserRepository userRepository, HttpSession httpSession) {
        this.userRepository = userRepository;
        this.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());
 
        Users user = saveOrUpdate(attributes);
        httpSession.setAttribute("user"new SessionUser(user));
 
        return new DefaultOAuth2User(
                Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
                attributes.getAttributes(),
                attributes.getNameAttributeKey()
        );
    }
 
    private Users saveOrUpdate(OAuthAttributes attributes) {
        Users user = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName()))
                .orElse(attributes.toEntity());
 
        return userRepository.save(user);
    }
}
cs

 

여기에서는 요청에서 들어온 Provider을 구분해주고 유저 정보에 접근한다.

유저 정보는 OAuthAttributes라는 이름에서 Provider별로 Scope에 따라 정보를 담아 준다.

 

그리고 정보에 따라 유저를 생성해주고 세션도 설정해준다.

 

유저는 간단히 id, name, email, password, role만 가진다.

 

Users

더보기
@Entity
@Getter
@NoArgsConstructor
public class Users {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;
    private String password;
    @Enumerated(EnumType.STRING)
    private Role role;

    @Builder
    public Users(Long id, String name, String email, String password, Role role) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.password = password;
        this.role = role;
    }

    public Users update(String name) {
        this.name = name;
        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }
}

 

UserRepository

더보기
public interface UserRepository extends JpaRepository<Users, Long> {
    Optional<Users> findByEmail(String email);
}

 

 

 

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
@Getter
@Builder
public class OAuthAttributes {
    private Map<String, Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
 
    public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
        if("naver".equals(registrationId)) {
            return ofNaver("id", attributes);
        } else if ("kakao".equals(registrationId)) {
            return ofKakao("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"))
                .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"))
                .attributes(response)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }
 
    private static OAuthAttributes ofKakao(String userNameAttributeName, Map<String, Object> attributes) {
        Map<String, Object> response = (Map<String, Object>) attributes.get("kakao_account");
Map<String, Object> account = (Map<String, Object>) attributes.get("profile");
 
        return OAuthAttributes.builder()
                .name((String) account.get("nickname"))
                .email((String) response.get("email"))
                .attributes(response)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }
 
    public Users toEntity() {
        return Users.builder()
                .name(name)
                .email(email)
                .role(Role.USER)
                .build();
    }
}
 
cs
 
 
 

 

OAuth2User의 return이 map 형태이기 때문에 변환이 필요하다.

여기서 provider에 따라 scope 이름에 따라 뽑아준다.

 

 

이렇게 하고 서버를 실행해주면

구글 로그인도

 

네이버 로그인도

 

카카오 로그인도

 

잘 들어가진다.

로그인 역시 잘 되고 이후 리다이랙션도 원래 index로 잘 넘어가진다.

 

 

 

DB에도 저장이 잘 되는 것을 볼 수 있다.

view단에 넘겨주고 싶다면 login유저 dto를 만들어서 세션에 담긴 정보를 넘겨줘도 된다.

 

이로써 동작 과정은 이렇다.

 

1. 소셜 로그인 요청

2. SecurityConfig에서 oauth2Login 요청 들어온 걸 캐치 ( default path : /oauth2/authorization/{registrationId} )

3. application.yaml에 작성한 secreat-id, secreat-key, redirect-url 속성을 가지고 권한 부여 요청

4. 소셜 로그인 진행

5. 로그인 후 유저 정보 생성

 

 

내 프로젝트는 vue.js를 사용하므로 다음에는 SPA에서 구현해보겠다.

 

 

 

 


코드

https://github.com/recordbuffer/Custom-er

 

GitHub - recordbuffer/Custom-er: 커스텀머-고객이 원하는 이미지로 직접 디자인해서 구매할 수 있는 사

커스텀머-고객이 원하는 이미지로 직접 디자인해서 구매할 수 있는 사이트. Contribute to recordbuffer/Custom-er development by creating an account on GitHub.

github.com

 

참고

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 | 이동욱 - 교보문고 (kyobobook.co.kr)

 

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 | 이동욱 - 교보문고

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 | 가장 빠르고 쉽게 웹 서비스의 모든 과정을 경험한다. 경험이 실력이 되는 순간!이 책은 제목 그대로 스프링 부트와 AWS로 웹 서비스를 구현합니다

product.kyobobook.co.kr

Authorization Grant Support :: Spring Security

 

Authorization Grant Support :: Spring Security

This section describes Spring Security’s support for authorization grants.

docs.spring.io

 

반응형

댓글