티스토리 뷰
Spring Boot Webflux - Spring Security Form Based Login & AuthenticationManager Custom
최서리나리 2019. 7. 28. 22:24
Spring Boot Webflux 환경에서 Security (form based / authentication manager customize)를 연동하는 방법에 대해 소개한다.
기본적인 initializr과정은 생략한다.
(webflux / security 필수)
기준버전은 2.1.6
해당 글에서는 Spring Security 구성, ReactiveAuthenticationManager를 상속받은 AutenticationManager 구현, 로그인 실패시 처리하는
ServerAuthenticationFailureHandler를 상속받은 LoginFailureHandler 구현만을 다루며 기본적인 사항은 다루지 않는다.
먼저 ReactiveAuthenticationManager를 상속받은 AutenticationManager를 구현하자.
전체 코드는 아래와 같으며, DAO 및 Business Logic은 직접 구현해야 한다.
@Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
@Resource (name = "loginService")
LoginService loginService; // business logic
/**
* 로그인 인증
* ERR01 = 아이디 없음
* ERR02 = 비밀번호 틀림
* ERR03 = 탈퇴회원
* ERR04 = 가입대기
* ERR05 = 가입반려
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
String userId = (String)authentication.getPrincipal();
String userPwd = (String) authentication.getCredentials();
USER_LIST userList = new userList(userId, userPwd, "inner");
return loginService.getUser(userList)
.switchIfEmpty(Mono.error(new BadCredentialsException("err01")))
.flatMap(v -> {
if ("Y".equals(v.getPwd_check())) {
if ("Y".equals(v.getOutYn())) {
throw new BadCredentialsException("err03");
} else {
switch (v.getUserStatus()){
case "UST_001" :
throw new BadCredentialsException("err04");
case "UST_003" :
throw new BadCredentialsException("err05");
}
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userId, userPwd, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
result.setDetails(loginService.getUserDetail(v));
return Mono.just(result);
}
} else {
throw new BadCredentialsException("err02");
}
});
}
}
위 코드는 다음과 같이 작동된다.
return loginService.getUser(userList)
.switchIfEmpty(Mono.error(new BadCredentialsException("err01")))
비즈니스 레이어에서는 계정정보를 조회 한 뒤 USER_LIST 객체를 Mono형식으로 리턴해준다.
USER_LIST 정보를 가지고올때, 반환값이 없을경우 예외처리를 실시한다.
반환값이 없는 경우는 계정정보가 없을 경우이므로 그에맞는 에러코드를 메세지로 넘겨준다.
.flatMap(v -> {
if ("Y".equals(v.getPwdCheck())) {
if ("Y".equals(v.getOutYn())) {
throw new BadCredentialsException("err03");
} else {
switch (v.getUserStatus()){
case "UST_001" :
throw new BadCredentialsException("err04");
case "UST_003" :
throw new BadCredentialsException("err05");
}
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userId, userPwd, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
result.setDetails(loginService.getUserDetail(v));
return Mono.just(result);
}
} else {
throw new BadCredentialsException("err02");
}
});
USER_LIST정보가 있을 경우에는 다음과같이 처리가 이루어진다.
1. 비밀번호가 일치 할 경우
- 회원이 탈퇴 된 경우 : 예외처리
- 탈퇴된 회원이 아닐 경우
- 가입승인 대기중인 회원인 경우 : 예외처리
- 가입승인 반려된 회원인 경우 : 예외처리
로그인 성공
2. 비밀번호가 일치하지 않을 경우
예외처리
위 처리는 예시일뿐이니, 각자 프로젝트의 요구사항에 맞게 구현하면 된다.
중요한 부분은 다음이다.
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userId, userPwd, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
result.setDetails(loginService.getUserDetail(v));
return Mono.just(result);
해당 부분은 기존 mvc security와 별 차이가 없다.
비즈니스로직을 모두 거친 후 UsernamePasswordAuthenticationToken을 생성하고, details에 유저정보를 담은 후
Mono<UsernamePasswordAuthenticationToken> 형식으로 리턴해준다.
함수의 반환값이 Mono<Authentication>이기 때문에, flatMap을 통해 이를 상속받은
Mono<UsernamePasswordAuthenticationToken>형식으로 리턴해주는것이다.
이렇게 AutenticationManager의 구성은 끝났다.
다음은 로그인 실패시 그에따른 사유를 출력해주는 LoginFailureHandler 클래스를 구현하자.
@Component
public class LoginFailureHandler implements ServerAuthenticationFailureHandler {
private ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
@Override
public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) {
if(e instanceof BadCredentialsException){
return redirectStrategy.sendRedirect(webFilterExchange.getExchange(), URI.create("/login?err="+e.getMessage()));
}
return webFilterExchange.getChain().filter(webFilterExchange.getExchange());
}
}
AuthenticationManager에서 구성한 예외처리 부분을 해당 부분에서 처리한다.
예외처리에 따른 상세로직은 프로젝트 요구사항에 맞게 구현하도록 하자.
해당 소스에서는 예외발생시 /login으로 리다이렉트 하고, parameter에 에러메세지를 같이 전달하도록 하였다.
다음은 위 클래스들을 실제로 사용하게 될 SecurityConfig 클래스이다.
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@ComponentScan(basePackages = {"package..."})
public class SecurityConfiguration {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/pattern/**")
.hasRole("ADMIN")
.anyExchange()
.permitAll()
.and()
.httpBasic()
.and()
.addFilterAt(new MultipartCsrfFilter(), SecurityWebFiltersOrder.FIRST)
.csrf()
.and()
.formLogin()
.loginPage("/login")
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/url"))
.authenticationFailureHandler(loginFailureHandler)
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler("/login"))
.and()
.authenticationManager(authenticationManager)
.exceptionHandling()
.accessDeniedHandler(new HttpStatusServerAccessDeniedHandler(HttpStatus.BAD_REQUEST))
.and()
.build();
}
public ServerLogoutSuccessHandler logoutSuccessHandler(String uri) {
RedirectServerLogoutSuccessHandler successHandler = new RedirectServerLogoutSuccessHandler();
successHandler.setLogoutSuccessUrl(URI.create(uri));
return successHandler;
}
}
@ComponentScan(basePackages = {"package..."})
우선 해당 부분에서 위 작성한 컴포넌트들을 스캔 할 수 있도록 지정해주어야 한다.
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private LoginFailureHandler loginFailureHandler;
그 후 @Autowired를 통해 컴포넌트를 와이어링 해주자.
public ServerLogoutSuccessHandler logoutSuccessHandler(String uri) {
RedirectServerLogoutSuccessHandler successHandler = new RedirectServerLogoutSuccessHandler();
successHandler.setLogoutSuccessUrl(URI.create(uri));
return successHandler;
}
LogoutSuccessHandler를 함수로 구현하였다.
단순히 로그아웃 성공시 Redirect만을 수행하면 되므로, 상속받아 별도의 컴포넌트로 구현하지는 않았다.
만약 그러한 상황이 발생한다면, LoginFailureHandler와 같이 상속받아 구성하도록 하자.
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/pattern/**")
.hasRole("ADMIN")
.anyExchange()
.permitAll()
.and()
.addFilterAt(new MultipartCsrfFilter(), SecurityWebFiltersOrder.FIRST)
.csrf()
.and()
.formLogin()
.loginPage("/login")
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/url"))
.authenticationFailureHandler(loginFailureHandler)
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler("/login"))
.and()
.authenticationManager(authenticationManager)
.exceptionHandling()
.accessDeniedHandler(new HttpStatusServerAccessDeniedHandler(HttpStatus.BAD_REQUEST))
.and()
.build();
}
해당 부분에서 SecurityWebFilterChain 빈을 주입한다.
권한롤에 따른 url매핑이나 등등등.. 기존 mvc security와 큰 차이는 없지만 체이닝 함수명 변동이나 기능이 변경된 부분들이 일부 존재한다.
중요한 부분은 아래와 같다.
.addFilterAt(new MultipartCsrfFilter(), SecurityWebFiltersOrder.FIRST)
.csrf()
csrf 활성화
자세한 사항은 아래 글 참고
https://seorinari.tistory.com/7
Spring Boot WebFlux - Multipart(multipart/form-data) 전송시 Security CSRF(XSRF) Filter 설정
WebFlux로 프로젝트를 진행하던 중, Multipart로 전송시 Spring Security에 설정한 CSRF 관련 사항과 충돌이 발생하여 이에대한 대응책을 남긴다. (invalid csrf token) Multipart의 경우 FormData가 ServerReque..
seorinari.tistory.com
.formLogin()
.loginPage("/login")
.authenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/url"))
.authenticationFailureHandler(loginFailureHandler)
form 로그인 사용 및 로그인 URL(실제 프로세스가 수행될 URL) 정의
그리고 로그인성공시 작동될 핸들러와 로그인이 실패 할 경우 작동될 핸들러를 정의한다.
로그인성공시에는 별 다른 액션 없이 redirect되도록 RedirectServerAuthenticationSuccessHandler를 등록하였다.
실패시에는 위에서 정의한 핸들러를 등록하였다.
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(logoutSuccessHandler("/login"))
로그아웃 URL과 로그아웃 성공시 작동될 핸들러를 정의한다.
위에서 작성한 함수를 등록하였다.
.authenticationManager(authenticationManager)
그리고 중요한 부분인 authenticationManager를 정의해준다.
해당 부분을 정의함으로써 우리가 작성한 CustomAuthenticationManager가 작동되게 된다.
다음은 컨트롤러와 thymeleaf를 통해 로그인페이지를 구성하도록 하자.
@RequestMapping("/login")
public String loginForm(ServerHttpRequest request,
Model model) {
model.addAllAttributes(request.getQueryParams().toSingleValueMap());
return "service/login";
}
<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div th:switch="${err}">
<p th:case="'err01'">아이디가 존재하지 않습니다.</p>
<p th:case="'err02'">비밀번호가 올바르지 않습니다.</p>
<p th:case="'err03'">탈퇴 된 회원 입니다.</p>
<p th:case="'err04'">승인 대기중 입니다.</p>
<p th:case="'err05'">반려 된 회원 입니다.</p>
</div>
<form th:action="@{/login}" th:method="post">
<input th:name="username" placeholder="아이디를 입력하세요."/>
<input th:name="password" type="password" placeholder="비밀번호를 입력하세요."/>
<button type="submit">로그인</button>
</form>
</body>
</html>
(에러코드 처리의 경우 공통 메세지를 통해 처리하도록 하자. 해당 글에서 언급 할 주제는 아니므로 생략.)
완성!
다음은 간략한 테스트 결과이다.
로그인에 성공 할 경우 Controller에서 Authentication나 @AuthenticationPrincipal를 인자로 받거나,
ReactiveSecurityContextHolder를 통해 principal 정보를 확인 할 수 있다.
끝
'개발 이야기 > SPRING' 카테고리의 다른 글
- Total
- Today
- Yesterday
- spring-data-jpa
- SI
- SpringDataJPA
- 친환경차
- memcached
- 취업
- query-dsl
- Spring Security
- Spring Boot
- Spring Cache
- JPA
- spring-jpa
- 스프링
- Java
- Weblogic
- hibernate
- Spring
- CSRF
- multipart
- 이직
- 저공해자동차
- intellij
- spring webflux
- 저공해자동차 스티커
- Util
- Thymeleaf
- WebFlux
- 국비교육
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |