본문 바로가기
관리자

Programming-[Backend]/Spring Security

[스프링 시큐리티] 12. DB 연동 인증 처리 : AuthenticationProvider, 로그인 및 로그아웃 페이지 처리

728x90
반응형

 

1. DB 연동 인증 : AuthenticationProvider 사용

 

이전 글에서는 AuthenticationManager 단에서 userDetailsService 메서드를 활용해서 인증을 처리했다. SecurityConfig에서 해당 메서드를 불러와서 처리되는 과정까지만 학습했다. 이번 글에서는 한 단계 더 나아가서, AuthenticationManager에서 호출하는 AuthenticationProvider의 기능을 구현해본다.

 

 

SecurityConfig - authenticationProvider

 

SecurityConfig 클래스에서는 이전에 사용했던 userDetailsService 대신 authenticationProvider 메서드를 사용한다. 여기서 authenticationProvider()를 생성하는데, @Bean 형태로 AuthenticationProvider를 상속받는 CustomAuthenticationProvider를 등록해준다.

 

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        auth.userDetailsService(userDetailsService);
        auth.authenticationProvider(authenticationProvider());
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        return new CustomAuthenticationProvider(userDetailsService, passwordEncoder());
    }

 

 

CustomAuthenticationProvider

 

AuthenticationProvider의 기본 메서드인 supports와 authenticate 메서드를 Override한다. AuthenticationProvider에서는 기본적으로 AuthenticationManager에서 전달해주는 Authentication 객체를 인자로 받는다. 

 

@AllArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;

    private final PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //설계나 정책에 따라서 인증 메서드를 다양하게 구현할 수 있다. 아래는 가장 기본적인 예시이다.

        String username = authentication.getName();
        String password = (String) authentication.getCredentials(); //Object 타입으로 반환되어 캐스팅

        AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(username); //직접 만든 클래스로 캐스팅

        if(!passwordEncoder.matches(password, accountContext.getAccount().getPassword())) {
            throw new BadCredentialsException("BadCredentialsException");
        }

        //인증 성공 시 성공한 인증 객체를 생성 후 반환
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(accountContext.getAccount(), null, accountContext.getAuthorities());

        return authenticationToken;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        //전달된 파라미터의 타입이 UsernamePasswordAuthenticationToken 타입과 일치하는지 검사한다.
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

 

supports 에서는 해당 authentication의 타입이 UsernamePasswordAuthenticationToken 타입인지 검사한다. 이렇게 하지 않고 다른 방식으로 구현할 수도 있다.

 

supports를 통과한 뒤 authenticate 메서드가 호출된다. 위 예시코드에서는 복잡한 내용은 없고, 사용자 요청으로부터 전달된 Authentication에 있는 비밀번호와 userDetailsService.loadUserbyUsername(username) 메서드를 통해 DB에서 조회한 유저의 비밀번호가 일치하는지만 확인한다. 그리고 인증에 성공한 객체를 DB에서 가져온 권한 정보도 담아서UsernamePasswordAuthenticationToken 타입으로 반환한다.

 

만약 JWT token등을 사용하고, DB에 저장하고 있다면 이러한 정보들을 추가로 Token에 담아서 전달해줄 수도 있을 것이다. 인증 과정을 얼마든지 Override를 통해 커스터마이징할 수 있는 점이 좋다.

 

이후 ProviderManager의 authenticate 메서드를 디버깅해보면, AuthenticationManager로부터 전달된 authentication 객체와, this.providers가 직접 작성한 CustomeAuthenticationProvider로 잘 생성되어 처리되는 것을 볼 수 있다.

 

 

 

 


 

2. 로그인 페이지 작성해보기

 

스프링부트가 제공하는 로그인 페이지가 아니라 직접 작성하는 로그인 페이지를 만들어본다. 우선 SecurityConfig의 configure 메서드에서 아래 코드를 추가해준다. loginPage, loginProcessingUrl, defaultSuccessUrl을 추가한다. 또한 해당 페이지들은 권한없이 접근이 가능해야하므로 permitAll을 적어준다.

 

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .authorizeRequests()
            .antMatchers("/", "/users").permitAll()
            .antMatchers("/mypage").hasRole("USER")
            .antMatchers("/manager").hasRole("MANAGER")
            .antMatchers("/admin").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login")
            .loginProcessingUrl("/login_proc")
            .defaultSuccessUrl("/")
            .permitAll()
    ;

}

 

간단하다. Controller 및 login.html 페이지 코드는 생략한다.

 

 


 

3. 로그아웃 및 화면 보안 처리

 

로그아웃 처리 방식

 

로그아웃은 다음의 두 가지로 가능하다.

 

  • <form> 태그 사용 + POST 요청
  • <a> 태그 사용 + GET 요청

 

POST 요청 시에는 logoutFilter를 타고 SecurityContextLogoutHandler에서 처리되지만, GET 요청 시에는 SecurityContextLogoutHandler에서 바로 처리되는 형태이다.

 

 

Thymeleaf - Spring Security 연동

 

Spring Security의 인증 상태에 따라 Thymeleaf의 view 화면을 다르게 할 수 있는 라이브러리가 thymeleaf-extras-springsecurity5이다. pom.xml에 아래와 같이 dependency를 추가한다.

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

 

 

화면 상단에 위치한 부분이 강의에서 구성한 top.html 파일 부분인데, 인증 전에는 로그인, 회원가입 등의 버튼이 보이다가, 인증 후에는 로그아웃 버튼이 보이고 관리자 권한으로 접속 시 관리자 버튼이 보이는 등 security의 상태에 따라 view를 달리 해줄 수 있다.

 

아래와 같이 "xmlns:sec = ..." 네임 스페이스를 html 태그에 추가해준다. 그리고 실제 버튼을 적용하기 위해서 "sec:authorize" 구문을 사용한다.

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<div th:fragment="header">
    <nav class="navbar navbar-dark sticky-top bg-dark ">
        <div class="container">
            <a class="text-light" href="#"><h4>Core Spring Security</h4></a>
            <ul class="nav justify-content-end">
                <li class="nav-item" sec:authorize="isAnonymous()"><a class="nav-link text-light" th:href="@{/login}">로그인</a></li>
                <li class="nav-item" sec:authorize="isAnonymous()"><a class="nav-link text-light" th:href="@{/accounts}">회원가입</a></li>
                <li class="nav-item" sec:authorize="isAuthenticated()"><a class="nav-link text-light" th:href="@{/logout}">로그아웃</a></li>
                <li class="nav-item" sec:authorize="hasAuthority('ROLE_ADMIN')"><a class="nav-link text-light" th:href="@{/admin}">관리자</a></li>
                <li class="nav-item" ><a class="nav-link text-light" href="/">HOME</a></li>
                <li class="nav-item" ><a class="nav-link text-light" th:href="@{/users}">회원가입</a></li>
            </ul>
        </div>
    </nav>
</div>
</html>

 

 

로그아웃 처리

 

로그아웃 처리는 Controller에서 authentication 객체를 호출하여 무효화하는 방식으로 진행한다. 로그인이 되어있는 상태라면 SecurityContextHolder에 authentication 객체가 저장된 상태일 것이므로 이를 불러오고, SecurityContextLogoutHandler의 logout 메서드에 request, response, authentication을 전달하여 logout 처리가 되도록 한다.

 

@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
   Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

   if (authentication != null) {
      new SecurityContextLogoutHandler().logout(request, response, authentication); //로그아웃 필터를 통해서 처리함
   }
   return "redirect:/login";
}

 

SecurityContextLogoutHandler의 logout 메서드를 한번 더 살펴보면, session.invalidate를 통해 session을 무효화 시키고, SecurityContextHolder내의 authentication 객체를 null로 만들고, SecurityContextHolder의 clearContext를 실행하여 인증 정보를 제거하는 것을 확인할 수 있다.

public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
    Assert.notNull(request, "HttpServletRequest required");
    if (this.invalidateHttpSession) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            this.logger.debug("Invalidating session: " + session.getId());
            session.invalidate();
        }
    }

    if (this.clearAuthentication) {
        SecurityContext context = SecurityContextHolder.getContext();
        context.setAuthentication((Authentication)null);
    }

    SecurityContextHolder.clearContext();
}

 

 

 

 

 


 

참조

 

1) 인프런 - 스프링 시큐리티 - Spring Boot 기반으로 개발하는 Spring Security - 정수원님 강의

https://www.inflearn.com/course/%EC%BD%94%EC%96%B4-%EC%8A%A4%ED%94%84%EB%A7%81-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0

 

728x90
반응형