이 글은 정수원님의 Infrean 강의를 학습한 내용을 정리하여 작성합니다.
Form 인증 - WebAuthenticationDetails, AuthenticationDetailsSource
- user가 아이디와 패스워드 외에도 추가적인 파라미터를 정의해 전달하는 경우, 추가 정보를 인증 과정에서 사용하거나 인증 이외에도 해당 정보를 계속해서 참조할 수 있도록 처리하는 클래스가 WebAuthenticationDetails 클래스이다.
- WebAuthenticationDetails 클래스를 생성하는 클래스가 AuthenticationDetailsSource 클래스이다.
- 인증 필터가 인증 처리를 수행한다.
- AuthenticationFilter는 Authentication 객체를 생성하는데, 해당 객체에는 사용자의 아이디와 패스워드 정보가 들어있다.
- 또한, Authentication 객체에는 details라는 이름의 속성을 가지는데 해당 속성의 타입은 Object로 어떠한 객체도 저장할 수 있다.
- Authentication 객체의 details 속성에는 WebAuthenticationDetails가 생성되어 저장된다.
- WebAuthenticationDetails에는 사용자가 추가로 전달하는 파라미터들이 저장되어 있다.
- 저장된 파라미터들은 request 객체를 통해 조회할 수 있다.
- 추가로 스프링 시큐리티가 remoteAddress와 SessionId 값을 기본적으로 처리하고 있다.
참고
UsernamePasswordAuthenticationFilter에는 다음과 같이 Details 속성을 설정하는 메서드가 존재한다.
WebAuthenticationDetails
- 인증 과정 중 전달된 데이터를 저장한다.
- Autehntication의 details 속성에 저장한다.
AuthenticationDetailsSource
- WebAuthenticationDetails 객체를 생성한다.
구현
FormWebAuthentcationDetails
- WebAuthenticationDetails 클래스를 상속받아 WebAuthenticationDetails 클래스 역할을 수행하는 FormWebAuthentcaitionDetails 클래스를 생성한다.
package io.security.corespringsecurity.security.common;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import javax.servlet.http.HttpServletRequest;
public class FormWebAuthenticationDetails extends WebAuthenticationDetails {
private String secretKey;
public FormWebAuthenticationDetails(HttpServletRequest request) {
super(request);
secretKey = request.getParameter("secret_key");
}
public String getSecretKey(){
return secretKey;
}
}
FormAuthenticationDetailsSource
- AuthenticationDetailsSrouce 인터페이스를 구현한 FormAuthenticationDetailsSource 클래스로 위에서 구현한 FormWebAuthentcaitionDetails 클래스를 생성하도록 한다.
package io.security.corespringsecurity.security.common;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class FormAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new FormWebAuthenticationDetails(context);
}
}
SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private AuthenticationDetailsSource authenticationDetailsSource;
...
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authz) -> authz
.antMatchers("/", "/users").permitAll()
.antMatchers("/mypage").hasRole("USER")
.antMatchers("/messages").hasRole("MANAGER")
.antMatchers("/config").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login_proc")
.authenticationDetailsSource(authenticationDetailsSource)
.defaultSuccessUrl("/")
.permitAll();
return http.build();
}
}
- AuthenticationDetailsSource 필드를 의존성 주입 받아 Form 인증 방식에서 제공해주는 authenticaitonDetailsSource API에 등록한다.
login.html
- 사용자가 추가적인 파라미터를 등록할 수 있도록 하였기에 로그인 페이지에 추가적인 정보를 입력할 수 있도록 수정한다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout/header::userHead"></head>
<body>
<div th:replace="layout/top::header"></div>
<div class="login-form d-flex justify-content-center">
<div class="col-sm-5" style="margin-top: 30px;">
<div class="panel">
<p>아이디와 비밀번호를 입력해주세요.</p>
</div>
<div th:if="${param.error}" class="form-group" >
<span th:text="${exception}" class="alert alert-danger"></span>
</div>
<form th:action="@{/login_proc}" class="form-signin" method="post">
<input type="hidden" th:value="secret" name="secret_key">
<div class="form-group">
<input type="text" class="form-control" name="username" placeholder="아이디" required="required" autofocus="autofocus">
</div>
<div class="form-group">
<input type="password" class="form-control" name="password" placeholder="비밀번호" required="required">
</div>
<button type="submit" class="btn btn-lg btn-primary btn-block">로그인</button>
</form>
</div>
</div>
</body>
</html>
- secret_key라는 이름으로 secret이라는 데이터가 전달될 것이다.
FormAuthenticationProvider
- 추가적으로 전달받은 secret_key 데이터를 인증 과정에서 사용해본다.
- 현재 FormAuthenticationProvider에는 다음과 같이 아이디와 패스워드 검증을 수행하는데, 추가적으로 secret_key를 통해 검증할 수 있도록 한다.
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password = (String)authentication.getCredentials();
AccountContext accountContext = (AccountContext) userDetailsService.loadUserByUsername(username);
// AuthenticationProvider는 사용자가 입력한 패스워드와 UserDetails의 패스워드와 비교한다.
if (!passwordEncoder.matches(password, accountContext.getPassword())) {
throw new BadCredentialsException("BadCredentialsException");
}
FormWebAuthenticationDetails formWebAuthenticationDetails = (FormWebAuthenticationDetails) authentication.getDetails();
String secretKey = formWebAuthenticationDetails.getSecretKey();
if (secretKey == null || !"secret".equals(secretKey)) {
throw new InsufficientAuthenticationException("InsufficientAuthenticationException");
}
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(accountContext.getAccount(), null, accountContext.getAuthorities());
return authenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
실행
- 생성한 user 계정으로 로그인을 시도한다.
- 인증 필터인 UsernamePasswordAuthenticationFilter의 setDetails 메서드가 호출되는 것을 확인할 수 있다.
- 추가적으로 여기서 authenticationDetailsSource 객체는 우리가 생성한 FormAuthenticationDetailsSource임을 확인할 수 있다.
- 다음으로 FormAuthenticationDetailsSource에서 WebAuthenticationDetails 객체를 생성하는 작업을 수행한다.
- 그러면 위에서 우리가 작성한 FormWebAuthenticationDetails 객체가 다음 생성자를 통해 생성되며 secretKey 값이 정상적으로 넘어오는 것을 확인할 수 있다.
- 이러한 과정을 모두 거치면 인증이 정상적으로 수행된다.
참고
login.html 파일에서 secret_key 값을 고의로 secret이 아닌 sec과 같이 변경해 인증을 수행하면 인증이 거부되어 다시 로그인 페이지로 이동하는 것을 확인할 수 있다.
'스프링 시큐리티 > 실전프로젝트 - 인증 프로세스 Form 인증 구현' 카테고리의 다른 글
인증 실패 핸들러: CustomAuthenticationFailureHandler (0) | 2023.02.12 |
---|---|
인증 성공 핸들러: CustomAuthenticationSuccessHandler (0) | 2023.02.12 |
로그아웃 및 인증에 따른 화면 보안 처리 (0) | 2023.02.12 |
커스텀 로그인 페이지 생성하기 (0) | 2023.02.11 |
DB 연동 인증 처리(2): CustomAuthenticationProvider (0) | 2023.02.11 |