오봉이와 함께하는 개발 블로그

Spring - Spring Security를 통해 Session 로그인 구현 본문

BE/Spring

Spring - Spring Security를 통해 Session 로그인 구현

오봉봉이 2023. 9. 3. 02:59
728x90

환경

  • Java 8
  • Spring Boot 2.7.5
  • 편의를 위해 DB를 사용하지 않고, HashMap을 사용
  • WebPage를 사용하지 않고 REST API 사용
    • Postman을 통해 테스트
  • 편의를 위해 모든 위험성, 설계 원칙을 열어두고 session을 통한 로그인 예제에만 집중
// 의존성
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

설명

Spring Security의 동작 내용은 후속 포스팅으로 할 예정이고 세션을 통해 간단하게 인증, 인가 예시를 남겨두고 싶었다.
설계 원칙도 위반하고, 위험성 또한 열어놨기 때문에 불편한 코드가 될 수 있지만 신경쓰지 않는 누군가에게 도움이 된다면 좋을 거 같다.
자세한 내용은 후속 포스팅에 진행할 예정이기 때문에 간단하게 설명한다.

CustomMemberDetailService

@Component("memberDetailService")
@RequiredArgsConstructor
@Slf4j
public class CustomMemberDetailService implements UserDetailsService {

  private final MemberRepository memberRepository;

  @Override
  public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
    log.info("loadUserByUsername");
    Member member = memberRepository.findByAccount(account);
    UserDetails userDetails = new User(member.getAccount(), member.getPassword(), AuthorityUtils.createAuthorityList("ROLE_"+ member.getLevel()));

    log.info("userDetails = {}", userDetails);

    return userDetails;
  }
}

repository에서 member 객체를 통해 ID, Password, Level을 Spring Security에서 사용하는 User를 생성한다.
어떻게 사용되는지는 필요한 정보에서 힌트를 얻을 수 있다.
ID, Password를 통해 인증하고, Level을 통해 권한을 판단해 인가를 해준다.

객체의 값은 아래와 같다.

org.springframework.security.core.userdetails.User [Username=admin, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]]

SecurityConfig

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

  private final CorsFilter corsFilter;
  private final SessionAccessDeniedHandler sessionAccessDeniedHandler;
  private final SessionAuthenticationEntryPoint sessionAuthenticationEntryPoint;

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  public WebSecurityCustomizer webSecurityCustomizer() {
    return (web) -> web.ignoring()
            .antMatchers("/resources/**", "/css/**", "/vendor/**",
                    "/js/**", "/favicon/**", "/img/**");
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors().disable()
            .csrf().disable()
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling(exceptionHandling -> exceptionHandling
                    .accessDeniedHandler(sessionAccessDeniedHandler)
                    .authenticationEntryPoint(sessionAuthenticationEntryPoint)
            )
            .sessionManagement(sessionManagement -> sessionManagement
                    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                    .sessionFixation().changeSessionId()
                    .maximumSessions(1)
                    .maxSessionsPreventsLogin(false)
            )
            .authorizeRequests()
            .antMatchers("/v1/member/join", "/v1/member/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin().disable()
            .httpBasic().disable();

    return http.build();
  }
}

WebSecurityConfigurerAdapter가 Deprecate되어 이제는 Spring에서 Bean을 주입하는 방식을 권장한다.

session 로그인 기능 관련해서 설명할 부분은 sessionManagement와 authorizeRequests, exceptionHandling이 있다.

sessionManagement 는 session 관리 전략을 설정할 수 있다.
간단한 옵션 설정을 통해 세션 생성 전략이나, 중복 인증 허용 등을 할 수 있다.

authorizeRequests 는 어떤 URI에 대해 허용하고, 인증이 필요하게 할지 설정하는 옵션이다.
"/v1/member/join", "/v1/member/login"은 인증 없이 모든 유저가 이용할 수 있도록 했고
그 외에 나머지는 모두 인증이 필요하도록 했다.
인가에 대한 부분은 controller에 어노테이션을 통해 설정했다.

@PreAuthorize("hasAnyRole('ADMIN')")

권한이 ADMIN인 Member에 대해서만 허용한다. 권한은 CustomMemberDetailService의 코드에서 AuthorityUtils.createAuthorityList("ROLE_"+ member.getLevel()) 를 통해 설정했다.
Spring Security에서 기본값은 ROLE_ 이라는 접미사가 없다면 인식이 되지 않는 걸로 알고 있으니 DB에 저장부터 ROLE_을 붙여서 저장하거나, User 객체를 만들 때 ROLE_ 접미사를 붙여야 한다.

exceptionHandling 은 accessDeniedHandler과 authenticationEntryPoint에 대해서만 설정했다.
접근 제한(인가)과, 인증되지 않은 계정에 대해 처리하는 옵션이다.

@Component
public class SessionAccessDeniedHandler implements AccessDeniedHandler {

   @Override
   public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
      //필요한 권한이 없이 접근하려 할때 403
      response.sendError(HttpServletResponse.SC_FORBIDDEN);
   }
}
@Component
public class SessionAuthenticationEntryPoint implements AuthenticationEntryPoint {

   @Override
   public void commence(HttpServletRequest request,
                        HttpServletResponse response,
                        AuthenticationException authException) throws IOException {
      // 유효한 자격증명을 제공하지 않고 접근하려 할때 401
      response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
   }
}

각각 정해진 인터페이스를 구현하면 된다.

테스트

로그인을 시도한다.

로그인을 하면 응답 헤더에 아래와 같은 속성이 있다.


Set-Cookie를 통해 SESSION ID를 Cookie에 설정하고, 다음 요청 헤더에 포함하게 된다.
/ 로 시작하는 모든 요청에 포함하며 JavaScript로 조작하지 못 하도록 httpOnly 옵션이 있다.


위 요청은 USER 권한을 가진 계정에 대해서만 인가되도록 설정했다. ADMIN 권한을 가진 계정을 통해 로그인 후 요청하면 아래와 같은 응답이 발생한다.

인증되지 않은 요청에 대해서 아래와 같이 401 에러를 응답하는 것을 확인할 수 있다.

전체 코드

https://github.com/rhqudco/Study_Spring/tree/main/session

728x90
Comments