오봉이와 함께하는 개발 블로그
Spring - Spring Security를 통해 Session 로그인 구현 본문
환경
- 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 에러를 응답하는 것을 확인할 수 있다.
전체 코드
'BE > Spring' 카테고리의 다른 글
스프링 핵심 원리 - 고급편_로그 추적기(요구사항 분석, 프로토타입 개발 V1) (0) | 2024.03.17 |
---|---|
Spring Framework - STOMP (convertAndSend vs convertAndSendToUser) (0) | 2024.02.22 |
스프링 MVC 2 - 예제로 구현하는 파일 업로드, 다운로드 (0) | 2022.09.01 |
스프링 MVC 2 - 스프링과 파일 업로드 (0) | 2022.09.01 |
스프링 MVC 2 - 서블릿과 파일 업로드 2 (1) | 2022.09.01 |