질문, 피드백 등 모든 댓글 환영합니다.
프로젝트를 진행하며 가장 신경쓰이고 아쉬웠던 부분이 보안 부분이었습니다.
지금까진 HttpSession을 통해 로그인 상태를 유지하고 직접 구현한 Interceptor를 추가하여 사용자 인증 기능을 만들었지만 스프링에서 제공하는 Spring Security 프레임워크를 이용하여 보안 수준을 높여보도록 하겠습니다.
참고 : 기존의 WebSecurityConfigurerAdapter는 스프링부트 2.7, 스프링 시큐리티 5.71 부터 deprecated 되었습니다. 해당 기사 참고해주세요.
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
testImplementation 'org.springframework.security:spring-security-test'
}
Configurer
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable().authorizeRequests()
.antMatchers("/", "/login", "/add", "/error").permitAll()
.antMatchers("/todo/**").authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/todo")
.and()
.logout()
.logoutSuccessUrl("/");
return http.build();
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/css/**", "/js/**");
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
SecurityFilterChain을 스프링 빈으로 등록시켜 스프링 시큐리티 관련 설정을 할 수 있습니다.
csrf 관련 설정은 disable()로 잠시 내려두었습니다. 이 부분은 추후에 업데이트하도록 하겠습니다.
permitAll()로 스프링 시큐리티의 인증을 적용하지 않을 url을 지정할 수 있습니다.
authenticated()는 그 반대
hasRole() 등)formLogin()으로 login 관련 설정이 가능합니다. formLogin()을 사용하면 "/login" 경로에 스프링 시큐리티가 제공하는 로그인 페이지가 제공됩니다.
저는 이전에 로그인페이지를 만들어 두었으므로 loginPage("/login")를 사용하여 제작한 로그인 페이지를 사용하겠습니다.
logout()으로 logout 관련 설정이 가능합니다. logout()을 사용하면 "logout" 경로에 Post 요청이 오면 스프링 시큐리티가 세션을 제거하여 로그아웃 처리를 합니다. logoutUrl("...") 을 사용하여 경로를 직접 지정할 수도 있습니다.
기존 방식인 configure(WebSecurity web) 대신 WebSecurityCustomizer 을 스프링 빈으로 등록하여 사용합니다.
WebSecurity의 ignoring()을 사용하여 스프링 시큐리티가 제공하는 필터를 거치 않도록 설정할 수 있습니다.
WebSecurity가HttpSecurity보다 먼저 작용하므로WebSecurity의ignoring()을 설정한 경로는 이후HttpSecurity의 설정이 적용되지 않습니다.
AuthenticationProviderImpl
@Component
@RequiredArgsConstructor
public class AuthenticationProviderImpl implements AuthenticationProvider {
private final UserDetailsService service;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
String username = token.getName();
String password = (String) token.getCredentials();
UserDetailsImpl loginMember = (UserDetailsImpl) service.loadUserByUsername(username);
if (!bCryptPasswordEncoder.matches(password, loginMember.getPassword())) {
throw new BadCredentialsException(loginMember.getUsername() + " 비밀번호를 확인해주세요.");
}
return new UsernamePasswordAuthenticationToken(loginMember, password, loginMember.getAuthorities());
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
AuthenticationProvider을 상속 받은 객체로 인증 작업이 이루어지는 부분입니다. @Component 사용하여 스프링 빈으로 등록하여 사용합니다.
토큰(Authentication, 더 정확히는 UsernamePasswordAuthenticationToken)에 저장 된 정보와 UserDetailsService에서 조회한 사용자 정보를 비교하여 인증 작업을 진행합니다.
UserDetailsServiceImpl
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final MemberRepository repository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new UserDetailsImpl(repository.findByLoginId(username).orElseThrow(() ->
new UsernameNotFoundException("해당 사용자가 존재하지 않습니다. : " + username)));
}
}
UserDetailsService를 상속 받은 객체로 이 서비스 객체를 통해 DB에서 loginId(username) 기반으로 사용자를 조회하고 UserDetails를 반환합니다.
UserDetailsImpl
@AllArgsConstructor
@Getter
public class UserDetailsImpl implements UserDetails {
private final Member member;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getLoginId();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
getAuthorities() : 유저의 권한 목록
isAccountNonExpired() : 계정 만료 여부, true : 만료 안됨, false : 만료
isAccountNonLocked() : 계정 잠김 여부, true : 잠기지 않음, false : 잠김
isCredentialsNonExpired() : 비밀번호 만료 여부, true : 만료 안됨, false : 만료
isEnabled() : 사용자 활성화 여부, true : 만료 안됨, false : 만료
본격적으로 스프링 시큐리티를 서비스에 적용시키겠습니다.