0+ 스프링/0+ 스프링 Security

[스프링 시큐리티] 6. 기본 API 및 Filter 이해(인증/인가 예외 처리 -ExceptionTranslationFilter, RequestCacheAwareFilter)

힘들면힘을내는쿼카 2023. 1. 9. 17:24
728x90
반응형

[스프링 시큐리티] 6. 기본 API 및 Filter 이해(인증/인가 예외 처리 -ExceptionTranslationFilter, RequestCacheAwareFilter)

해당 포스팅은 인프런에서 스프링 시큐리티 정수원님의 강의를 참고하여 작성했습니다.
스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 인프런 | 강의

 

스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 인프런 | 강의

초급에서 중.고급에 이르기까지 스프링 시큐리티의 기본 개념부터 API 사용법과 내부 아키텍처를 학습하게 되고 이를 바탕으로 실전 프로젝트를 완성해 나감으로써 스프링 시큐리티의 인증과

www.inflearn.com

 

인증과 인가

 

사용자가 서버에 접근하기 위해서는 먼저 인증을 받습니다.
여기서 인증된 사용자가 아닐 경우 예외를 처리해야합니다.
예를 들어 “아이디 혹은 비밀번호를 확인해 주세요” 와 같은 예외 처리 처럼요.

인증이 완료된 사용자는 권한이 존재하게 됩니다.
권한에 따라서 인가된 자원만이 접근 가능합니다.
만약 해당 자원에 접근 권한이 없는 사용자는 “회원님은 해당 메뉴를 선택할 수 없습니다.” 같은 예외처리를 해야합니다.

스프링 시큐리티에서 인증과 인가에 대한 예외처리를 어떻게 할 수 있는지 알아봅시다.

ExceptionTranslationFilter

  • AuthenticationException, AccessDeniedException를 호출하는 필터 입니다.

AuthenticationException

인증 예외 처리 클래스 입니다.

  • AuthenticationEntryPoint(인터페이스)를 호출
    • 로그인 페이지 이동 또는 401 전달
  • 인증 예외가 발생하기 전의 요청 정보를 저장합니다.
    • RequestCache
      • 사용자 이전 요청 정보를 세션에 저장하고 이를 꺼내오는 캐시 메커니즘 입니다.
    • SavedRequest
      • 사용자가 요청했던 request 파라미터 값들, 그 당시의 헤더값 등이 저장됩니다.

 

AccessDeniedException

  • 인가 예외 처리
    • AccessDeniedHandler에서 예외 처리

인증, 인가 처리 과정

  • 부가설명
    • fully authenticated 정책을 설정한 상황에서
      remember-me 인증 객체가 생성 되었다면 AccessDeniedHandler로 이동하지 않고 AuthenticationException으로 이동 합니다.
    •  remember-me 인증
      fully authenticated를 의미하지는 않기 때문입니다.
      • fully authenticated는 로그인 행위에 있어
        id, password와 같은 신원증명을 할 수 있는것을 의미합니다.
    • remember-me의 토큰이
      만료 되었을 경우에도 인증 예외가 발생합니다.

 

실습

MyAccessDeniedHandler

인증 예외 처리

/**
 * 인증 예외 처리
 */
@Slf4j
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException)
            throws IOException, ServletException {

        String encode = URLEncoder.encode(authException.getMessage(), StandardCharsets.UTF_8);
        response.sendRedirect("/login?exception="+encode);
    }
}

MyAuthenticationEntryPoint

인가 예외 처리

/**
 * 인가 예외 처리
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException)
            throws IOException, ServletException {

        String encode = URLEncoder.encode(accessDeniedException.getMessage(), StandardCharsets.UTF_8);
        response.sendRedirect("/denied?exception="+encode);
    }
}

LoginSuccessHandler

/**
 * 로그인 성공 후 처리
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    // 사용자 이전 요청 정보를 세션에 저장하고 이를 꺼내오는 캐시
    private final RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        // 사용자 이전 요청 정보 조회
        SavedRequest savedRequest = requestCache.getRequest(request, response);

        if(savedRequest != null) {
            // 사용자 이전 요청 url로 redirect
            response.sendRedirect(savedRequest.getRedirectUrl());
        } else {
            response.sendRedirect("/"); // root page로 이동
        }
    }
}

MySecurityConfig

@EnableWebSecurity
@RequiredArgsConstructor
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    private final MyAuthenticationEntryPoint myAuthenticationEntryPoint;
    private final MyAccessDeniedHandler myAccessDeniedHandler;
    private final LoginSuccessHandler loginSuccessHandler;

    /**
     * 권한 설정
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 메모리 방식
        auth.inMemoryAuthentication().withUser("user").password("{noop}123").roles("USER");
        auth.inMemoryAuthentication().withUser("manager").password("{noop}123").roles("MANAGER");
        auth.inMemoryAuthentication().withUser("admin").password("{noop}123").roles("ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                .antMatchers("/user").hasRole("USER")
                .antMatchers("/manager").hasRole("MANAGER")
                .antMatchers("/admin-manager").access("hasRole('ADMIN') or hasRole('MANAGER')")
                .anyRequest().authenticated();

        http
                .formLogin()
                .successHandler(loginSuccessHandler); // 로그인 성공 후 로직

        http
                .exceptionHandling()
                .authenticationEntryPoint(myAuthenticationEntryPoint) // 인증 예외
                .accessDeniedHandler(myAccessDeniedHandler); // 인가 예외

        http
                .csrf().disable();
    }
}

MyController

@Controller
public class MyController {
    @ResponseBody
    @GetMapping("/user")
    public String user() {
        return "user";
    }

    @ResponseBody
    @GetMapping("/manager")
    public String manager() {
        return "manager";
    }

    @ResponseBody
    @GetMapping("/admin-manager")
    public String adminManager() {
        return "admin-manager";
    }

    @ResponseBody
    @GetMapping("/denied")
    public String denied(@RequestParam(value = "exception", required = false) String exception) {
        return exception;
    }

    @GetMapping("/login")
    public String login(@RequestParam(value = "exception", required = false) String exception,
                        Model model) {

        model.addAttribute("exception", exception);

        return "login";
    }

    @ResponseBody
    @GetMapping("/hello")
    public String hello() {
        return "hello! 사용자 이전 요청 페이지";
    }

    @ResponseBody
    @GetMapping("/")
    public String root() {
        return "root page";
    }
}

login.html

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Please sign in</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" 
    rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" 
    rel="stylesheet" crossorigin="anonymous"/>
</head>
<body>
<div class="container">
    <form class="form-signin" method="post" action="/login">
        <h2 class="form-signin-heading">Please sign in</h2>
        <h2 th:if="${exception != null}" th:text="${exception}" style="color: red"></h2>
        <p>
            <label for="username" class="sr-only">Username</label>
            <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
        </p>
        <p>
            <label for="password" class="sr-only">Password</label>
            <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
        </p>
<!--        <input name="_csrf" type="hidden" value="825947ce-e260-42a1-8395-bd08a01dcc72" />-->
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
    </form>
</div>
</body></html>

결과

  • GET localhost:8080/hello
    • RequestCache에 해당 정보 저장
  • 인증 예외 발생 localhost:8080/login?exception=Full+authentication+is+required+to+access+this+resource
  • user계정으로 로그인 후 LoginSuccessHandler에서 RequestCache를 이용해서 localhost:8080/helloredirect
  • GET localhost:8080/manager
  • 인가 예외 발생 GET localhost:8080/denied?exception=Access+is+denied
    • user는 manager권한이 아님

 

 

 

728x90
반응형