정수원님의 강의 스프링 시큐리티 완전 정복 [6.x 개정판] 보면서 공부한 내용입니다.
@Bean
@Order(1)
public SecurityFilterChain restSecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/login")
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**","/images/**","/js/**","/favicon.*","/*/icon-*").permitAll()
.anyRequest().permitAll()
)
.csrf(AbstractHttpConfigurer::disable)
;
return http.build(); // securityFilterChain 빈 생성
}
http.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)
http.addFilterAfter(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)
http.addFilter(new CustomFilter());
http.addFilterAfter(new CustomFilter(), UsernamePasswordAuthenticationFilter.class);
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
// 비동기식으로 올 때만 인증 필터 작용하도록 설정
if(HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)){
throw new IllegalArgumentException("POST나 비동기 방식이 아닙니다.");
}
// 사용자가 입력한 정보를 가져와서 인증처리 진행히여 AccountDto에 담도록 설정
AccountDto accountDto = objectMapper.readValue(request.getReader(), AccountDto.class);
if(!StringUtils.hasText(accountDto.getUsername()) || !StringUtils.hasText(accountDto.getPassword())){
// username 또는 password에 값이 없으면 예외 발생
throw new AuthenticationServiceException("아이디 또는 비밀번호가 없습니다.");
}
// 모든 조건 통과하면 인증 처리되도록 진행
RestAuthenticationToken restAuthenticationToken = new RestAuthenticationToken(accountDto.getUsername(),accountDto.getPassword());
return getAuthenticationManager().authenticate(restAuthenticationToken);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String loginId = authentication.getName();
String password = (String) authentication.getCredentials();
AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(loginId);
if(!passwordEncoder.matches(password, accountContext.getPassword())){
throw new BadCredentialsException("Invalid password");
}
return new RestAuthenticationToken(accountContext.getAuthorities(), accountContext.getAccountDto(), null);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
AccountDto accountDto = (AccountDto) authentication.getPrincipal();
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
accountDto.setPassword(null);
mapper.writeValue(response.getWriter(),accountDto);
clearAuthenticationAttributes(request);
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
// 401 코드 => 인증 실패
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
if(exception instanceof BadCredentialsException){
mapper.writeValue(response.getWriter(), "유효하지 않은 아이디 또는 비밀번호 입니다.");
}
mapper.writeValue(response.getWriter(),"인증 실패");
}
/**
* 세션을 사용하도록 설정
*/
private SecurityContextRepository getSecurityContextRepository(HttpSecurity http) {
SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
if(securityContextRepository == null){
securityContextRepository = new DelegatingSecurityContextRepository(
new RequestAttributeSecurityContextRepository(), new HttpSessionSecurityContextRepository()
);
}
return securityContextRepository;
}
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 인증받지 못한 상태에서 왔기 때문에 오류코드를 응답해야됨
// => 401 코드
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// int SC_UNAUTHORIZED = 401; 로 정의되어 있음
response.getWriter().write(mapper.writeValueAsString(HttpServletResponse.SC_UNAUTHORIZED));
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 인증 받은 상태에서 접근 거부 당했기 때문에 오류코드를 응답해야됨
// => 403 코드
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// int SC_FORBIDDEN = 403; 로 정의되어 있음
response.getWriter().write(mapper.writeValueAsString(HttpServletResponse.SC_FORBIDDEN));
}
. exceptionHandling(exption -> exption
.authenticationEntryPoint(new RestAuthenticationEntryPoint())
.accessDeniedHandler(new RestAccessDeniedHandler()))
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response){
Authentication authentication = SecurityContextHolder.getContextHolderStrategy().getContext().getAuthentication();
if(authentication != null){
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
return "logout";
}
function login() {
const csrfHeader = $('meta[name="_csrf_header"]').attr('content');
const csrfToken = $('meta[name="_csrf"]').attr('content')
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
[csrfHeader]: csrfToken
},
body: JSON.stringify({ username, password }),
})
.then(response => {
response.json().then(function (data) {
console.log(data);
window.location.replace('/api')
})
})
.catch(error => {
console.error('Error during login:', error);
});
}
.with(new RestApiDsl<>(), restDsl -> restDsl
.restSuccessHandler(restSuccessHandler)
.restFailureHandler(restFailureHandler)
.loginPage("/api/login")
.loginProcessingUrl("/api/login"))