Spring/Spring Security

Spring Security + JWT로 구글 로그인 구현하기

60jong 2023. 4. 2. 02:29

Velog에서도 포스팅 중입니다:)

 

Springboot 구글 OAuth2 로그인 + JWT

JWT

velog.io

이번에 앱 런칭(Todoary)을 준비하며 보안을 위해 Spring Security를 사용해 보았다.

개발은 Rest API 서버 개발이다.

요구사항은

  • 일반 로그인 -> 자동 로그인 선택 가능
  • 소셜 로그인 (구글 , 애플) -> 자동 로그인
  • 매 요청마다 JWT (Access Token, Refresh Token) 를 이용한 인증
  • JWT는 DB에 저장되는 유저의 id를 이용한다.
  • Access Token은 2시간, Refresh Token은 90일의 생명주기를 같는다.
  • Refresh Token은 DB에 저장한다.

이다.

Spring Security란?

Spring의 하위 프레임워크로, Spring 기반으로 애플리케이션의 보안(인증, 권한, 인가)을 담당한다. 인증 및 인가는 주로 Filter의 흐름을 통해 이루어진다.

  • Filter : 클라이언트에서 요청이 들어오면 Dispatcher Servlet을 거쳐 알맞은 Controller로 요청이 진행되는데, Dispatcher Servlet에 이르기 전에 Filter를 먼저 거치고, 응답이 나가기 전에도 거치게 된다.
  • 키워드
    • 인증 : Authentication, 권한을 확인
    • 인가 : Authorization, 권한을 부여
    • 권한 : Authority

 

Spring Security에서는 권한(ROLE)을 통해 uri 접근을 제한한다. 

http
        .authroizeRequests()
        .antMathcers("/auth/**").permitAll()
        .anyRequest().authenticated();

이런 식으로, 

    - .authroizeRequests() : 요청에 대해 권한을 체크하겠다.
    - .antMathcers("/auth/**") : /auth/** 형식의 uri는 | .permitAll() : 모두 허용한다.
    - .anyRequest() : 그 외의 요청들은 | .authenticated() : 인증이 필요하다.

하지만, 유효한 JWT를 통해서만 정보를 응답받을 수 있고, 모든 Role은 User이므로 권한 체크는 생략하기로 한다.



프로젝트 구조

  • build.gradle
  • dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' implementation('javax.xml.bind:jaxb-api') implementation('io.jsonwebtoken:jjwt-api:0.11.5') implementation('io.jsonwebtoken:jjwt-impl:0.11.5') implementation('io.jsonwebtoken:jjwt-jackson:0.11.5') }

<br>
<br>

***

# 프로젝트 설명

Spring Security에서 Security설정을 하기 위해서는 `@EnableWebSecurity` 어노테이션과
`WebSecurityConfigurerAdapter`를 상속 받는 클래스가 필요하다.

```java
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    ```
}

 

먼저, @EnableWebSecurity(debug=true)를 통해 현재 Security filter chain을 확인해보자.

Security filter chain: [
  DisableEncodeUrlFilter
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  LogoutFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
]

 

우리는 OAuth2 로그인도 진행할 것이기 때문에 http.oauth2Login()을 추가해보자.

@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();

        http
                .oauth2Login()
                .userInfoEndpoint()
                .userService(principalOAuth2DetailsService);
    }
}

.userInfoEndpoint().userService(principalOAuth2DetailsService)는 밑에서 설명.

Security filter chain: [
  DisableEncodeUrlFilter
  WebAsyncManagerIntegrationFilter
  SecurityContextPersistenceFilter
  HeaderWriterFilter
  LogoutFilter
  OAuth2AuthorizationRequestRedirectFilter
  OAuth2LoginAuthenticationFilter
  DefaultLoginPageGeneratingFilter
  DefaultLogoutPageGeneratingFilter
  RequestCacheAwareFilter
  SecurityContextHolderAwareRequestFilter
  AnonymousAuthenticationFilter
  SessionManagementFilter
  ExceptionTranslationFilter
]

중간에 OAuth2~~로 시작하는 filter들이 추가된 것을 확인할 수 있다.


구글 OAuth2 로그인 흐름

  1. http://localhost:8080/oauth2/authorization/google로 접속
  2. Google Server는 로그인 페이지 응답
  3. ID, PWD 입력 시, Google Server에서 인증 시도
  4. 인증 성공 시, Client로 Authentication token 발급 & Redirect
  5. Client는 Authentication token을 이용해 Google Server에 Access Token 발급 요청

우리는 자체적으로 Access Token과 Refresh Token을 발급 및 관리할 것이기 때문에
위 과정은 생략하고 바로 인증된 OAuth2User 객체를 받아와서 처리한다.

따라서 SecurityConfig

http
                .oauth2Login()
                .userInfoEndpoint()
                .userService(principalOAuth2DetailsService); 

를 추가한 것이다.

  • .oauth2Login() : OAuth2 로그인을 이용한다.
  • .userInfoEndpoint() : 로그인된 유저의 정보를 가져온다.
  • .userService(principalOAuth2DetailsService) : 가져온 유저의 정보를 principalOAuth2DetailsService 객체가 처리한다.

 

DefaultOAuth2UserService

언급된 principalOAuth2DetailsService 객체의 클래스를 먼저 보자.

@Component
public class PrincipalOAuth2DetailsService extends DefaultOAuth2UserService {
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        System.out.println(oAuth2User.getAttributes());

        return oAuth2User;
    }
}

 

PrincipalOAuth2DetailsService 클래스는 DefaultOAuth2UserService클래스를 확장하는데, OAuth2 로그인에 성공하면, OAuth2UserRequest에 유저 정보가 담기게 된다.

따라서 아래 코드를 다시 설명하면,

http
                .oauth2Login()
                .userInfoEndpoint()
                .userService(principalOAuth2DetailsService); 

OAuth2 로그인을 진행하고, 로그인 성공 시에 유저 정보를 가져와 principalOAuth2DetailsService 객체의 loadUser 함수를 호출하며 OAuth2UserRequest에 유저 정보를 담게 된다.

 

그러면 로그인을 진행해 console에 oAuth2User가 찍히는지 확인해보자.

  1. http://localhost:8080/oauth2/authorization/google 접속
  1. 로그인 성공 시(콘솔에 찍힌 것을 가져옴)
  2. { sub=xxxxxxxxxxxxxxx, name=유경종, given_name=경종, family_name=유, picture=https:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, email=xxxxxxxxxxxxx@gmail.com, email_verified=true, locale=ko }

올바르게 유저 정보가 넘어온 것을 확인할 수 있다.

이 정보를 이용해 회원 정보가 DB에 있다면 로그인을 진행하며 Token을 발급하고, DB에 없다면 강제로 회원가입을 진행시켜준 뒤, 로그인과 Token 발급을 진행하려고 한다.


일반 로그인 흐름

  1. 회원가입이 먼저
    POST : http://localhost:8080/auth/signup 요청할 때,
    @RequestBody, JSON 형식으로 가입 정보를 담아서 요청
  2. { "username" : "유경종", "nickname" : "YKJ", "email" : "xxxxxx@gmail.com", "password" : "123456" }
  1. DB에 회원 정보가 있는지 확인, 없다면 회원가입을 진행
  2. 회원가입 정보를 통해 로그인
    일반 로그인 : http://localhost:8080/auth/signin -> Access Token 발급
    자동 로그인 : http://localhost:8080/auth/signin/auto -> Access Token, Refresh Token 발급

구현 내용

 

일반 로그인

회원가입 구현

/auth/signup으로 PostMapping된 join Bean은 회원가입할 때 필요한 정보를 담은 PostSignUpReq라는 DTO 객체를 요청 시에 입력 받는다.

{
    "username" : "유경종",
    "nickname" : "YKJ",
    "email" : "xxxxxx@gmail.com",
    "password" : "123456"
}  

이를 이용해 email로 중복 validation을 체크하고 중복이 없다면, DB에 추가한다. 이때, 비밀번호는 PasswordEncoder로 복호화가 불가능하게 암호화해 저장한다.

(사용자 인증시에는 matches()로 raw pwd(입력한)와 encoded pwd(저장된) 일치 여부를 확인할 수 있다.)

  • Test

DB에 저장될 때, password는 BCrypt 암호화를 거친 모습을 볼 수 있다.

로그인 구현

/auth/signin, /auth/signin/auto으로 PostMapping된 login, loginAuto Bean들은
email과 password를 입력 받아서 먼저 이를 통해 유효한 email-pwd인지 인증authenticate한다.

유효하다면, Access Token or Access Token & Refresh Token을 발급하게 된다.

  • Test (일반 로그인)

일반 로그인은 Access Token만 발급한다.

 

  • Test (자동 로그인)

자동 로그인은 Access Token과 Refresh Token을 발급하고, DB에도 user_id에 맞게 Refresh Token이 저장된 것을 볼 수 있다.


구글 로그인

http://localhost:8080/oauth2/authorization/google 에서 로그인에 성공할 경우, OAuth2SuccessHandler 객체가 호출되는데 이 곳에서 Access Token & Refresh Token을 발급하게 된다.

  • Test


구글 로그인에 성공하면 Access Token과 Refresh Token을 모두 발급하는데 정상적으로 응답 받았고, DB에도 회원정보, Refresh Token이 정상적으로 저장됨을 확인할 수 있다.


자세한 코드는 https://github.com/60jong/Security-Study 참고

'Spring > Spring Security' 카테고리의 다른 글

Spring Security의 Architecture  (0) 2023.04.02