스프링 시큐리티

ppp·2025년 7월 16일

스프링 시큐리티란?

스프링 시큐리티(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'
}

UserDetails 인터페이스

스프링 시큐리티에서 사용자의 인증 정보를 저장하는 인터페이스이다. 스프링 시큐리티에서 해당 객체를 통해 인증 정보를 가져오려면 필수 오버라이드 메서드들을 여러 개 사용해야 한다.

@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;
  }
}
  • User 클래스(엔티티)가 구현한 UserDetails는 스프링 스큐리티에서 사용자의 인증 정보를 담아두는 인터페이스이다.

UserDetailsService 인터페이스

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 요청에 대한 웹 기반 보안 구성

// 특정 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);
}
  • 사용자 정보를 가져올 서비스를 재정의하거나, 인증 방법, 예를 들어 LDAP, JDBC 기반 인증 등을 설정할 때 사용합니다.

패스워드 인코더로 사용할 빈 등록

// 패스워드 인코더로 사용할 빈 등록
@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가 있다.

특징

  • 무상태성 : 사용자의 인증 정보가 담겨 있는 토큰이 서버가 아닌 클라이언트에 있으므로 서버에서 인증 정보를 관리할 필요가 없다.

  • 확장성 : 토큰을 클라이언트에서 관리하기 때문에 결제 서버와 주문 서버를 분리해서 운영해도 두 곳 모두에서 토큰 인증을 할 수 있다. 서버에서 관리를 하면 별도의 작업이 필요하다.

  • 무결성 : 토큰을 발급한 이후에는 토큰 정보를 변경하는 행위를 할 수 없다.

JWT

  • 헤더(header) : 토큰의 타입(typ)과 해싱 알고리즘(alg)을 지정하는 정보를 담는다.

  • 내용(payload) : 토큰과 관련된 정보를 담는다. 내용의 한 덩어리를 클레임(claim)이라고 하며, 클레임은 키값의 한 쌍으로 이루어져 있다. 클레임은 등록된 클레임, 공개 클레임, 비공개 클레임으로 나눌 수 있다. 아래는 등록된 클레임 종류이다.

이름설명
iss토큰 발급자(issuer)
sub토큰 제목(subject)
aud토큰 대상자(audience)
exp토큰의 만료 시간(expiration)
nbf토큰의 활성 날짜와 비슷한 개념으로 nbf는 Not Before를 의미한다.
iat토큰이 발급된 시간(issued at)
jtiJWT의 고유 식별자로서 주로 일회용 토큰에 사용한다.
  • 서명(signature) : 해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도로 사용하며, 헤더의 인코딩값과 내용의 인코딩값을 합친 후에 주어진 비밀키를 사용해 해시값을 생성한다.

의존성 추가

dependencies {
    ...
    // 자바 JWT 라이브러리
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    // XML 문서와 Java 객체 간 매핑 자동화
    implementation 'javax.xml.bind:jaxb-api:2.3.1'
}

JwtProperties

application.yml 파일에 설정한 환경 변수 값을 클래스의 프로퍼티값으로 가져오기 위해 @ConfigurationProperties 애너테이션을 사용한다.

@Setter
@Getter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {
  private String issuer;
  private String secretKey;
}

TokenProvider

JWT 토큰 생성 메서드

// 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 토큰 유효성 검증 메서드

// 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를 가져오는 메서드

// 토큰 기반으로 유저 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;
  }
}

OAuthUserCustomService

리소스 서버에서 보내주는 사용자 정보를 불러오는 메서드인 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() 메서드를 사용해 사용자 객체를 불러온다.

  • 사용자 객체는 식별자, 이름, 이메일, 프로필 사진 링크 등의 정보를 담고 있다.

WebOAuthSecurityConfig

@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에 필요한 정보를 세션이 아닌 쿠키에 저장해서 쓸 수 있도록 인증 요청과 관련된 상태를 저장할 저장소 설정

OAuth2AuthorizationRequestBasedOnCookieRepository

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);
  }
}

OAuth2SuccessHandler

// 리프레시 토큰 생성 -> 저장 -> 쿠키에 저장
  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);
  • 엑세스 토큰을 쿼리 파라미터로 가진 URL로 리다이렉트한다.

전체 코드

@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() 메서드를 오버라이딩 했다.

0개의 댓글