SpringBoot SpringSecurity 설정 1-3. 인증 실패 처리

jeonbang123·2023년 4월 1일
0

springboot

목록 보기
9/14

1. AuthenticationEntryPoint

  • 401 Error 처리 (AuthenticationException)
    .antMatchers("/api/v1/test/auth").authenticated()에서 authenticated() 이 인증 여부에 따라 401Error를 뱉으면 AuthenticationEntryPoint 에서 처리한다.

1-1. SecurityAuthenticationEntryPoint 추가

package com.codesign.base.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;


@Slf4j
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper = new ObjectMapper();

//    Response에 401이 떨어질만한 에러가 발생할 경우 해당로직을 타게되어, commence라는 메소드를 실행하게됩니다.
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        log.error("UnAuthorized!!! message : {}", authException.getMessage());

        Map<String, Object> responseMap = new HashMap<String,Object>();
        responseMap.put("message", "UnAuthorized!!!");
        responseMap.put("status", HttpStatus.UNAUTHORIZED.value());

        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.UNAUTHORIZED.value());

        try(ServletOutputStream os = response.getOutputStream()){
            objectMapper.writeValue(os, responseMap);
            os.flush();
        }
    }
}
속성
설명
AuthenticationEntryPoint인증 실패시 401Error가 떨어질 경우 처리 위한 클래스
commence()request, response, AuthenticationException을 인자로 받고,
인증 실패시 로직 작성
os.flush()스트림 버퍼에 저장되어 있는 데이터를 강제적으로 출력시킴
기본적인 출력 스트림은 버퍼에 데이터가 가득 차면 그때 데이터를 출력시키는데 이 메서드를 사용하면 저장된 데이터의 크기에 관계없이 바로 출력

1-2. SecurityConfig.java - authenticationEntryPoint 주입, 체인 추가

- authenticationEntryPoint 주입

@Autowired
SecurityAuthenticationEntryPoint authenticationEntryPoint;

- 체인 추가

.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and() 체인 추가


전체 SecurityConfig.java

package com.codesign.base.configure;

import com.codesign.base.security.SecurityAuthenticationEntryPoint;
import com.codesign.base.security.filter.SecurityAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    SecurityAuthenticationEntryPoint authenticationEntryPoint;

    @Bean
    public SecurityAuthenticationFilter securityAuthenticationFilter(){
        return new SecurityAuthenticationFilter();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/api/v1/test/permit-all").permitAll()
                .antMatchers("/api/v1/test/auth").authenticated()
//                .antMatchers("/**").authenticated()
//                .anyRequest().permitAll().and()
                .anyRequest().authenticated().and()
                .formLogin().disable()
        ;

        http.addFilterBefore(securityAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

1.3 SecurityAuthenticationFilter.java - 요청 정보와 db정보로 유저 인증 로직 수정

package com.codesign.base.security.filter;

import com.codesign.base.security.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class SecurityAuthenticationFilter extends OncePerRequestFilter {

    // TODO 수정자 필드 주입다시 확인해보기
    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

//        String username = request.getParameter("username");
        String requestUsername = "test";
        String requestPassword = request.getParameter("password"); // "test" - 성공 시나리오, 나머지는 실패

        UserDetails authenticatedUser = customUserDetailsService.loadUserByUsername(requestUsername);

        UsernamePasswordAuthenticationToken authToken = null;
        // 여기에서 authenticatedUser와 request에 담긴 username과 password를 비교하는 로직
        if(requestUsername.equals(authenticatedUser.getUsername()) && requestPassword.equals(authenticatedUser.getPassword())){
            // 아이디, 패스워드 일치
             authToken = new UsernamePasswordAuthenticationToken(authenticatedUser.getUsername(), null, null);
        }else{
            // 아이디, 패스워드 불일치
             authToken = new UsernamePasswordAuthenticationToken(authenticatedUser.getUsername(), null);
        }

        // 인증시키고, 토큰을 SecurityContext에 저장
        SecurityContextHolder.getContext().setAuthentication(authToken);

        filterChain.doFilter(request, response);
    }
}
속성
설명
new UsernamePasswordAuthenticationToken(authenticatedUser.getUsername(), null, null);UsernamePasswordAuthenticationToken 내부 생성자 로직 - 인증 O
super.setAuthenticated(true);
new UsernamePasswordAuthenticationToken(authenticatedUser.getUsername(), null);UsernamePasswordAuthenticationToken 내부 생성자 로직 - 인증 X
super.setAuthenticated(false);

1-4 테스트

  • /api/v1/test/auth?password=test

  • /api/v1/test/auth?password=failure

password=test만 통과되고, 나머지는 401 Error!


2. AccessDeniedHandler

  • 403 Error 처리 (AccessDeniedException)
    .antMatchers("/api/v1/test/auth").hasRole("AUTH")hasRole()이 세부 인가 여부에 따라 403Error를 뱉으면 AccessDeniedHandler 에서 처리한다.

2-1. CustomAccessDeniedHandler 추가

package com.codesign.base.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        log.error("Forbidden error: {}", accessDeniedException.getMessage());

        Map<String, Object> responseMap = new HashMap<>();
        responseMap.put("statue", HttpStatus.FORBIDDEN.value());
        responseMap.put("message", "Forbidden error ");

        response.setCharacterEncoding("utf-8");
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.FORBIDDEN.value());

        try(ServletOutputStream os = response.getOutputStream()){
            objectMapper.writeValue(os, responseMap);
            os.flush();
        }

    }
}

2-2. accessDeniedHandler(), hasRole("AUTH") 체인 추가

securityConfig.java

  • customAccessDeniedHandler 추가
  @Autowired
  CustomAccessDeniedHandler customAccessDeniedHandler;
  • accessDeniedHandler() 추가
    .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(customAccessDeniedHandler).and()

  • hasRole("AUTH")로 수정
    .antMatchers("/api/v1/test/auth").hasRole("AUTH")


package com.codesign.base.configure;

import com.codesign.base.security.CustomAccessDeniedHandler;
import com.codesign.base.security.SecurityAuthenticationEntryPoint;
import com.codesign.base.security.filter.SecurityAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    SecurityAuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    CustomAccessDeniedHandler customAccessDeniedHandler;

    @Bean
    public SecurityAuthenticationFilter securityAuthenticationFilter(){
        return new SecurityAuthenticationFilter();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors().and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(customAccessDeniedHandler).and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/api/v1/test/permit-all").permitAll()
//                .antMatchers("/api/v1/test/auth").authenticated()
                .antMatchers("/api/v1/test/auth").hasRole("AUTH")
//                .antMatchers("/**").authenticated()
//                .anyRequest().permitAll().and()
                .anyRequest().authenticated().and()
                .formLogin().disable()
        ;

        http.addFilterBefore(securityAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }
}

2-3. 테스트

  • /api/v1/test/auth?password=test

    .hasRole('AUTH') 추가로 권한이 없어서 403 Error!

2-4. hasRole('AUTH') 통과 시키기

  • SecurityUser.java
    권한을 받을수 있는 생성자 추가
public class SecurityUser implements UserDetails {

    private String username;
    private Collection<? extends GrantedAuthority> authorities;

    public SecurityUser(String username, List<String> roles) {
        this.username = username;
        this.authorities = Optional.ofNullable(roles)
                .orElse(Collections.emptyList())
                .stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }
    
    ... 이하 동일

  • SecurityAuthenticationFilter.java
    아이디, 패스워드 일치시, authenticatedUser.getAuthorities() 넣어주기
      if(requestUsername.equals(authenticatedUser.getUsername()) && requestPassword.equals(authenticatedUser.getPassword())){
            // 아이디, 패스워드 일치
             authToken = new UsernamePasswordAuthenticationToken(authenticatedUser.getUsername(), null, authenticatedUser.getAuthorities());
        }else{
            // 아이디, 패스워드 불일치
             authToken = new UsernamePasswordAuthenticationToken(authenticatedUser.getUsername(), null);
        }

  • CustomUserDetailsService.java
    return new SecurityUser(username, Arrays.asList("ROLE_AUTH")); 권한을 받는 생성자로 변경
package com.codesign.base.security.service;

import com.codesign.base.security.model.SecurityUser;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Arrays;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.out.println(" UserDetailsService 인증을 받습니다. ");
        // TODO id만으로 db에서 user정보를 조회

        if (!username.equals("test")) throw new UsernameNotFoundException("해당 유저가 존재하지 않습니다.");

        return new SecurityUser(username, Arrays.asList("ROLE_AUTH"));
//        return new SecurityUser(username, Arrays.asList("ROLE_MNG"));
    }
}

hasRole("AUTH") === ROLE_AUTH >>> prefix - ROLE_ 를 알아서 붙여준다.

2-5 테스트

  • hasRole("AUTH") 통과 확인
  • 다른 권한일 경우 - 403 Error
//        return new SecurityUser(username, Arrays.asList("ROLE_AUTH"));
        return new SecurityUser(username, Arrays.asList("ROLE_MNG"));

참고 https://sas-study.tistory.com/362?category=784778
https://blog.naver.com/PostView.nhn?blogId=qjawnswkd&logNo=222303477758

profile
Design Awesome Style Code

0개의 댓글