이번에 앱 런칭(Todoary)을 준비하며 보안을 위해 Spring Security를 사용해 보았다.
개발은 Rest API 서버 개발이다.
요구사항은
이다.
Spring의 하위 프레임워크로, Spring 기반으로 애플리케이션의 보안(인증, 권한, 인가)을 담당한다. 인증 및 인가는 주로 Filter의 흐름을 통해 이루어진다.
- Filter : 클라이언트에서 요청이 들어오면 Dispatcher Servlet을 거쳐 알맞은 Controller로 요청이 진행되는데, Dispatcher Servlet에 이르기 전에 Filter를 먼저 거치고, 응답이 나가기 전에도 거치게 된다.
Spring Security에서는 권한(ROLE)을 통해 uri 접근을 제한한다.
http
.authroizeRequests()
.antMathcers("/auth/**").permitAll()
.anyRequest().authenticated();
이런 식으로,
- .authroizeRequests() : 요청에 대해 권한을 체크하겠다.
- .antMathcers("/auth/**") : /auth/** 형식의 uri는 | .permitAll() : 모두 허용한다.
- .anyRequest() : 그 외의 요청들은 | .authenticated() : 인증이 필요하다.
하지만, 유효한 JWT를 통해서만 정보를 응답받을 수 있고, 모든 Role은 User이므로 권한 체크는 생략하기로 한다.
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')
}
Spring Security에서 Security설정을 하기 위해서는 @EnableWebSecurity
어노테이션과
WebSecurityConfigurerAdapter
를 상속 받는 클래스가 필요하다.
@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들이 추가된 것을 확인할 수 있다.
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);
를 추가한 것이다.
언급된 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 발급을 진행하려고 한다.
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(저장된) 일치 여부를 확인할 수 있다.)
DB에 저장될 때, password는 BCrypt 암호화를 거친 모습을 볼 수 있다.
/auth/signin
, /auth/signin/auto
으로 PostMapping된 login
, loginAuto
Bean들은
email과 password를 입력 받아서 먼저 이를 통해 유효한 email-pwd인지 인증authenticate
한다.
유효하다면, Access Token or Access Token & Refresh Token을 발급하게 된다.
일반 로그인은 Access Token만 발급한다.
자동 로그인은 Access Token과 Refresh Token을 발급하고, DB에도 user_id에 맞게 Refresh Token이 저장된 것을 볼 수 있다.
http://localhost:8080/oauth2/authorization/google 에서 로그인에 성공할 경우, OAuth2SuccessHandler 객체가 호출되는데 이 곳에서 Access Token & Refresh Token을 발급하게 된다.
구글 로그인에 성공하면 Access Token과 Refresh Token을 모두 발급하는데 정상적으로 응답 받았고, DB에도 회원정보, Refresh Token이 정상적으로 저장됨을 확인할 수 있다.
자세한 코드는 https://github.com/60jong/Security-Study 참고
안녕하세요! 글 너무 잘 봤습니다.
다름이 아니라 궁금한점이 있는데 OAuth2 로그인 성공시 AccessToken 을 Resource Server 에서 받아오는게 아닌 JwtTokenProvider 를 이용해 AccessToken을 만드는거 같은데 설명하실때
http://localhost:8080/oauth2/authorization/google로 접속
Google Server는 로그인 페이지 응답
ID, PWD 입력 시, Google Server에서 인증 시도
인증 성공 시, Client로 Authentication token 발급 & Redirect
Client는 Authentication token을 이용해 Google Server에 Access Token 발급 요청
다음과 같은 순서로 AccessToken 을 발급받는다고 하셨는데 Google Server 에 AccessToken을 발급요청 하지 않고 자체적으로 만들어진 이유가 혹시 있을까요 ??