앞서 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"/>
'Auth (Authentication & Authorization)' 카테고리의 다른 글
JWT란? (0) | 2020.10.12 |
---|---|
Spring-security authorizeRequest 동적으로 설정하기 (0) | 2020.10.12 |
Spring-Security Authentication Structure (0) | 2020.10.12 |
Interceptor를 이용한 Auth 구현 (0) | 2020.10.12 |
인증(Authentication)과 인가(Authorization) (0) | 2020.08.19 |
댓글