본문 바로가기
Auth (Authentication & Authorization)

Spring Security를 이용한 Auth 구현

by kellis 2020. 10. 12.

앞서 Spring Security를 사용하지 않고 Interceptor를 이용한 A/A 구현 및 Spring Security의 구조 등을 살펴보았습니다. 이 포스트에서는 이를 기반으로 Spring Security를 이용하여 A/A 기능을 구현해 보도록 하겠습니다. 

 

(1) Maven dependency추가 

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>

 

(2) Spring-security config

보안 흐름을 정의하기 위해서 WebSecurityConfigurerAdapter를 확장합니다. 여기에서 다음과 같은 설정들을 할 수 있습니다.

  • 어떤 요청에 대해서 인증을 요구할 것인지
  • 특정 요청에 대해서 어떤 권한을 요구할 것인지
  • 인증되지 않은 요청을 어떤 url로 redirect 시킬지
  • 로그인이 성공하면 어느 화면으로 이동시킬지
  • logout 요청 시 어떤 작업을 수행시킬지
  • 권한이 맞지 않아 403 에러가 발생할 경우 어떻게 처리할지
  • Authentication 정보(username, password)가 유효한 정보인지 체크하는 방법
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthProvider authProvider;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/", "/main").permitAll()
                .antMatchers("/admin/*").hasAuthority("ADMIN")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .usernameParameter("userId")
                .successHandler(new LoginSuccessHandler("/home"))
                .permitAll()
                .and()
            .logout()
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()
                .and()
            .authenticationProvider(authProvider)
            .exceptionHandling().accessDeniedHandler(accessDeniedHandler());
    }
}
  • 11라인 : authorizeRequest() 요청에 대한 인증/권한 설정을 담당합니다.
  • 12라인 : "/", "main" 요청은 인증 체크를 하지 않습니다.
  • 13라인 : "admin/*"에 해당하는 요청은 "ADMIN" 권한이 있어야 접근 가능합니다. 
  • 14라인 : 위에서 정의한 요청을 제외한 모든 요청에 대해서 인증을 요구합니다.
  • 16라인 : 로그인 관련 설정을 담당합니다.
  • 17라인 : 아직 인증 전 상태이며 인증을 요구하는 요청이라면 "/login"으로 redirect 시킵니다.
  • 18라인 : 앞서 말했듯 Spring-security는 기본적으로 로그인 폼으로부터 오는 데이터를 username과 password로 인식하고 있습니다. 따라서 jsp에서 input의 name을 username/password로 일치시켜주어야 합니다.
<form action="/login" method="post">
        <input type="text" id="username" name="username"/>
        <input type="password" id="password" name="password"/>
        <button type="submit">Log in</button>
</form>

이를 변경하길 원한다면, usernameParameter, passwordParameter 설정을 해주어야 합니다. 이 글에서는 username대신 loginId로 명명했기 때문에 usernameParameter 설정을 했습니다.

<form action="/login" method="post">
        <input type="text" id="loginId" name="loginId"/>
        <input type="password" id="password" name="password"/>
        <button type="submit">Log in</button>
</form>
  • 19라인 : 인증에 성공했을 때 어떤 URL로 Redirect 시킬지 정의합니다. 이를 위해 AuthenticationSuccessHandler를 구현했고, 자세한 내용은(3) AuthenticationSuccessHandler 구현에 있습니다.
  • 22라인 : 로그아웃 관련 설정을 담당합니다.
  • 23라인 : 로그아웃 요청이 오면 Session을 무효화합니다.
  • 24라인 : 로그아웃 요청이 오면 키-JSESSIONID로 저장된 쿠키를 제거합니다.
  • 27라인 : 로그인 폼으로부터 오는 데이터가 유효한 계정 정보인지를 판단하기 위해 AuthenticationProvider를 구현했고, 자세한 내용은 (4) AuthenticationProvider 구현에서 다루겠습니다.
  • 28라인 : 인증에는 성공했으나 권한이 맞지 않을 경우 실행될 Handler를 등록합니다. 이는 (5) AccessDeniedHandler 구현에서 다루겠습니다. 

 

(3) AuthenticationSuccessHandler 구현

인증 성공 후에 Redirect 시킬 URL을 설정하기 위해 AuthenticationSuccessHandler를 구현해야 합니다. Spring-security는 AuthenticationSuccessHandler의 구현체인 SavedRequestAwareAuthenticationSuccessHandler를 내장하고 있습니다. 

따라서 config에서 별도의 successHandler를 지정해주지 않으면 SavedRequestAwareAuthenticationSuccessHandler를 실행합니다. 이를 사용하면 인증 성공 후 원래 요청했던 URL로 redirect 시켜줍니다. 그리고 원래 요청했던 URL이 없다면, "/"(defaultTargetUrl)로 redirect 합니다. 

그러나 필자는 defaultTargetUrl을 다르게 설정하기 위해 SavedRequestAwareAuthenticationSuccessHandler를 구현한 LoginSuccessHandler를 작성했습니다. 

(안타깝게도 SavedRequestAwareAuthenticationSuccessHandler는 defaultTargetUrl을 매개변수로 가지는 생성자를 보유하지 않아 custom AutenticationSuccessHandler를 구현해야만 했습니다.)

/**
 * @author sujin
 */
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
  
    public LoginSuccessHandler(String defaultTargetUrl) {
        setDefaultTargetUrl(defaultTargetUrl);
    }
}

 

(4) AuthenticationProvider 구현

실제로 인증을 수행하기 위해 AuthenticationProvider를 구현한 AuthProvider를 작성했습니다. 

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String userId = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
  
        User user = userService.findByUserId(userId);
        if(user == null) {
            throw new UsernameNotFoundException(userId);
        }
         
        if (!matchPassword(password, user.getPassword())) {
            throw new BadCredentialsException(userId);
        }
  
        if (!user.isEnabled()) {
            throw new BadCredentialsException(userId);
        }
         
        ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
        auth.add(new SimpleGrantedAuthority(user.getAuthority()));
        return new UsernamePasswordAuthenticationToken(user, password, auth);
}
  • 2라인 : [1] Spring-security Authentication structure의 그림에서 봤듯이, 파라미터로 전달받은 Authentication은 AuthenticationFilter에 의해 생성되고, 로그인 폼으로부터 입력받은 loginId/password로 생성한 AuthenticationToken입니다.
  • 4라인 : loginId는 AuthenticationToken의 principal에 저장되어 있습니다.
  • 5라인 : password는 AuthenticationToken의 credentials에 저장되어 있습니다. 
  • 7라인 : Mongodb로부터 조회해 유효한 사용자인지 확인합니다. 이때 사용되는 User DTO는 필자가 정의한 Custom DTO입니다. 
  • 8라인~18라인 : 유효성을 판단하여 유효하지 않으면 Exception을 발생시킵니다. 여기서 사용된 UsernameNotFoundException과 BadCredentialsException은 모두 AuthenticationException입니다. 
  • 앞서 AuthenticationManager가 등록된 AuthenticationProvider들을 chain으로 실행한다고 언급했었는데, AuthenticationException이 발생하면 Exception을 전파하지 않고 chain에 엮여있는 다음 AuthenticationProvider를 실행합니다. 
  • (이에 대한 구현 내용은 AuthenticationManager를 구현하는 ProviderManager에서 확인할 수 있습니다. ProviderManager는 Spring-security의 내장 객체입니다.)
  • 22라인 : 새로운 AuthenticationToken을 만듭니다. 파라미터로 받은 AuthenticationToken과의 차이를 살펴보겠습니다. 
  •   1) principal의 정보가 확장되었습니다. (loginId -> Mongodb로부터 조회한 User 정보)
  •   2) 권한 목록을 3번째 파라미터에 추가합니다. 이는 권한 체크를 할 때 사용됩니다. 

 

(5) AccessDeniedHandler 구현 

인증에 성공했으나 권한이 적합하지 않을 경우 다음과 같이 403 에러 화면이 노출됩니다. 

이는 사용자 입장에서 시스템의 에러처럼 느껴질 수 있으므로 일관적인 UI를 유지시켜야 합니다. 이런 경우에 요청을 redirect 시키기 위해 AccessDeniedHandler를 구현했습니다. 이 Handler의 handle method는 AccessDeniedException이 발생했을 때 실행됩니다. 

public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exc)
            throws IOException, ServletException {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null) {
            LOGGER.info("User: " + auth.getName() + " attempted to access the protected URL: " + request.getRequestURI());
        }
   
        response.sendRedirect(request.getContextPath() + "/accessDenied");
    }
}

MappingURL "/accessDenied"에 대한 컨트롤러와 Jsp를 추가하여 다음과 같은 화면으로 대체되어 노출됩니다.

 

(6) jsp에서 AuthenticationToken 정보 조회

pom.xml에 spring-security-taglibs 패키지를 추가합니다. 

<dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-taglibs</artifactId>
</dependency>

 

jsp 상단에 추가한 taglib를 import 합니다.

<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>

 

그러면 taglib를 이용해 다음과 같이 AuthenticationToken 정보를 조회할 수 있습니다.

<sec:authentication property="principal.userId"/>

 

 

 

댓글