슬기로운 개발생활

Spring Boot 게시판 Security 로그인 실패시 메시지 출력하기

by coco3o
반응형

로그인 화면을 커스터마이징 했지만 부족한 부분들이 있다. 만약 로그인 페이지에서 잘못된 로그인 정보를 입력하면,

로그인에 실패했지만 아무런 메시지를 보지 못하고 로그인 페이지만 재로딩될 것이다.

로그인에 실패했으면 어떤 이유로 실패했는지 에러 메시지를 띄워주어야 한다고 생각하기 때문에,

이번 포스팅에서는 로그인 실패시 에러 메시지를 보여주는 것을 목표로 하고 구현하려 한다.


1. SecirutyConfig

@RequiredArgsConstructor
@Configuration 
@EnableWebSecurity 
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomUserDetailsService customUserDetailsService;
    /* 로그인 실패 핸들러 의존성 주입 */
    private final AuthenticationFailureHandler customFailureHandler;

    @Bean
    public BCryptPasswordEncoder Encoder() {
        return new BCryptPasswordEncoder();
    }
    
    /* 시큐리티가 로그인 과정에서 password를 가로챌때 어떤 해쉬로 암호화 했는지 확인 */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(customUserDetailsService).passwordEncoder(Encoder());
    }

    /* static 관련설정은 무시 */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web
                .ignoring().antMatchers( "/css/**", "/js/**", "/img/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().ignoringAntMatchers("/api/**") /* REST API 사용 예외처리 */
                .and()
                .authorizeRequests()
                .antMatchers("/", "/auth/**", "/posts/read/**", "/posts/search/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/auth/login")
                .loginProcessingUrl("/auth/loginProc")
                .failureHandler(customFailureHandler) // 로그인 실패 핸들러
                .defaultSuccessUrl("/")
                .and()
                .logout()
                .invalidateHttpSession(true).deleteCookies("JSESSIONID");
    }
}

추가된 부분은 다음과 같다.

  • private final AuthenticationFailureHandler customFailureHandler
  • .failureHandler(customFailureHandler)

SecurityConfig에 실패 핸들러를 적용해줬다.


2. CustomAuthFailureHandler

@Component
public class CustomAuthFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        String errorMessage;
        if (exception instanceof BadCredentialsException) {
            errorMessage = "아이디 또는 비밀번호가 맞지 않습니다. 다시 확인해 주세요.";
        } else if (exception instanceof InternalAuthenticationServiceException) {
            errorMessage = "내부적으로 발생한 시스템 문제로 인해 요청을 처리할 수 없습니다. 관리자에게 문의하세요.";
        } else if (exception instanceof UsernameNotFoundException) {
            errorMessage = "계정이 존재하지 않습니다. 회원가입 진행 후 로그인 해주세요.";
        } else if (exception instanceof AuthenticationCredentialsNotFoundException) {
            errorMessage = "인증 요청이 거부되었습니다. 관리자에게 문의하세요.";
        } else {
            errorMessage = "알 수 없는 이유로 로그인에 실패하였습니다 관리자에게 문의하세요.";
        }
        setDefaultFailureUrl("/auth/login?error=true&exception="+errorMessage);

        super.onAuthenticationFailure(request, response, exception);
    }
}

onAuthenticationFailure를 오버라이딩하여 예외처리를 해주고, 에러 메시지를 띄워준다.


3. Controller

    @GetMapping("/auth/login")
    public String login(@RequestParam(value = "error", required = false)String error,
                        @RequestParam(value = "exception", required = false)String exception,
                        Model model) {
        model.addAttribute("error", error);
        model.addAttribute("exception", exception);
        return "/user/user-login";
    }

error와 exception을 model에 담아 넘겨준다.


4. user-login.mustache

{{>layout/header}}
<div id="posts_list">
    <div class="container col-md-6">
        <form action="/auth/loginProc" method="post">
            <input type="hidden" name="_csrf" value="{{_csrf.token}}"/>
            <div class="form-group">
                <label>아이디</label>
                <input type="text" class="form-control" name="username" placeholder="아이디를 입력해주세요">
            </div>

            <div class="form-group">
                <label>비밀번호</label>
                <input type="password" class="form-control" name="password" placeholder="비밀번호를 입력해주세요">
            </div>

            <span>
                {{#error}}
                    <p id="valid" class="alert alert-danger">{{exception}}</p>
                {{/error}}
            </span>
                <button class="form-control btn btn-primary bi bi-lock-fill"> 로그인</button>
        </form>
    </div>
</div>
{{>layout/footer}}

{{#error}}
<p id="valid" class="alert alert-danger">{{exception}}</p>
{{/error}}
으로 묶어서 error가 있다면, exception 에러메시지를 보여주도록 추가했다.


5. 결과 확인

??????????????????????????

에러 메시지가 한글이라서 저렇게 출력되는걸까 하고 메시지를 영문으로 바꿔서 다시 해봤다.

영문처리는 정상적으로 된다. URL 부분을 다시 확인 해보자

  • 한글 예외 메시지 URL

/auth/login?error=true&exception=???%20??%20?????%20??%20????.%20?%2..

  • 영문 예외 메시지 URL

/auth/login?error=true&exception=ID%20or%20password%20does%20not%20match.

// URL에서 인코딩 오류 발생
setDefaultFailureUrl("/auth/login?error=true&exception="+errorMessage);

한글 문자 자체는 브라우저에서 URL에 맞도록 자동으로 인코딩을 해주지 않기 때문에 문제가 발생한 것이였다.
그래서 다음과 같이 onAuthenticationFailure 메서드에 UTF-8 인코딩 처리부분을 추가해줬다.

errorMessage = URLEncoder.encode(errorMessage, "UTF-8");

이제 한글이 정상적으로 출력된다.

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기