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 로그인 흐름
http://localhost:8080/oauth2/authorization/google
로 접속- Google Server는 로그인 페이지 응답
- ID, PWD 입력 시, Google Server에서 인증 시도
- 인증 성공 시, Client로 Authentication token 발급 & Redirect
- 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가 찍히는지 확인해보자.
- 로그인 성공 시(콘솔에 찍힌 것을 가져옴)
{ sub=xxxxxxxxxxxxxxx, name=유경종, given_name=경종, family_name=유, picture=https:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, email=xxxxxxxxxxxxx@gmail.com, email_verified=true, locale=ko }
올바르게 유저 정보가 넘어온 것을 확인할 수 있다.
이 정보를 이용해 회원 정보가 DB에 있다면 로그인을 진행하며 Token을 발급하고, DB에 없다면 강제로 회원가입을 진행시켜준 뒤, 로그인과 Token 발급을 진행하려고 한다.
일반 로그인 흐름
- 회원가입이 먼저
POST :http://localhost:8080/auth/signup
요청할 때,@RequestBody
, JSON 형식으로 가입 정보를 담아서 요청 { "username" : "유경종", "nickname" : "YKJ", "email" : "xxxxxx@gmail.com", "password" : "123456" }
- DB에 회원 정보가 있는지 확인, 없다면 회원가입을 진행
- 회원가입 정보를 통해 로그인
일반 로그인 :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 |
---|