스프링 시큐리티를 적용하기 전에 간단한 사용자 테이블을 생성해주었다.
테이블 이름: user_info
컬럼: user_id, create_dt, password, role
유저 엔티티 클래스
@Getter
@Entity
@Table(name = "user_info")
@NoArgsConstructor
public class User {
@Id
private String userId;
private String password;
@Enumerated(EnumType.STRING)
private UserRole role;
private LocalDateTime createDt;
@Builder
public User(String userId, String password, UserRole role) {
this.userId = userId;
this.password = password;
this.role = role;
this.createDt = LocalDateTime.now();
}
}
role은 admin과 user 두개를 만들어 주었다.
@Getter
public enum UserRole {
ADMIN("ROLE_ADMIN"),
USER("ROLE_USER");
private String name;
UserRole(String name) {
this.name = name;
}
}
아래와 같은 순서로 진행했다.
1. 로그인을 위해 AbstractAuthenticationProcessingFilter를 상속한 SecurityFilter 생성
2. AuthenticationManager을 구현한 인증 로직을 담을 AuthenticationManagerImpl 생성
3. AccessDeniedHandler, AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler 구현
4. SecurityConfig 설정
1. 로그인을 위해 AbstractAuthenticationProcessingFilter를 상속한 SecurityFilter 생성
생성자에서 url과 manager, 그리고 success&failure handler를 받아 셋팅해준다.
attemptAuthentication 메서드에서 manager의 authenticate 메서드를 호출해 인증을 수행한다.
@Slf4j
public class SecurityFilter extends AbstractAuthenticationProcessingFilter {
public SecurityFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager,
AuthenticationSuccessHandler successHandler, AuthenticationFailureHandler failureHandler) {
super(defaultFilterProcessesUrl, authenticationManager);
this.setAuthenticationSuccessHandler(successHandler);
this.setAuthenticationFailureHandler(failureHandler);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
String username = request.getParameter("userId");
String password = request.getParameter("password");
return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
}
2. AuthenticationManager을 구현하여 인증 로직을 담을 AuthenticationManagerImpl 생성
여기서 인증을 수행해 주었는데 굳이 userdetails를 구현하지 않았다.
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthenticationManagerImpl implements AuthenticationManager {
private final UserService userService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userId = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
LoginUserDto loginUser = userService.login(userId, password);
return new UsernamePasswordAuthenticationToken(userId, loginUser.getPassword(), loginUser.getRoles());
}
}
로그인 메서드에서 각각 상황에 맞는 예외를 던져 FailureHandler에서 받을 수 있게 해주었다.
public LoginUserDto login(String userId, String password) {
User findUser = userRepository.findByUserId(userId);
if(findUser == null)
throw new UsernameNotFoundException("user is not exists");
if(!passwordEncoder.matches(password, findUser.getPassword()))
throw new BadCredentialsException("password is not matches");
return new LoginUserDto(findUser);
}
PasswordEncoder는 아래와 같이 빈으로 등록해 주었다.
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
3. AccessDeniedHandler, AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler 구현
별다른 로직 없이 일단은 로그랑 응답값만 설정해 주었다.
@Slf4j
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.info("AccessDenied [{}]", accessDeniedException.getMessage());
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().print("AccessDenied SC_FORBIDDEN");
}
}
@Slf4j
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.info("AuthenticationEntryPoint [{}]", authException.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().print("AuthenticationEntryPoint SC_UNAUTHORIZED");
}
}
@Slf4j
@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("AuthenticationFailureHandler [{}]", exception.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().print(exception.getMessage());
}
}
@Slf4j
@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("AuthenticationSuccess [{}]", authentication.getDetails());
response.getWriter().print("AuthenticationSuccess " + authentication.getPrincipal());
}
}
4. SecurityConfig 설정
이제 구현한 객체들로 설정을 완료해주자.
기존에 사용하던 WebSecurityConfigurerAdapter가 deprecated 되었다!
대신 SecurityFilterChain을 빈을 생성하여 구성한다. (자세한 방법은 아래 스프링 블로그 참조~!)
UsernamePasswordAuthenticationFilter 필터 전에 직접 구현한 SecurityFilter를 등록해주고 각각 핸들러들을 셋팅해 주었다.
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final AuthenticationManagerImpl authenticationManager;
private final AuthenticationSuccessHandlerImpl successHandler;
private final AuthenticationFailureHandlerImpl failureHandler;
private final AccessDeniedHandlerImpl accessDeniedHandler;
private final AuthenticationEntryPointImpl authenticationEntryPoint;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) ->
requests.antMatchers("/un-auth/**").permitAll()
.antMatchers("/admin/**").hasRole(ADMIN.name())
.antMatchers("/user/**").hasRole(USER.name())
.anyRequest().authenticated())
.formLogin().disable()
.addFilterBefore(
new SecurityFilter("/login", authenticationManager, successHandler, failureHandler),
UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint)
.and()
.csrf().disable();
return http.build();
}
}
참고
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter
Spring | Home
Cloud Your code, any cloud—we’ve got you covered. Connect and scale your services, whatever your platform.
spring.io
마지막으로, 직접 테스트를 해보자
먼저 user 계정(role:USER)과 admin 계정(role:ADMIN)을 생성해 주었다. (비밀번호는 1234로 동일)
비밀번호가 틀리면(BadCredentialsException) 아래와 같이 응답값이 온다.
없는 아이디를 입력했을때(UsernameNotFoundException) 의 응답.
로그인 성공
user로 로그인 했을 시 /user/** uri는 권한이 존재하기 때문에 정상적으로 응답이 온다.
하지만 admin 권한이 필요한 uri 호출시 AccessDenied 된다!
Spring Security의 세계는 너무 방대해서 세세하게 알려면 많은 시간과 노력이 필요하다.
개발을 시작한지 거의 2년이 다 되어 가지만 시큐리티는 정말 어려운 것,,,,
다음엔 OAuth를 이용한 로그인을 해보려고 한다!
참고
https://docs.spring.io/spring-security/reference/index.html
Spring Security :: Spring Security
If you are ready to start securing an application see the Getting Started sections for servlet and reactive. These sections will walk you through creating your first Spring Security applications. If you want to understand how Spring Security works, you can
docs.spring.io
'프로그래밍 실습 > Spring' 카테고리의 다른 글
프론트 컨트롤러 도입 (0) | 2023.01.28 |
---|---|
프론트 컨트롤러(Front Controller) 패턴 (0) | 2023.01.27 |
A Foreign key refering ... has the wrong number of column. should be 2 (0) | 2023.01.21 |
InvalidDefinitionException Exception (1) | 2023.01.15 |
marked as rollback-only Exception 해결기 (1) | 2023.01.13 |
댓글