Spring | security success & failure handler

DoItDev·2021년 11월 2일
3
post-thumbnail

Success & Failure Handler

스크린샷 2021-11-07 오후 12 19 45

스프링 시큐리티에서 success & failuare handler (이하 handler) 의 경우 AuthenticationProvider에서 인증에 대하여 성공/실패의 유무에 대한 액션을 취한다.

Success Handler

공식적인 설명은 아래와 같다.

- 성공적인 사용자 인증을 처리하는 데 사용되는 전략입니다.
- 구현은 원하는 대로 할 수 있지만 일반적인 동작은 제어하는 것입니다.
- 후속 목적지로의 탐색 (리디렉션 또는 포워드 사용)
  예를 들어, 사용자가 로그인 양식을 제출하여 로그인한 후 애플리케이션에서 결정해야 합니다.

일단 우리가 로그인을 성공했을때 예를들어 유저의 DB에서 count 를 치거나 아니면 유저가 로그인 시간을 log 에 남긴다는 요구사항이 나오면 아래의 핸들러에서 핸들링 하면 된다.

onAuthenticationSuccess 메소드에서 첫번째 메소드의 경우 사용자가 성공적으로 인증되면 호출됩니다

  • HttpServletRequest : 요청 객체
  • HttpServletResponse : 응답 객체
  • FilterChain : 다음 필터를 호출하기 위한 객체
  • Authentication : 인증된 객체
@Component(value = "authenticationSuccessHandler")
public class DomainSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
	 // 유저 성공 로직을 추가 해준다.
 AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
    }

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

    }
}

Failure Handler

공식적인 설명의 경우 아래와 같습니다

- 실패한 인증 시도를 처리하는 데 사용되는 전략입니다.
- 일반적인 동작은 사용자를 인증 페이지로 리디렉션하는 것일 수 있습니다(이 경우 양식 로그인)을 사용하여 다시 시도할 수 있습니다. 더 정교한 논리는
- 예외 유형에 따라 구현됩니다.

로그인 실패 핸들러의 경우 onAuthenticationFailure 메소드로 인증 시도가 실패할 때 호출됩니다. 메소드의 인자에서 AuthenticationException 예외의 종류로는 아래와 같습니다.

  • BadCredentialsException : 비밀번호불일치
  • UsernameNotFoundException : 계정없음
  • AccountExpiredException : 계정만료
  • CredentialsExpiredException : 비밀번호만료
  • DisabledException : 계정비활성화
  • LockedException : 계정잠김

따로 메소드를 만들어서 getExceptionMessage 라는 메소드를 만들어서 예외를 체크해서 메세지를 만들어 줍니다.

writePrintErrorResponse의 메소드는 응답객체에 메세지를 만들기 위해서 작성된 메소드 입니다.

여기서 핸들링할때 성공 핸들링과 동일하게 로그를 남겨도되고 db작업이나 로그인 실패시 행위를 핸들링하면 됩니다.

@Component(value = "authenticationFailureHandler")
public class DomainFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {

	// 실패로직 핸들링

        exception.printStackTrace();

        writePrintErrorResponse(response, exception);
    }

    private void writePrintErrorResponse(HttpServletResponse response, AuthenticationException exception) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();

            Map<String, Object> responseMap = new HashMap<>();

            String message = getExceptionMessage(exception);

            responseMap.put("status", 401);

            responseMap.put("message", message);

            response.getOutputStream().println(objectMapper.writeValueAsString(responseMap));

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String getExceptionMessage(AuthenticationException exception) {
        if (exception instanceof BadCredentialsException) {
            return "비밀번호불일치";
        } else if (exception instanceof UsernameNotFoundException) {
            return "계정없음";
        } else if (exception instanceof AccountExpiredException) {
            return "계정만료";
        } else if (exception instanceof CredentialsExpiredException) {
            return "비밀번호만료";
        } else if (exception instanceof DisabledException) {
            return "계정비활성화";
        } else if (exception instanceof LockedException) {
            return "계정잠김";
        } else {
            return "확인된 에러가 없습니다.";
        }
    }

}

exception check logic update

위의 getExceptionMessage 의 경우 if 절이 너무 많이 사용하는 것 같아서

로직을 변경 하였다.

enum으로 처리하려고 생각을 하였다.

일단 enum을 생성을 하였다.

public enum AuthenticationTypes implements BaseEnumCode<String> {

    BadCredentialsException("비밀번호불일치", "비밀번호불일치"),
    UsernameNotFoundException("계정없음", "계정없음"),
    AccountExpiredException("계정만료", "계정만료"),
    CredentialsExpiredException("비밀번호만료", "비밀번호만료"),
    DisabledException("계정비활성화", "계정비활성화"),
    LockedException("계정잠김", "계정잠김"),
    NoneException("알수없는 에러", "알 수 없는 에러 입니다.");

    private String desc;

    private String value;

    AuthenticationTypes(String desc, String value) {
        this.desc = desc;
        this.value = value;
    }

    @Override
    public String getValue() {
        return this.value;
    }

    private static final Map<String, AuthenticationTypes> descriptions = Collections.unmodifiableMap(Stream.of(values()).collect(Collectors.toMap(AuthenticationTypes::name, Function.identity())));

    public static AuthenticationTypes findOf(String findValue) {
        return Optional.ofNullable(descriptions.get(findValue)).orElse(NoneException);
    }

}

위의 코드 처럼 enum을 작성을 하였다.

value 의 경우 excetpin에서 내려줄 메세지라고 생각하면 된다.

그리고 desc 의 경우 단순한 설명이다.

그리고 getExceptionMessage의 경우 아래의 코드 처럼 변경을 하였다.

    private String getExceptionMessage(AuthenticationException exception) {
        AuthenticationTypes authenticationTypes = AuthenticationTypes.findOf(exception.getClass().getSimpleName());
        return authenticationTypes.getValue();
    }

exception.getClass().getSimpleName() 의 경우 디버깅 해서 확인했더니 단순한 클래스 이름을 string으로 변환해서 내려준다.

그렇기 때문에 enum에서 name 값과 비교하여서 만들어 주면 될거 같다라는 생각을 하였다.

여기서 findOf 메소드의 경우 Map 을 통하여 값을 enum 타입에 대한 값을 찾아준다

optional 을 통하여 null 일시 알 수 없는 에러 (NoneException)로 정의해서 내려준다.

SecurityConfiguration

시큐리티 설정 java 파일에서는 config 메소드에서 HttpSecurity객체에 아래와 같이 설정을 해주면 빈을 전역변수로 authenticationSuccessHandler,authenticationFailureHandler를 선언(빈생성)을 해주었다면 @Component어노테이션에서 설정을 해주었기 때문에 자동적으로 custom handler 가 자동으로 빈 등록을 시켜준다.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
	           .antMatchers("/**")
                .permitAll()
                .and()
                .formLogin()
        		.loginPage("/login")
                .loginProcessingUrl("/login-process")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .permitAll()
        ;
    }

Note:

  • 여기서 만약에 defaultSuccessUrl 을 설정을 해준다면 successHandler가 불러오지 못합니다.
  • 그렇기 때문에 defaultSuccessUrl를 세팅 해주지말구 하시면 성공적 핸들링을 사용이 가능합니다

Custom Login Page

로그인 페이지를 커스텀 하기 위해서는 시큐리티 설정 java 파일에서 아래와 같이 설정을 해줍니다.

      http.formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login-progress")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .permitAll();

loginPage 라는 항목이 있는데 시큐리티에서 제공되는 폼이 아니라 개발자가 만는 폼을 매핑 시켜줄 수 있습니다

그렇게 html 파일을 매핑을 해줄 수 있는데

화면 캡처 2021-11-19 093131

아래의 화면이 보여집니다. 여기서 webconfiguration 에서 뷰를 매핑을 시켜주는 방법과 controller에서 매핑을 해주는 방법중 선택해서 매핑을 시켜줍니다

그 후에 타임리프를 사용을 하기 때문에 csrf 설정을 넣어준 후 success 되는 것을 볼 수 있을 것입니다.

단, 폼에서 loginProcessingUrl action 값을 여기 설정되어있는 url 로 설정 하시면 됩니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>

<form action='/login-progress' method='POST'>
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
    <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>
    <button class="btn btn-lg btn-primary btn-block" type="submit">Login</button>
</form>

</body>
</html>
profile
Back-End Engineer

0개의 댓글