스프링 시큐리티(Spring Security)는 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크이다.
스프링 스큐리티는 필터 기반으로 동작한다. 각 필터에서 인증, 인가와 관련된 작업을 처리한다.
기본적으로 세션 & 쿠키 방식으로 인증을 처리한다.
OAuth2와 JWT를 사용해 인증, 인가를 구현할 수도 있다.
인증(Authentication) : 보호된 리소스에 접근하는 것을 허용하기 이전에 등록한 사용자의 신원을 입증하는 과정
인가(Authorization) : 특정 부분에 접근할 수 있는지 확인하는 작업
dependencies {
...
// 스프링 시큐리티를 사용하기 위한 스타터
implementation 'org.springframework.boot:spring-boot-starter-security'
// 타임리프에서 스프링 시큐리티를 사용하기 위한 의존성 추가
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
// 스프링 시큐리티를 테스트하기 위한 의존성 추가
testImplementation 'org.springframework.security:spring-security-test'
}
스프링 시큐리티에서 사용자의 인증 정보를 저장하는 인터페이스이다. 스프링 시큐리티에서 해당 객체를 통해 인증 정보를 가져오려면 필수 오버라이드 메서드들을 여러 개 사용해야 한다.
@Table(name = "users")
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "password")
private String password;
@Builder
public User(String email, String password, String auth) {
this.email = email;
this.password = password;
}
// 권한 반환
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("user"));
}
// 사용자의 id를 반환(고유한 값)
@Override
public String getUsername() {
return email;
}
// 사용자의 패스워드 반환
@Override
public String getPassword() {
return password;
}
// 계정 만료 여부 반환
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정 잠금 여부 반환
@Override
public boolean isAccountNonLocked() {
return true;
}
// 패스워드의 만료 여부 반환
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정 사용 가능 여부 반환
@Override
public boolean isEnabled() {
return true;
}
}
UserDetailsService 인터페이스 구현을 통해 필수로 구현해야 하는 loadUserByUsername() 메서드를 오버라이딩해서 사용자 정보를 가져오는 로직을 작성한다.
@RequiredArgsConstructor
@Service
public class UserDetailService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) {
return userRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException((email)));
}
}
// 스프링 시큐리티 기능 비활성화
@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers(toH2Console())
.requestMatchers(new AntPathRequestMatcher("/static/**"));
}
인증, 인가 서비스를 적용하지 않는 부분에 스프링 스큐리티의 모든 기능을 사용하지 않게 설정하는 코드이다.
정적 리소스와 h2-console 하위 url을 대상으로 ignoring() 메서드를 사용했다.
requestMatchers() : 특정 요청과 일치하는 url에 대한 액세스를 설정한다.
// 특정 HTTP 요청에 대한 웹 기반 보안 구성
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeRequests(auth -> auth
.requestMatchers(
new AntPathRequestMatcher("/login"),
new AntPathRequestMatcher("/signup"),
new AntPathRequestMatcher("/user")
).permitAll()
.anyRequest()
.authenticated()
)
.formLogin(formLogin -> formLogin.loginPage("/login").defaultSuccessUrl("/articles"))
.logout(logout -> logout.logoutSuccessUrl("/login").invalidateHttpSession(true))
.csrf(AbstractHttpConfigurer::disable)
.build();
}
특정 HTTP 요청에 대해 웹 기반 보안을 구성한다. 이 메서드에서 인증/인가 및 로그인, 로그아웃 관련 설정을 할 수 있다.
formLogin() : 폼 기반 로그인 설정
permitAll() : 누구나 접근이 가능하게 설정한다. 즉, “/login”, “/signup”, “/user”로 요청이 오면 인증/인가 없이도 접근할 수 있다.
anyRequest() : 위에서 설정한 url 이외의 요청에 대해서 설정
authenticated() : 별도의 인가는 필요하지 않지만 인증이 성공된 상태여야 접근할 수 있다.
invalidateHttpSession() : 로그아웃 이후에 세션을 전체 삭제할지 여부를 설정
CSRF 설정이 비활성화되어 있는데 실무에서는 활성화하는 게 좋다.
// 인증 관리자 관련 설정
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailService); // 사용자 정보 서비스 설정
authProvider.setPasswordEncoder(bCryptPasswordEncoder);
return new ProviderManager(authProvider);
}
// 패스워드 인코더로 사용할 빈 등록
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final UserDetailService userDetailService;
// 스프링 시큐리티 기능 비활성화
@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers(toH2Console())
.requestMatchers(new AntPathRequestMatcher("/static/**"));
}
// 특정 HTTP 요청에 대한 웹 기반 보안 구성
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeRequests(auth -> auth
.requestMatchers(
new AntPathRequestMatcher("/login"),
new AntPathRequestMatcher("/signup"),
new AntPathRequestMatcher("/user")
).permitAll()
.anyRequest()
.authenticated()
)
.formLogin(formLogin -> formLogin.loginPage("/login").defaultSuccessUrl("/articles"))
.logout(logout -> logout.logoutSuccessUrl("/login").invalidateHttpSession(true))
.csrf(AbstractHttpConfigurer::disable)
.build();
}
// 인증 관리자 관련 설정
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder) throws Exception {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailService); // 사용자 정보 서비스 설정
authProvider.setPasswordEncoder(bCryptPasswordEncoder);
return new ProviderManager(authProvider);
}
// 패스워드 인코더로 사용할 빈 등록
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
인증 정보를 서버나 세션에 저장하지 않고 클라이언트에서 발급받은 토큰을 저장하여 요청시 토큰을 전송하는 방식을 토큰 기반 인증이라고 한다. 대표적으로 JWT가 있다.
무상태성 : 사용자의 인증 정보가 담겨 있는 토큰이 서버가 아닌 클라이언트에 있으므로 서버에서 인증 정보를 관리할 필요가 없다.
확장성 : 토큰을 클라이언트에서 관리하기 때문에 결제 서버와 주문 서버를 분리해서 운영해도 두 곳 모두에서 토큰 인증을 할 수 있다. 서버에서 관리를 하면 별도의 작업이 필요하다.
무결성 : 토큰을 발급한 이후에는 토큰 정보를 변경하는 행위를 할 수 없다.
헤더(header) : 토큰의 타입(typ)과 해싱 알고리즘(alg)을 지정하는 정보를 담는다.
내용(payload) : 토큰과 관련된 정보를 담는다. 내용의 한 덩어리를 클레임(claim)이라고 하며, 클레임은 키값의 한 쌍으로 이루어져 있다. 클레임은 등록된 클레임, 공개 클레임, 비공개 클레임으로 나눌 수 있다. 아래는 등록된 클레임 종류이다.
| 이름 | 설명 |
|---|---|
| iss | 토큰 발급자(issuer) |
| sub | 토큰 제목(subject) |
| aud | 토큰 대상자(audience) |
| exp | 토큰의 만료 시간(expiration) |
| nbf | 토큰의 활성 날짜와 비슷한 개념으로 nbf는 Not Before를 의미한다. |
| iat | 토큰이 발급된 시간(issued at) |
| jti | JWT의 고유 식별자로서 주로 일회용 토큰에 사용한다. |
dependencies {
...
// 자바 JWT 라이브러리
implementation 'io.jsonwebtoken:jjwt:0.9.1'
// XML 문서와 Java 객체 간 매핑 자동화
implementation 'javax.xml.bind:jaxb-api:2.3.1'
}
application.yml 파일에 설정한 환경 변수 값을 클래스의 프로퍼티값으로 가져오기 위해 @ConfigurationProperties 애너테이션을 사용한다.
@Setter
@Getter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {
private String issuer;
private String secretKey;
}
// JWT 토큰 생성 메서드
private String makeToken(Date expiry, User user) {
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiry)
.setSubject(user.getEmail())
.claim("id", user.getId())
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
헤더에는 typ(타입), 내용에는 iss(발급자), iat(발급일시), exp(만료일시), sub(토큰 제목), 클레임에는 유저 ID를 지정
토큰을 만들 때는 프로퍼티즈 파일에 선언해둔 비밀키와 함께 HS256 방식으로 암호화한다.
// JWT 토큰 유효성 검증 메서드
public boolean validToken(String token) {
try {
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
비밀키와 함께 토큰 복호화를 진행한다.
복호화 과정에서 에러가 발생하면 유효하지 않은 토큰이므로 false를 반환한다.
// 토큰 기반으로 인증 정보를 가져오는 메서드
public Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(
new org.springframework.security.core.userdetails.User(
claims.getSubject(), "", authorities
), token, authorities
);
}
getClaims() 를 호출해서 클레임 정보를 반환받아 사용자 이메일이 들어 있는 토큰 제목 sub와 토큰 기반으로 인증 정보를 생성한다.// 토큰 기반으로 유저 ID를 가져오는 메서드
public Long getUserId(String token) {
Claims claims = getClaims(token);
return claims.get("id", Long.class);
}
getClaims() 를 호출해서 클레임 정보를 반환받고 클레임에서 id 키로 저장된 값을 가져와 반환한다.@RequiredArgsConstructor
@Service
public class TokenProvider {
private final JwtProperties jwtProperties;
public String generateToken(User user, Duration expireAt) {
Date now = new Date();
return makeToken(new Date(now.getTime() + expireAt.toMillis()), user);
}
// JWT 토큰 생성 메서드
private String makeToken(Date expiry, User user) {
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiry)
.setSubject(user.getEmail())
.claim("id", user.getId())
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
// JWT 토큰 유효성 검증 메서드
public boolean validToken(String token) {
try {
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
// 토큰 기반으로 인증 정보를 가져오는 메서드
public Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(
new org.springframework.security.core.userdetails.User(
claims.getSubject(), "", authorities
), token, authorities
);
}
// 토큰 기반으로 유저 ID를 가져오는 메서드
public Long getUserId(String token) {
Claims claims = getClaims(token);
return claims.get("id", Long.class);
}
private Claims getClaims(String token) {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
}
필터는 실제로 요청이 전달되기 전과 후에 URL 패턴에 맞는 모든 요청을 처리하는 기능을 제공한다.
시큐리티 컨텍스트는 인증 객체가 저장되는 보관소이다. 이 클래스는 쓰레드마다 공간을 할당하는 즉, 쓰레드 로컬에 저장되므로 코드 아무 곳에서나 참조할 수 있고, 다른 쓰레드와 공유하지 않으므로 독립적으로 사용할 수 있다.
스큐리티 컨텍스트 객체를 저장하는 객체가 시큐리티 컨텍스트 홀더이다.
요청이 오면 헤더값을 비교해서 토큰이 있는지 확인하고 유효 토큰이라면 시큐리티 콘텍스트 홀더에 인증 정보를 저장한다.
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
private final static String HEADER_AUTHORIZATION = "Authorization";
private final static String TOKEN_PREFIX = "Bearer";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
String token = getAccessToken(authorizationHeader);
if (tokenProvider.validToken(token)) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getAccessToken(String authorizationHeader) {
if (authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)) {
return authorizationHeader.substring(TOKEN_PREFIX.length());
}
return null;
}
}
리소스 서버에서 보내주는 사용자 정보를 불러오는 메서드인 loadUser() 를 통해 사용자를 조회한다.
@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에서 제공하는 OAuth 서비스에서 제공하는 정보를 기반으로 유저 객체를 만들어주는 loadUser() 메서드를 사용해 사용자 객체를 불러온다.
사용자 객체는 식별자, 이름, 이메일, 프로필 사진 링크 등의 정보를 담고 있다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/api/token")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/api/**")).authenticated()
.anyRequest().permitAll())
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint.authorizationRequestRepository(oAuth2AuthorizationRequestBasedOnCookieRepository()))
.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint.userService(oAuth2UserCustomService))
.successHandler(oAuth2SuccessHandler())
).exceptionHandling(exceptionHandling ->
exceptionHandling.defaultAuthenticationEntryPointFor(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new AntPathRequestMatcher("/api**")
))
.build();
}
토큰 방식으로 인증을 하기 때문에 기존에 사용하던 폼 로그인, 세션 비활성화
토큰 재발급 URL은 인증 없이 접근 가능하도록 설정, 나머지 API URL은 인증 필요
OAuth2에 필요한 정보를 세션이 아닌 쿠키에 저장해서 쓸 수 있도록 인증 요청과 관련된 상태를 저장할 저장소 설정
OAuth2에 필요한 정보를 세션이 아닌 쿠키에 저장해서 쓸 수 있도록 인증 요청과 관련된 상태를 저장할 저장소를 구현한다. 권한 인증 흐름에서 클라이언트의 요청을 유지하는 데 사용하는 AuthorizationRequestRepository 클래스를 구현해 쿠키를 사용해 OAuth의 정보를 가져오고 저장하는 로직을 작성한다.
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 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
);
}
@Override
public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) {
return this.loadAuthorizationRequest(request);
}
public void removeAuthorizationRequestCookies(HttpServletRequest request, HttpServletResponse response) {
CookieUtil.deleteCookie(request, response, OAUTH2_AUTHORIZATION_REQUEST_COOKIE_NAME);
}
}
// 리프레시 토큰 생성 -> 저장 -> 쿠키에 저장
String refreshToken = tokenProvider.generateToken(user, REFRESH_TOKEN_DURATION);
saveRefreshToken(user.getId(), refreshToken);
addRefreshTokenToCookie(request, response, refreshToken);
// 생성된 리프레시 토큰을 전달받아 데이터베이스에 저장
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);
}
// 액세스 토큰 생성 -> 패스에 액세스 토큰 추가
String accessToken = tokenProvider.generateToken(user, ACCESS_TOKEN_DURATION);
String targetUrl = getTargetUrl(accessToken);
// 액세스 토큰을 path에 추가
private String getTargetUrl(String token) {
return UriComponentsBuilder.fromUriString(REDIRECT_PATH)
.queryParam("token", token)
.build()
.toUriString();
}
// 인증 관련 설정값, 쿠키 제거
clearAuthenticationAttributes(request, response);
private void clearAuthenticationAttributes(HttpServletRequest request, HttpServletResponse response) {
super.clearAuthenticationAttributes(request);
authorizationRequestRepository.removeAuthorizationRequestCookies(request, response);
}
인증 프로세스를 진행하면서 세션과 쿠키에 임시로 저장해둔 인증 관련 데이터를 제거한다.
removeAuthorizationRequestCookies() 를 추가로 호출해 OAuth 인증을 위해 저장된 정보도 삭제한다.
// 리다이렉트
getRedirectStrategy().sendRedirect(request, response, targetUrl);
@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, ServletException {
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);
}
// 액세스 토큰을 path에 추가
private String getTargetUrl(String token) {
return UriComponentsBuilder.fromUriString(REDIRECT_PATH)
.queryParam("token", token)
.build()
.toUriString();
}
}
스프링 시큐리티의 기본 로직에서는 별도의 authenticationSuccessHandler를 지정하지 않으면 로그인 성공 이후 SimpleUrlAuthenticationSuccessHandler를 사용한다.
일반적인 로직은 동일하게 사용하고, 토큰과 관련된 작업만 추가로 처리하기 위해 SimpleUrlAuthenticationSuccessHandler을 상속받은 뒤에 onAuthenticationSuccess() 메서드를 오버라이딩 했다.