[Spring] Spring Security + OAuth2.0 + Jwt 예제 구현
이전 포스팅의 구현 코드를 기반으로 예제를 구현했으니 참고해주세요
[Spring] Spring Security 와 Jwt 를 이용한 회원가입, 로그인 구현
코드 구현 설계 이번에 제가 만들 프로젝트에서는 일반 로그인 (이메일과 비밀번호를 이용한 로그인) 과 소셜로그인 (구글 로그인, 애플 로그인) 각각 두가지 방식으로 로그인을 진행할 수 있게
comumu.tistory.com
프로젝트 설정
라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
OAuth2.0 관련 기능을 구현하기 위해 다음과 같이 라이브러리를 build.gradle 파일에 추가합니다.
이외에 Google Cloud 에서 client-id 와 client-secret 를 발급받아 application.yml 파일에 추가해 줘야 하는데 발급하는 과정은 생략하겠습니다.
발급받은 key 추가
발급받은 client-id 와 client-secret 를 application.yml 파일에 추가합니다.
spring:
security:
oauth2:
client:
registration:
google:
client-id: [발급받은 Client Id]
client-secret: [발급 받은 Client Secret]
scope: profile, email
OAuth 2.0 관련 클래스
파일 구조
oauth 패키지를 만들어 OAuth2.0 기능을 구현하는데 필요한 클래스들을 다음과 같이 설정했습니다. 코드에 대해 자세히 설명하기 전 각각의 클래스들이 어떤 역할을 가지고 있는지 간략하게 설명하겠습니다.
CustomOAuth2UserService : OAuth 로그인 로직을 담당하는 기능 구현
CustomOAuth2User : OAuth2UserService 에서 사용할 OAuth2 로그인용 클래스
OAuthAttributes : 각 소셜별로 받아오는 데이터가 다르므로 소셜별로 데이터를 처리하는 DTO 클래스
OAuth2UserInfo : 각 소셜 타입별로 유저 정보를 가지는 추상 클래스
GoogleOAuth2UserInfo : Google 에서 받아올 유저 정보를 정의한 DTO
OAuth2LoginFailureHandler : OAuth 로그인이 실패했을때 작동하는 기능 구현
OAuth2LoginSuccessHandler : OAuth 로그인이 성공했을때 작동하는 기능 구현
🎯 CustomOAuth2User
package security.account.oauth;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import security.account.domain.Role;
import java.util.Collection;
import java.util.Map;
@Getter
public class CustomOAuth2User extends DefaultOAuth2User {
private String email;
private Role role;
public CustomOAuth2User(Collection<? extends GrantedAuthority> authorities,
Map<String, Object> attributes, String nameAttributeKey,
String email, Role role) {
super(authorities, attributes, nameAttributeKey);
this.email = email;
this.role = role;
}
}
OAuth 로그인을 할때 사용자 인증에 사용되는 객체 입니다. OAuth 인증에 사용되는 객체를 구현할때는 기본적으로 DefaultOAuth2User 를 상속받아 구현합니다. email 과 role 필드를 추가해 커스텀했습니다.
만약 email 과 role 과 같은 추가 정보가 필요 없다면 DefaultOAuth2User 를 그대로 사용해도 괜찮습니다.
🎯 OAuthAttributes
package security.account.oauth;
import lombok.Builder;
import lombok.Getter;
import security.account.domain.Role;
import security.account.domain.SocialType;
import security.account.domain.User;
import security.account.oauth.userinfo.GoogleOAuth2UserInfo;
import security.account.oauth.userinfo.OAuth2UserInfo;
import java.util.Map;
import java.util.UUID;
@Getter
public class OAuthAttributes {
private String nameAttributeKey;
private OAuth2UserInfo oauth2UserInfo;
@Builder
private OAuthAttributes(String nameAttributeKey, OAuth2UserInfo oauth2UserInfo) {
this.nameAttributeKey = nameAttributeKey;
this.oauth2UserInfo = oauth2UserInfo;
}
public static OAuthAttributes of(SocialType socialType,
String userNameAttributeName, Map<String, Object> attributes) {
// if (socialType == SocialType.GOOGLE) {
// return ofNaver(userNameAttributeName, attributes);
// }
// if (socialType == SocialType.APPLE) {
// return ofKakao(userNameAttributeName, attributes);
// }
return ofGoogle(userNameAttributeName, attributes);
}
public static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
return OAuthAttributes.builder()
.nameAttributeKey(userNameAttributeName)
.oauth2UserInfo(new GoogleOAuth2UserInfo(attributes))
.build();
}
public User toEntity(SocialType socialType, OAuth2UserInfo oauth2UserInfo) {
return User.builder()
.socialType(socialType)
.socialId(oauth2UserInfo.getId())
.email(UUID.randomUUID() + "@socialUser.com")
.nickName(oauth2UserInfo.getNickname())
.role(Role.NORMAL)
.build();
}
}
nameAttributeKey : OAuth 로그인 진행 시 키가 되는 필드값입니다.
oauthUserInfo : 소셜타입별로 로그인 유저 정보를 가진 객체입니다.
of 메서드 : 정적 팩토리 메서드 패턴으로 구현했고, SocialType 별로 필요한 데이터를 추출해 객체를 생성합니다. 하지만 여기선 Google OAuth2 로그인만 다루기 때문에 소셜타입별로 데이터를 추출하는 과정은 생략했습니다.
toEntity 메서드 : 자신의 DTO 인스턴스를 이전 포스팅에서 구현한 User 엔티티로 변환하는 기능입니다. 단 여기서 email 을 UUId 로 생성한 이유는 Jwt 토큰을 발급하기 위한 용도이므로 임의로 설정하기 위함입니다.
🎯 OAuth2UserInfo
package security.account.oauth.userinfo;
import java.util.Map;
public abstract class OAuth2UserInfo {
protected Map<String, Object> attributes;
public OAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
public abstract String getId();
public abstract String getNickname();
}
소셜 타입별로 유저 정보를 갖는 추상 클래스입니다.
🎯 GoogleOAuth2UserInfo
package security.account.oauth.userinfo;
import java.util.Map;
public class GoogleOAuth2UserInfo extends OAuth2UserInfo {
public GoogleOAuth2UserInfo(Map<String, Object> attributes) {
super(attributes);
}
@Override
public String getId() {
return (String) attributes.get("sub");
}
@Override
public String getNickname() {
return (String) attributes.get("name");
}
}
🎯 CustomOAuth2UserService
package security.account.oauth.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.OAuth2User;
import org.springframework.stereotype.Service;
import security.account.domain.SocialType;
import security.account.domain.User;
import security.account.oauth.CustomOAuth2User;
import security.account.oauth.OAuthAttributes;
import security.account.repository.UserRepository;
import java.util.Collections;
import java.util.Map;
//OAuth2 의 로그인 로직을 담당
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.info("OAuth2 로그인 요청 진입");
/**
* DefaultOAuth2UserService 객체를 생성해 .loadUser() 메서드 를 이용해 OAuth 서비스에서 사용자 정보를 가져온다(OAuth2User)
*/
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
/**
* userRequest 는 oauth2/authorization/google 다음과 같은 요청이 들어온다.
*/
String registrationId = userRequest.getClientRegistration().getRegistrationId(); // getRegistrationId() 를 이용하면 userRequest 에서 google 부분을 추출한다.
SocialType socialType = getSocialType(registrationId);
/**
* userNameAttributeName 는 social 종류 별로 다르게 들어온다. 소셜 식별 값 : 구글 : "sub", 카카오 : "id", 네이버 : "id"
*/
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
// 소셜로그인 API 가 제공하는 사용자 Json 정보
Map<String, Object> attributes = oAuth2User.getAttributes();
OAuthAttributes extractAttributes = OAuthAttributes.of(socialType, userNameAttributeName, attributes);
User createUSer = getUser(extractAttributes, socialType);
// CustomOAuth2User 를 생성해서 반환한다. 만약 여기까지 코드가 정상 실행되면 OAuth2LoginSuccessHandler 가 실행되게 SecurityConfig 에서 설정할 예정이다.
return new CustomOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(createUSer.getRole().getKey())),
attributes,
extractAttributes.getNameAttributeKey(),
createUSer.getEmail(),
createUSer.getRole()
);
}
/**
* 소셜 타입에 따라 생성한 OAuthAttributes 와 SocialType 을 이용해 DB 에서 사용자를 조회한다.
* 만약 DB 에 사용자가 없으면 사용자를 DB 에 저장한다.
*/
private User getUser(OAuthAttributes extractAttributes, SocialType socialType) {
User findUser = userRepository.findBySocialTypeAndSocialId(socialType,
extractAttributes.getOauth2UserInfo().getId()).orElse(null);
if(findUser == null) {
return saveUser(extractAttributes, socialType);
}
return findUser;
}
private User saveUser(OAuthAttributes extractAttributes, SocialType socialType) {
User createdUser = extractAttributes.toEntity(socialType, extractAttributes.getOauth2UserInfo());
return userRepository.save(createdUser);
}
private SocialType getSocialType(String registrationId) {
// 만약 소셜 타입이 추가된다면 registrationId 을 이용한 소셜타입을 필터링 하는 로직 추가
return SocialType.GOOGLE;
}
}
CustomOAuth2UserService 클래스는 OAuth2 로그인 로직을 담당합니다.
OAuth2UserService<OAuth2UserRequest, OAuth2User> 를 상속받아 loadUser 를 구현하면 loadUser 안에 OAuth2 로직을 구현하면 됩니다.
loadUser 안에 구현한 로그인 로직이 모두 정상 작동하면 CustomOAuth2User 를 만들어 반환합니다. 이때 CustomOAuth2User 클래스는 OAuth2User 를 상속받아 구현한 클래스이기 때문에 반환할 수 있는겁니다.
loadUser 로직이 끝나면 OAuth2LoginSuccessHandler 클래스를 실행해야 하는데 이는 SecurityConfig 에 추가하면됩니다(아래에서 다시한번 설명드리겠습니다.)
🎯 OAuth2LoginSuccessHandler
package security.account.oauth.handler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.parameters.P;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import security.account.domain.Role;
import security.account.filter.JwtAuthenticationFilter;
import security.account.oauth.CustomOAuth2User;
import security.account.repository.UserRepository;
import security.account.service.JwtService;
import org.springframework.security.core.Authentication;
import java.io.IOException;
@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler implements AuthenticationSuccessHandler {
private final JwtService jwtService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("OAuth2 Login 성공");
try {
//CustomOAuth2UserService 에서 반환한 CustomOAuth2User 를 가져온다.
CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();
if (oAuth2User.getRole() == Role.NORMAL) {
String accessToken = jwtService.createAccessToken(oAuth2User.getEmail());
response.addHeader(jwtService.getAccessHeader(), "Bearer " + accessToken);
response.sendRedirect("oauth2/sign-up"); // 프론트쪽의 회원가입 추가 정보 입력 폼으로 리다리렉트
jwtService.sendAccessAndRefreshToken(response, accessToken, null);
return;
} else {
loginSuccess(request, response, oAuth2User);
}
} catch (Exception e) {
throw e;
}
}
private void loginSuccess(HttpServletRequest request, HttpServletResponse response, CustomOAuth2User oAuth2User) {
String refreshToken = extractRefreshToken(request);
if (refreshToken != null) {
createOnlyAccessToken(oAuth2User, response);
return;
}
if (refreshToken == null) {
createAccessTokenAndRefreshToken(oAuth2User, response);
return;
}
}
private String extractRefreshToken(HttpServletRequest request) {
return jwtService.extractRefreshToken(request)
.filter(jwtService::validateToken)
.orElse(null);
}
private void createOnlyAccessToken(CustomOAuth2User oAuth2User, HttpServletResponse response) {
String accessToken = jwtService.createAccessToken(oAuth2User.getEmail());
response.addHeader(jwtService.getAccessHeader(), "Bearer " + accessToken);
jwtService.sendAccessToken(response, accessToken);
log.info("AccessToken 만 발급 = " + accessToken);
}
private void createAccessTokenAndRefreshToken(CustomOAuth2User oAuth2User, HttpServletResponse response) {
String accessToken = jwtService.createAccessToken(oAuth2User.getEmail());
String updateRefreshToken = jwtService.createRefreshToken();
response.addHeader(jwtService.getAccessHeader(), "Bearer " + accessToken);
response.addHeader(jwtService.getRefreshHeader(), "Bearer " + updateRefreshToken);
jwtService.sendAccessAndRefreshToken(response, accessToken, updateRefreshToken);
jwtService.updateRefreshToken(oAuth2User.getEmail(), updateRefreshToken);
log.info("AccessToken 발급 = " + accessToken);
log.info("RefreshToken 발급 = " + updateRefreshToken);
}
}
OAuth 로그인이 성공했을때 실행시킬 클래스입니다. AuthenticationSuccessHandler 를 상속받아 onAuthenticationSuccess 메서드안에 성공했을때 어떤 처리를 진행할지 구현한 코드입니다.
Role 이 NORMAL 일때는 추가적인 회원 정보(나이, 휴대폰 번호) 등록이 필요합니다. 때문에 리다이렉트 URL 을 클라이언트에게 보내고 클라이언트는 응답받은 URL 로 이동하게끔 구현하면 됩니다.
만약 이미 추가 정보를 등록한 사용자가 로그인을 한다면 loginSuccess 메서드가 실행되는데 RefreshToken 여부를 확인해 AccessToken 만 발행할지 아니면 RefreshToken 도 함께 발행할지를 확인합니다.
🎯 OAuth2LoginFailureHandler
package security.account.oauth.handler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
public class OAuth2LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("소셜 로그인 실패");
log.info("소셜 로그인에 실패했습니다. 에러 메시지 : {}", exception.getMessage());
}
}
OAuth 로그인을 실패했을때 실패 로그와 함께 에러메시지를 확인합니다.
🎯 SecurityConfig
package security.account.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import security.account.filter.CustomLoginAuthenticationFilter;
import security.account.filter.JwtAuthenticationFilter;
import security.account.handler.LoginFailureHandler;
import security.account.handler.LoginSuccessHandler;
import security.account.oauth.handler.OAuth2LoginFailureHandler;
import security.account.oauth.handler.OAuth2LoginSuccessHandler;
import security.account.oauth.service.CustomOAuth2UserService;
import security.account.repository.UserRepository;
import security.account.service.JwtService;
import security.account.service.LoginService;
/**
* 인증은 CustomJsonUsernamePasswordAuthenticationFilter에서 authenticate()로 인증된 사용자로 처리
* JwtAuthenticationProcessingFilter는 AccessToken, RefreshToken 재발급
*/
@Configuration
@EnableWebSecurity // @EnableWebSecurity 어노테이션을 붙여야 Spring Security 기능을 사용할 수 있다.
@RequiredArgsConstructor
public class SecurityConfig {
private final LoginService loginService;
private final JwtService jwtService;
private final UserRepository userRepository;
private final ObjectMapper objectMapper;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin((formLogin) -> formLogin.disable()) // Spring Security 에서는 아무 설정을 하지 않으면 기본 FormLogin 형식의 로그인을 제공한다
.httpBasic((httpBasic) -> httpBasic.disable()) // JWT 토큰을 사용한 로그인 방식이기 때문에 httpBasic disable 로 설정
.csrf((csrfConfig) -> csrfConfig.disable()) // 서버에 인증 정보를 저장하지 않고, 요청 시 인증 정보를 담아서 요청 하므로 보안 기능인 csrf 불필요
.httpBasic((httpBasic) -> httpBasic.disable())
.headers((headerConfig) ->
headerConfig.frameOptions(frameOptionsConfig ->
frameOptionsConfig.disable()
)
)
// 세션 사용 x
.sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/sign-up").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2Login((oAuth2LoginConfigurer) -> oAuth2LoginConfigurer
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler(oAuth2LoginFailureHandler)
.userInfoEndpoint((userInfoEndpointConfig -> userInfoEndpointConfig.userService(customOAuth2UserService))));
http.addFilterAfter(customLoginAuthenticationFilter(), LogoutFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), CustomLoginAuthenticationFilter.class);
return http.build();
}
@Bean
public CustomLoginAuthenticationFilter customLoginAuthenticationFilter() {
//CustomJsonUsernamePasswordAuthenticationFilter 에서 인증할 객체(Authentication) 생성
CustomLoginAuthenticationFilter customJsonUsernamePasswordLoginFilter
= new CustomLoginAuthenticationFilter(objectMapper);
//일반 로그인 인증 로직
customJsonUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
customJsonUsernamePasswordLoginFilter.setAuthenticationSuccessHandler(loginSuccessHandler());
customJsonUsernamePasswordLoginFilter.setAuthenticationFailureHandler(loginFailureHandler());
return customJsonUsernamePasswordLoginFilter;
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//비밀번호 인코딩
provider.setPasswordEncoder(passwordEncoder());
//loginService loadUserByUsername 호출 이때 DaoAuthenticationProvider 가 username 을 꺼내서 loadUserByUsername 을 호출
provider.setUserDetailsService(loginService);
// loadUserByUsername 에서 전달받은 UserDetails 에서 password를 추출해 내부의 PasswordEncoder 에서 password 가 일치하는지 검증을 수행
return new ProviderManager(provider);
}
// jwt 인증필터 빈 등록
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(jwtService, userRepository);
return jwtAuthenticationFilter;
}
// 로그인 성공 시 호출되는 LoginSuccessHandler 빈 등록
@Bean
public LoginSuccessHandler loginSuccessHandler() {
return new LoginSuccessHandler(jwtService, userRepository);
}
// 로그인 실패 시 호출되는 LoginFailureHandler 빈 등록
@Bean
public LoginFailureHandler loginFailureHandler() {
return new LoginFailureHandler();
}
// 패스워드 인코딩을 위한 기능 빈 등록
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
Spring Security Config 설정 클래스입니다. (이전 포스팅 참고)
.oauth2Login((oAuth2LoginConfigurer) -> oAuth2LoginConfigurer
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler(oAuth2LoginFailureHandler)
.userInfoEndpoint((userInfoEndpointConfig -> userInfoEndpointConfig.userService(customOAuth2UserService))));
OAuth2.0 기능이 실행되기 위해 추가한 코드고 성공했을때, 실패했을때 Handler를 각각 넣어줬고 userInfoEndpoint() 부분은 로그인 로직을 담당할 CustomOAuth2UserService 를 넣어줬습니다!
그러면 자연스럽게 CustomOAuth2UserService 안에 있는 loadUser 메서드가 실행되고 만약 정상적으로 모든 로그인 로직이 동작한다면 OAuth2User 객체를 만들어 반환해 OAuth2LoginSuccessHandler 안에 있는 onAuthecticationSuccess 메서드 파라미터의 Authectication 에 담겨 보내지고 (CustomOAuth2User) 형변환을 통해 로그인이 성공했을때의 로직을 실행할 수 있게됩니다!
테스트
(1) http://localhost:8080/login 접속
http://localhost:8080/login 주소로 접속하면 Spring Security 가 기본으로 제공하는 로그인 페이지가 제공됩니다. Google 버튼을 클릭하면 /oauth2/authorization/google 이 호출되며 Google Login 폼이 나옵니다.
(2) DB 확인
제공된 구글 로그인 폼을 모두 통과하면 DB에 다음과 같이 데이터가 들어가는걸 확인할 수 있습니다.
(3) AccessToken 유효성 확인
@GetMapping("/admin/test")
public String testAdmin(){
System.out.println("ADMIN API CALL");
return "ok";
}
UserController 클래스에 다음과 같이 테스트용 api 를 추가합니다.
/admin 으로 시작하는 요청은 Role 이 Admin 일때 허용되게끔 SecurityConfig 클래스에 설정해 줬기때문에 DB 에 사용자 권한을 ADMIN 으로 변경합니다.
이후 다시한번 로그인을 진행하고 Network Response Header 의 Authorization 에 AccessToken 이 잘 들어온걸 확인할 수 있습니다.
발급받은 AccessToken 을 Header 에 담아 요청을 보내 테스트해봅니다. (이때 Bearer 를 토큰 앞에 꼭 추가해줘야 합니다.) 정상적으로 동작하면 ok 라는 응답을 받게 됩니다!