// build.gradle
dependencies {
~ 생략 ~
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
쿠키 생성•삭제를 담당하는 쿠키 관리 클래스를 구현한다.
// utill - CookieUtill.java
public class CookieUtil {
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
Cookie cookie = new Cookie(name, value);
cookie.setPath("/");
cookie.setMaxAge(maxAge);
response.addCookie(cookie);
}
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return;
}
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {
cookie.setValue("");
cookie.setPath("/");
cookie.setMaxAge(0);
response.addCookie(cookie);
}
}
}
public static String serialize(Object obj) {
return Base64.getUrlEncoder()
.encodeToString(SerializationUtils.serialize(obj));
}
public static <T> T deserialize(Cookie cookie, Class<T> cls) {
return cls.cast(
SerializationUtils.deserialize(
Base64.getUrlDecoder().decode(cookie.getValue())
)
);
}
}
addCookie()
요청값(이름, 값, 만료 기간)을 바탕으로 HTTP 응답에 쿠키를 추가한다.
cf. 첫 번째 매개변수인 HttpServletResponse response는 해당 메서드가 클라이언트로부터의 HTTP 요청에 대한 응답을 생성하기 위해 사용되는 객체이다.
- HttpServletRequest
해당 인터페이스는 클라이언트로부터의 HTTP 요청에 대한 정보를 담고 있다. 이를 통해 서블릿이나 JSP 페이지에서 클라이언트로부터 전송된 데이터를 읽을 수 있다. 또한 요청의 속성, 헤더, 쿠키, 세션 등의 정보도 가져올 수 있다. 주요 메서드로는 요청의 파라미터를 가져오는 getParameter() 및 파라미터 이름의 모든 값을 가져오는 getParameterValues() 등이 있다.
- HttpServletResponse
해당 인터페이스는 서블릿이나 JSP 페이지에서 클라이언트로 응답을 생성하고 전송하는 데 사용된다. 이를 통해 서버는 클라이언트로부터의 요청에 대해 적절한 응답을 생성하고, 이를 클라이언트에게 보낸다. 주로 HTML 컨텐츠를 생성하거나 리다이렉션을 수행하는 데 사용된다. 주요 메서드로는 응답에 헤더를 추가하는 addHeader(), 쿠키를 추가하는 addCookie(), 리다이렉트하는 sendRedirect() 등이 있다.
- Cookie 클래스
HTTP 쿠키를 나타내는 객체이다. 이 클래스는 javax.servlet.http 패키지에 속해 있다. 쿠키는 클라이언트와 서버 간에 정보를 주고 받을 때 사용된다.
- Cookie 클래스 주요 메서드
- Cookie(String name, String value)
주어진 이름과 값으로 새로운 쿠키를 생성한다.
- setMaxAge(int expiry)
쿠키의 수명을 초 단위로 설정한다. 음수 값은 쿠키를 세션 쿠키로 설정하고, 0 이하의 값은 쿠키를 즉시 삭제한다.
- setPath(String uri)
쿠키의 경로를 설정한다. 이 경로에 속하는 URL만 해당 쿠키를 전송한다.
- getName()
쿠키의 이름을 반환한다.
- getValue()
쿠키의 값 반환한다.
- getPath()
쿠키의 경로를 반환한다.
- getMaxAge()
쿠키의 수명을 반환한다.
deleteCookie()
쿠키 이름을 입력받아 쿠키를 삭제한다. 실제로 삭제하는 방법은 없으므로 파라미터로 넘어온 키의 쿠키를 빈 값으로 바꾸고 만료 시간을 0으로 설정해 쿠키가 재생성 되자마자 만료 처리한다.
request.getCookies()
해당 메서드는 현재 요청에 포함된 모든 쿠키를 배열로 반환한다. 각각의 쿠키는 Cookie 클래스의 인스턴스로 표현되며, 이를 통해 쿠키의 이름, 값, 경로, 만료 시간 등을 확인할 수 있다.
일치하는 쿠키를 찾았을 경우, 해당 쿠키의 값을 빈 문자열로 설정하여 삭제하고, 경로를 "/"로 설정하고, 수명을 0으로 설정하여 만료시킨다.
response.addCookie(cookie)
변경된 쿠키를 응답에 추가하여 클라이언트에게 전달한다.
serialize()
객체를 직렬화해 쿠키의 값으로 들어갈 값으로 변환한다.
SerializationUtils.serialize(obj)
Apache Commons Lang 라이브러리의 SerializationUtils 클래스의 serialize() 메서드를 사용하여 주어진 객체를 바이트 배열로 직렬화한다. 직렬화란 객체의 상태를 저장하고 이를 나중에 다시 복원할 수 있도록 하는 프로세스이다.
Base64.getUrlEncoder().encodeToString(...)
Java 8에서 제공하는 Base64 인코더를 사용하여 직렬화된 바이트 배열을 Base64로 인코딩한다.
cf. Base64 인코딩은 주로 바이너리 데이터를 텍스트 형식으로 변환하는 데 사용된다.
deserialize
쿠키를 역직렬화 객체로 변환한다.
Base64.getUrlDecoder().decode(cookie.getValue())
먼저 주어진 쿠키의 값에서 Base64 디코딩을 수행한다. 이렇게 하면 쿠키에 저장된 직렬화된 객체의 바이트 배열이 얻어진다.
SerializationUtils.deserialize(...): Apache Commons Lang 라이브러리의 SerializationUtils 클래스의 deserialize() 메서드를 사용하여 바이트 배열을 역직렬화하여 객체로 변환한다. 이 과정에서 직렬화된 바이트 배열이 해당 클래스의 인스턴스로 다시 복원된다.
사용자 정보를 조회해 users 테이블에 사용자 정보가 있다면 리소스 서버에서 제공해주는 이름을 업데이트하고 없다면 users 테이블에 새 사용자를 생성해 데이터베이스에 저장하는 서비스를 구현한다.
// domain - User.java
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class User implements UserDetails { // UserDetails를 상속 받아 인증 객체로 사용
~ 생략 ~
// 사용자 이름
@Column(name = "nickname", unique = true)
private String nickname;
// 생성자에 nickname 추가
@Builder
public User(String email, String password, String auth, String nickname) {
this.email = email;
this.password = password;
this.nickname = nickname;
}
~ 생략 ~
// 사용자 이름 변경
public User update(String nickname) {
this.nickname = nickname;
return this;
}
~ 생략 ~
}
// config - oauth - OAuth2UserCustomService.java
@RequiredArgsConstructor
@Service
public class OAuth2UserCustomService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User user = super.loadUser(userRequest); // ❶ 요청을 바탕으로 유저 정보를 담은 객체 반환
saveOrUpdate(user);
return user;
}
// ❷ 유저가 있으면 업데이트, 없으면 유저 생성
private User saveOrUpdate(OAuth2User oAuth2User) {
Map<String, Object> attributes = oAuth2User.getAttributes();
String email = (String) attributes.get("email");
String name = (String) attributes.get("name");
User user = userRepository.findByEmail(email)
.map(entity -> entity.update(name))
.orElse(User.builder()
.email(email)
.nickname(name)
.build());
return userRepository.save(user);
}
}
- DefaultOAuth2UserService
Spring Security OAuth2 모듈에서 제공하는 기본 OAuth2 사용자 클래스이다. 해당 클래스는 OAuth2 프로바이더(예: Google, Facebook 등)로부터 사용자의 인증 및 권한 부여를 처리하는 데 사용된다.
해당 클래스는 OAuth2 프로바이더의 엔드포인트를 호출하여 사용자 정보를 가져오고, 이를 Spring Security의 인증 시스템과 통합한다. 이를 통해 사용자는 OAuth2 프로바이더를 통해 로그인할 수 있고, Spring Security는 해당 사용자를 인증하고 권한을 부여할 수 있다.
- OAuth2UserRequest
OAuth 2.0 프로바이더로부터 사용자 정보를 요청하는 데 사용되는 객체다. Spring Security OAuth 모듈에서 사용된다. 이 객체에는 OAuth 2.0 프로바이더와 관련된 여러 정보가 포함되어 있다.
- OAuth2User 인터페이스는 Spring Security OAuth 모듈에서 OAuth 2.0 프로바이더(예: Google, Facebook 등)로부터 받은 사용자 정보를 나타낸다. 해당 인터페이스는 사용자의 기본적인 정보와 사용자의 추가적인 속성(attribute)과 다음 메서드를 제공한다.
- getName()
사용자의 이름을 반환한다. 일반적으로는 사용자의 실명을 반환한다.
- getAuthorities()
사용자의 권한(권한 부여된 역할) 목록을 반환한다. 해당 권한은 인증된 사용자가 수행할 수 있는 작업을 나타낸다.
- getAttribute(String name)
지정된 이름의 사용자 속성을 반환한다. 일반적으로는 사용자의 이메일 주소, 프로필 사진 URL 등이 포함된다.
- getAttributes()
사용자의 모든 속성을 Map 형태로 반환한다. 이를 통해 필요한 모든 사용자 정보에 접근할 수 있다.
// config - WebOAuthSecurityConfig.java
@RequiredArgsConstructor
@Configuration
public class WebOAuthSecurityConfig {
private final OAuth2UserCustomService oAuth2UserCustomService;
private final TokenProvider tokenProvider;
private final RefreshTokenRepository refreshTokenRepository;
private final UserService userService;
@Bean
public WebSecurityCustomizer configure() { // 스프링 시큐리티 기능 비활성화
return (web) -> web.ignoring()
.requestMatchers(toH2Console())
.requestMatchers("/img/**", "/css/**", "/js/**");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable);
http.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.authorizeHttpRequests(request -> request
.requestMatchers("/api/token").permitAll()
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll());
http.oauth2Login(oauth2Login -> oauth2Login
.loginPage("/login")
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint
.authorizationRequestRepository(oAuth2AuthorizationRequestBasedOnCookieRepository()))
.successHandler(oAuth2SuccessHandler())
.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
.userService(oAuth2UserCustomService)));
http.logout(logout -> logout
.logoutSuccessUrl("/login"));
http.exceptionHandling(exceptionHandling -> exceptionHandling
.defaultAuthenticationEntryPointFor(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new AntPathRequestMatcher("/api/**")));
return http.build();
}
@Bean
public OAuth2SuccessHandler oAuth2SuccessHandler() {
return new OAuth2SuccessHandler(tokenProvider,
refreshTokenRepository,
oAuth2AuthorizationRequestBasedOnCookieRepository(),
userService
);
}
@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter(tokenProvider);
}
@Bean
public OAuth2AuthorizationRequestBasedOnCookieRepository oAuth2AuthorizationRequestBasedOnCookieRepository() {
return new OAuth2AuthorizationRequestBasedOnCookieRepository();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
configure(){~}
웹 보안을 구성하는 메서드이다.
WebSecurityCustomizer
Spring Security에서 웹 보안을 커스터마이징할 수 있는 인터페이스이다. 이 인터페이스를 구현하면 웹 보안 구성을 수정할 수 있다.
web.ignoring()
해당 메서드는 ignoring() 메서드를 호출하여 웹 보안을 무시하도록 설정합니다. 즉, 해당 경로에 대한 보안 검사를 건너뛰게 됩니다.
.requestMatchers()
무시할 요청 매처를 지정한다. 이는 보안 검사를 건너뛸 요청의 패턴을 지정하는 데 사용된다.
toH2Console()
Spring Boot에서 H2 데이터베이스 콘솔에 액세스하는 데 사용되는 메서드이다.
"/img/", "/css/", "/js/**"
정적 자원에 대한 경로를 나타낸다.
filterChain(HttpSecurity http) throws Exception {~}
토큰 방식으로 인증을 하므로 기존 폼 로그인, 세션 기능을 비활성화 한다.
http.csrf(AbstractHttpConfigurer::disable)
CSRF(Cross-Site Request Forgery) 보호를 비활성화 한다. 애플리케이션이 RESTful API를 사용하는 경우에는 CSRF 공격의 위험이 낮기 때문에 보통 해당 기능을 비활성화 한다.
http.httpBasic(AbstractHttpConfigurer::disable)
OAuth 2.0 토큰 기반 인증 방식을 사용하기 때문에 HTTP Basic 인증 방식을 사용할 필요가 없기 때문에 비활성화 한다.
http.formLogin(AbstractHttpConfigurer::disable)
OAuth 2.0 토큰 기반 인증 방식을 사용하기 때문에 폼 로그인 인증 방식을 비활성화 한다.
http.logout(AbstractHttpConfigurer::disable)
로그아웃 기능을 비활성화한다. 클라이언트 측에서 로그아웃을 처리하는 대신에 인증 서버에 로그아웃 요청을 전달하여 세션을 종료하고 토큰을 무효화한다.
http.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
RESTful API를 사용하는 경우에는 세션을 사용하지 않고 상태를 유지하지 않는 stateless한 통신 방식을 사용한다.
addFilterBefore()
헤더값을 확인할 커스텀 필터를 UsernamePasswordAuthenticationFilter 앞에 추가한다. 해당 필터는 9.2.4 '토큰 필터 구현'에서 구현한 TokenAuthenticationFilter 클래스이다.
해당 필터는 액세스 토큰 값이 담긴 Authorization 헤더 값을 가져온 뒤 엑세스 토큰이 유효하다면 인증 정보를 시큐리티 컨텍스트에 저장한다.
출처 : https://chathurangat.wordpress.com/category/spring-security/
토큰 기반의 인증을 먼저 시도하기 위해 해당 필터를 UsernamePasswordAuthenticationFilter 앞에 추가한다.
http.authorizeHttpRequests(~);
Spring Security에서 HTTP 요청에 대한 인가 규칙을 설정하는 메서드이다.
해당 코드에서는 토큰 재발급 URL은 인증 없이 접근하도록 설정하고 나머지 API들은 모두 인증을 해야 접근할 수 있도록 설정한다.
http.oauth2Login(~);
OAuth2 기반 로그인을 구성하는 메서드이다.
loginPage("/login")
사용자가 로그인을 요청할 때 이동할 로그인 페이지의 URL을 설정한다.
authorizationEndpoint()
인가 엔드포인트에 대한 설정을 정의합니다. OAuth 2.0 인가 요청의 설정을 구성한다. 이 설정은 OAuth 2.0 흐름을 제어하고, 클라이언트 애플리케이션에서 인증을 시작할 때 필요한 정보를 관리한다.
authorizationRequestRepository()
OAuth 2.0 인가 요청을 저장하고 관리하는 데 사용되는 리포지토리를 설정한다. 이 리포지토리는 인증 흐름 중에 사용되는 인가 요청을 저장하고, 필요할 때 해당 리포리티지에서 검색한다. 이 경우 oAuth2AuthorizationRequestBasedOnCookieRepository()를 사용하여 쿠키에 기반한 인가 요청 리포지토리를 설정한다.
oAuth2AuthorizationRequestBasedOnCookieRepository()는 OAuth 2.0 인증 요청을 쿠키를 기반으로 저장하고 관리하는 데 사용되는 리포지토리를 생성하는 메서드이다. 해당 리포지토리는 사용자가 OAuth 2.0 인증 프로세스를 시작할 때 생성된 인증 요청을 저장하고, 나중에 해당 요청을 검색하여 처리하는 데 사용된다.
oAuth2AuthorizationRequestBasedOnCookieRepository()는 사용자가 외부 인증 서비스로부터 리디렉션되기 전에 애플리케이션이 사용자의 인증 요청을 저장하고 관리하는 데 사용된다. 해당 메서드를 통해 생성된 리포지토리는 인증 요청을 쿠키에 저장하여 사용자의 브라우저에 유지하고, 사용자가 다시 애플리케이션으로 리디렉션되었을 때 해당 요청을 검색할 수 있다.
successHandler()
OAuth 2.0 로그인 성공 후의 처리를 담당하는 핸들러를 설정한다. 해당 핸들러는 로그인이 성공했을 때 수행할 작업을 정의한다. 일반적으로 사용자 정보를 추출하고, 사용자 세션을 생성하거나 업데이트하는 등의 작업이 여기에서 이루어진다.
userInfoEndpoint()
OAuth 2.0에서 사용자 정보를 가져오는 엔드포인트에 대한 설정을 정의한다. 이 설정은 외부 공급자에서 사용자 정보를 가져오는 데 사용된다. 여기서 userService(oAuth2UserCustomService)를 호출하여 사용자 정보 엔드포인트에 사용자 지정 서비스를 연결한다. 해당 서비스는 사용자 정보를 가져오고 Spring Security가 인증을 처리할 수 있도록 반환한다.
일반적으로 OAuth 2.0 인증 플로우에서 사용자 정보는 인증 서버 또는 자체 서버의 사용자 데이터베이스와 같은 소스에서 제공된다. 해당 정보는 OAuth 2.0 공급자에서 제공되는 토큰에 포함되어 있을 수도 있고, 별도의 엔드포인트를 통해 요청하여 가져올 수도 있다.
Spring Security에서는 userInfoEndpoint를 사용하여 사용자 정보를 가져오는 방법을 구성한다.
http.logout(logout -> logout.logoutSuccessUrl("/login"));
사용자가 로그아웃할 때 로그아웃 성공 후에 로그인 페이지로 리디렉션되도록 설정 한다.
http.exceptionHandling()
/api로 시작하는 url인 경우 인증 실패 시 401 상태 코드, 즉, Unauthorized를 반환한다.
- 해당 메서드는 다음 매개변수들을 갖는다.
- AuthenticationEntryPoint
인증이 실패했을 때 클라이언트에게 반환할 엔트리 포인트를 정의한다. 해당 경우 HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)를 사용하여 HTTP 상태 코드 401(Unauthorized)을 반환한다.
cf. HttpStatusEntryPoint는 인증이 필요한 리소스에 접근할 때 HTTP 상태 코드를 반환하는 인증 진입점이다. 인증 진입점은 사용자가 보호된 리소스에 접근할 때 인증 상태를 확인하고, 인증되지 않았거나 인증이 실패한 경우에는 적절한 응답을 생성하여 클라이언트에게 전달하는 역할을 한다.
- RequestMatcher
특정 요청 패턴에 대한 매처를 지정한다. 해당 매처에 매칭되는 요청에 대해 설정된 인증 진입점이 사용된다. 해당 경우 AntPathRequestMatcher("/api/**
")를 사용하여 /api/** 패턴에 매칭되는 모든 요청에 대해 설정된 인증 진입점을 사용한다.
OAuth2에 필요한 정보를 세션이 아닌 쿠키에 저장해서 쓸 수 있도록 인증 요청과 관련된 상태를 저장할 저장소를 구현한다. 권한 인증 흐름에서 클라이언트의 요청을 유지하는 데 사용되는 OAuth2AuthorizationRequestBasedOnCookieRepository 클래스를 구현해 쿠키를 사용해 OAuth의 정보를 가져오고 저장하는 로직을 작성한다.
// config - oauth - OAuth2AuthorizationRequestBasedOnCookieRepository.java
public class OAuth2AuthorizationRequestBasedOnCookieRepository implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
public final static String OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME = "oauth2_auth_request";
private final static int COOKIE_EXPIRE_SECONDS = 18000;
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
return this.loadAuthorizationRequest(request);
}
@Override
public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
return CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class);
}
@Override
public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest, HttpServletRequest request, HttpServletResponse response) {
if (authorizationRequest == null) {
removeAuthorizationRequestCookies(request, response);
return;
}
CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS);
}
public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
}
}
해당 코드는 OAuth2.0 인증 요청을 쿠키를 사용하여 저장하고 검색하기 위한 Spring Security의 AuthorizationRequestRepository를 구현한 클래스이다.
AuthorizationRequestRepository는 인증 요청을 저장하고 관리하기 위한 인터페이스이다. 해당 인터페이스를 구현하여 클라이언트의 인증 요청을 저장하고 검색할 수 있다.
removeAuthorizationRequest(~){~}
loadAuthorizationRequest 메서드를 호출하여 저장된 인증 요청을 가져와 반환한다.
loadAuthorizationRequest(~){~}
이 메서드는 HTTP 요청을 받아서 저장된 OAuth 2.0 인증 요청을 가져온다. 주어진 요청에서 지정된 이름의 쿠키를 가져와 역직렬화하여 OAuth2AuthorizationRequest 객체로 변환한다. 이렇게 함으로써 이전에 저장된 OAuth 2.0 인증 요청을 복원할 수 있다.
WebUtils.getCookie(request, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME)
주어진 HTTP 요청에서 지정된 이름의 쿠키를 가져온다. 여기서는 OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME 상수에 정의된 이름의 쿠키를 가져오고, 이를 cookie 변수에 저장한다.
CookieUtil.deserialize(cookie, OAuth2AuthorizationRequest.class)
가져온 쿠키를 역직렬화하여 OAuth2AuthorizationRequest 객체로 변환한다. 이를 통해 이전에 저장된 OAuth 2.0 인증 요청을 복원할 수 있다.
saveAuthorizationRequest(~){~}
인증 요청을 저장한다. 해당 메서드는 클라이언트가 인증을 요청할 때 호출되어 인증 요청을 저장한다.
authorizationRequest == null
만약 인증 요청이 null이라면, 즉 저장할 인증 요청이 없다면, removeAuthorizationRequestCookies 메서드를 호출하여 기존에 저장된 인증 요청 쿠키를 삭제하고, 해당 메서드를 종료한다.
CookieUtil.addCookie(response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME, CookieUtil.serialize(authorizationRequest), COOKIE_EXPIRE_SECONDS)
인증 요청이 존재할 경우, 해당 인증 요청을 쿠키로 저장한다. addCookie 메서드는 주어진 응답 객체(response)에 쿠키를 추가하는 역할을 한다. 이를 통해 인증 요청이 쿠키로 저장되며, 이 쿠키는 OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME 상수에 정의된 이름을 가지게 된다. 인증 요청은 CookieUtil.serialize(authorizationRequest)를 통해 직렬화된 후 저장되며, 쿠키의 유효 기간은 COOKIE_EXPIRE_SECONDS 상수에 정의된 시간으로 설정된다.
removeAuthorizationRequestCookies(~){}
CookieUtil.deleteCookie 메서드를 호출하여 주어진 요청과 응답에서 특정 이름의 쿠키를 삭제한다. 여기서 request와 response는 삭제할 쿠키를 포함하고 있는 HTTP 요청과 응답 객체이다. OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME은 삭제할 쿠키의 이름을 나타내는 상수이다.
해당 메서드를 통해 저장된 OAuth 2.0 인증 요청 쿠키를 삭제할 수 있다. 이를 통해 사용자가 새로운 인증 요청을 시작할 때 이전에 저장된 인증 요청 정보가 제거되어 보안과 개인정보 보호를 강화할 수 있다.
인증 성공 시 실행할 핸들러를 구현하기 전에, 해당 빈을 구현할 때 사용할 메서드를 만들기 위해 service 패키지의 UserService.java 파일을 수정한다.
BCryptPasswordEncoder를 삭제하고 BCryptPasswordEncoder를 생성자를 사용해 직접 생성해서 패스워드를 암호화할 수있게 코드를 수정한 다음 findByEmail() 메서드를 추가한다.
// service - UserService.java
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
public Long save(AddUserRequest dto) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
return userRepository.save(User.builder()
.email(dto.getEmail())
.password(encoder.encode(dto.getPassword()))
.build()).getId();
}
public User findById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("Unexpected user"));
}
public User findByEmail(String email) {
return userRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("Unexpected user"));
}
}
OAuth2에서 제공하는 이메일은 유일 값이기 때문에 이메일을 통해 유저를 찾을 수 있다.
// config - oauth - OAuth2SuccessHandler
@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
public static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token";
public static final Duration REFRESH_TOKEN_DURATION = Duration.ofDays(14);
public static final Duration ACCESS_TOKEN_DURATION = Duration.ofDays(1);
public static final String REDIRECT_PATH = "/articles";
private final TokenProvider tokenProvider;
private final RefreshTokenRepository refreshTokenRepository;
private final OAuth2AuthorizationRequestBasedOnCookieRepository authorizationRequestRepository;
private final UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
User user = userService.findByEmail((String) oAuth2User.getAttributes().get("email"));
// 리프레시 토큰 생성 -> 저장 -> 쿠키에 저장
String refreshToken = tokenProvider.generateToken(user, REFRESH_TOKEN_DURATION);
saveRefreshToken(user.getId(), refreshToken);
addRefreshTokenToCookie(request, response, refreshToken);
// 엑세스 토큰 생성 -> 패스에 엑세스 토큰을 추가
String accessToken = tokenProvider.generateToken(user, ACCESS_TOKEN_DURATION);
String targetUrl = getTargetUrl(accessToken);
// 인증 관련 설정값, 쿠키 제거
clearAuthenticationAttributes(request, response);
// 리다이렉트
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
// 생성된 리프레시 토큰을 전달받아 데이터베이스에 저장
private void saveRefreshToken(Long userId, String newRefreshToken) {
RefreshToken refreshToken = refreshTokenRepository.findByUserId(userId)
.map(entity -> entity.update(newRefreshToken))
.orElse(new RefreshToken(userId, newRefreshToken));
refreshTokenRepository.save(refreshToken);
}
// 생성된 리프레시 토큰을 쿠키에 저장
private void addRefreshTokenToCookie(HttpServletRequest request, HttpServletResponse response, String refreshToken) {
int cookieMaxAge = (int) REFRESH_TOKEN_DURATION.toSeconds();
CookieUtil.deleteCookie(request, response, REFRESH_TOKEN_COOKIE_NAME);
CookieUtil.addCookie(response, REFRESH_TOKEN_COOKIE_NAME, refreshToken, cookieMaxAge);
}
// 인증 관련 설정값, 쿠키 제거
private void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
super.clearAuthenticationAttributes(request);
authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}
// 엑세스 토큰을 패스에 추가
private String getTargetUrl(String token) {
return UriComponentsBuilder.fromUriString(REDIRECT_PATH)
.queryParam("token", token)
.build()
.toUriString();
}
}
onAuthenticationSuccess(~){~}
OAuth2 사용자 정보 가져오기
리프레시 토큰 생성, 저장 및 쿠키에 추가
tokenProvider를 사용하여 사용자에 대한 새로운 리프레시 토큰을 생성한다.
생성된 리프레시 토큰을 데이터베이스에 저장한다.
- saveRefreshToken(user.getId(), refreshToken){~}
해당 메서드는 주어진 사용자 ID에 대한 리프레시 토큰을 저장한다.
- refreshTokenRepository를 사용하여 주어진 사용자 ID에 대한 리프레시 토큰을 데이터베이스에서 찾는다.
- 만약 해당 사용자에 대한 리프레시 토큰이 이미 존재한다면, 해당 엔티티를 업데이트하여 새로운 리프레시 토큰으로 갱신한다.
- 그렇지 않으면, 새로운 RefreshToken 객체를 생성하여 주어진 사용자 ID와 새로운 리프레시 토큰을 사용하여 새 엔티티를 만든다.
- 최종적으로, 이 엔티티를 refreshTokenRepository를 사용하여 데이터베이스에 저장한다.
생성된 리프레시 토큰을 쿠키에 추가하여 클라이언트에게 전달한다. 이는 사용자의 브라우저에서 저장된다.
- addRefreshTokenToCookie(HttpServletRequest request, HttpServletResponse response, String refreshToken)
해당 메서드는 주어진 리프레시 토큰을 쿠키에 추가한다.
- 먼저, 리프레시 토큰이 저장될 쿠키의 만료 시간을 설정한다. 이는 REFRESH_TOKEN_DURATION에 정의된 기간으로 설정된다.
- 다음, CookieUtil을 사용하여 이전에 저장된 동일한 이름의 쿠키를 삭제한다. 이는 새로운 쿠키를 추가하기 전에 이전 쿠키를 제거해야하기 때문이다.
- 마지막으로, CookieUtil을 사용하여 새로운 리프레시 토큰을 쿠키에 추가합니다. 쿠키의 이름은 REFRESH_TOKEN_COOKIE_NAME으로 지정되고, 값은 주어진 리프레시 토큰이다. 만료 시간은 이전에 설정한 값인 cookieMaxAge로 설정된다.
엑세스 토큰 생성 및 패스에 추가
- getTargetUrl(String token){~}
- UriComponentsBuilder
URI를 구성하기 위한 편리한 방법을 제공하는 Spring Framework의 유틸리티 클래스이다. 해당 클래스를 사용하여 경로, 쿼리 매개변수 등을 쉽게 조작할 수 있다.
- fromUriString(REDIRECT_PATH)
기본 경로를 기반으로 UriComponentsBuilder 객체를 생성한다.
- queryParam("token", token)
쿼리 매개변수 "token"에 새로 생성된 엑세스 토큰을 추가한다.
- build()
UriComponentsBuilder 객체를 사용하여 최종 URI를 구성한다.
- toUriString()
구성된 URI를 문자열 형태로 반환한다.
- 엑세스 토큰 전달 예
http://localhost:8080/articles?token=eyj0ex~...
인증 관련 설정값 및 쿠키 제거
인증 후에는 사용된 속성을 지우고, 인증 요청과 관련된 쿠키를 제거합니다. 이는 보안 및 클린업을 위해 수행된다.
- clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response){~}
- super.clearAuthenticationAttributes(request)
부모 클래스인 SimpleUrlAuthenticationSuccessHandler의 clearAuthenticationAttributes 메서드를 호출하여 인증과 관련된 모든 속성을 지운다. 이는 Spring Security에서 내부적으로 사용되는 인증 관련 속성을 클리어하는 역할을 한다.
- authorizationRequestRepository.removeAuthorizationRequestCookies(request, response)
authorizationRequestRepository의 removeAuthorizationRequestCookies 메서드를 사용하여 인증 요청과 관련된 쿠키를 제거한다.
getRedirectStrategy().sendRedirect(request, response, targetUrl)
Spring Security에서 제공하는 리다이렉트 전략을 사용하여 클라이언트를 지정된 URL로 리다이렉트하는 역할을 한다. HTTP 응답의 Location 헤더에 새로운 URL을 포함하여 클라이언트에게 리다이렉트하도록 요청하는 방식이다.