1. 소개
이 기사에서는 표준 로그인 양식에 추가 필드를 추가하여 Spring Security 로 사용자 정의 인증 시나리오를 구현할 것 입니다.
우리는 프레임 워크의 다양성과이를 사용할 수있는 유연한 방법을 보여주기 위해 두 가지 접근 방식 에 초점을 맞출 것입니다.
우리의 첫 번째 접근법 은 기존의 핵심 Spring Security 구현의 재사용에 초점을 맞춘 간단한 솔루션이 될 것입니다.
두 번째 접근 방식 은 고급 사용 사례에 더 적합한 맞춤형 솔루션이 될 것입니다.
Spring Security login 에 대한 이전 기사에서 논의한 개념을 기반으로 구축 할 것 입니다.
2. Maven 설정
Spring Boot 스타터를 사용하여 프로젝트를 부트 스트랩하고 필요한 모든 의존성을 가져올 것입니다.
우리가 사용할 설정에는 부모 선언, 웹 스타터 및 Security 스타터가 필요합니다. 또한 thymeleaf를 포함 할 것입니다.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
</dependencies>
가장 최신 버전의 Spring Boot Security 스타터는 Maven Central에서 찾을 수 있습니다 .
3. 간단한 프로젝트 설정
첫 번째 접근 방식에서는 Spring Security에서 제공하는 구현을 재사용하는 데 초점을 맞출 것입니다. 특히, DaoAuthenticationProvider 및 UsernamePasswordToken 은 "즉시 사용할 수있는"상태로 재사용 됩니다.
주요 구성 요소는 다음과 같습니다.
- SimpleAuthenticationFilter – UsernamePasswordAuthenticationFilter 의 확장
- SimpleUserDetailsService – UserDetailsService 의 구현
- Us er – 추가 도메인 필드를 선언하는 Spring Security에서 제공하는 User 클래스의 확장
- Securi tyConfig – SimpleAuthenticationFilter 를 필터 체인에삽입하고Security 규칙을 선언하며 의존성을 연결하는Spring Security 구성
- login.html – 사용자 이름 , 비밀번호 및 도메인 을 수집하는 로그인 페이지
3.1. 단순 인증 필터
우리에 SimpleAuthenticationFilter , 도메인 및 사용자 이름 필드는 요청에서 추출됩니다 . 이러한 값을 연결하고이를 사용하여 UsernamePasswordAuthenticationToken 인스턴스를 만듭니다 .
그런 다음 토큰은 인증을 위해 AuthenticationProvider 에 전달됩니다 .
public class SimpleAuthenticationFilter
extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
// ...
UsernamePasswordAuthenticationToken authRequest
= getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager()
.authenticate(authRequest);
}
private UsernamePasswordAuthenticationToken getAuthRequest(
HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String domain = obtainDomain(request);
// ...
String usernameDomain = String.format("%s%s%s", username.trim(),
String.valueOf(Character.LINE_SEPARATOR), domain);
return new UsernamePasswordAuthenticationToken(
usernameDomain, password);
}
// other methods
}
3.2. 간단한 UserDetails 서비스
경우 UserDetailsService 계약이라는 하나의 메소드 정의 loadUserByUsername을. 우리의 구현은 사용자 이름 과 도메인을 추출 합니다. 그런 다음 값은 User 를 얻기 위해 U serRepository 로 전달됩니다 .
public class SimpleUserDetailsService implements UserDetailsService {
// ...
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String[] usernameAndDomain = StringUtils.split(
username, String.valueOf(Character.LINE_SEPARATOR));
if (usernameAndDomain == null || usernameAndDomain.length != 2) {
throw new UsernameNotFoundException("Username and domain must be provided");
}
User user = userRepository.findUser(usernameAndDomain[0], usernameAndDomain[1]);
if (user == null) {
throw new UsernameNotFoundException(
String.format("Username not found for domain, username=%s, domain=%s",
usernameAndDomain[0], usernameAndDomain[1]));
}
return user;
}
}
3.3. 스프링 Security 구성
우리의 설정은 addFilterBefore를 호출 하여 기본값 앞에 SimpleAuthenticationFilter 를 필터 체인에 삽입 하기 때문에 표준 Spring Security 구성 과 다릅니다 .
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(authenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").authenticated()
.and()
.formLogin().loginPage("/login")
.and()
.logout()
.logoutUrl("/logout");
}
제공된 DaoAuthenticationProvider를 SimpleUserDetailsService로 구성했기 때문에 사용할 수 있습니다 . 리콜는 것을 우리 SimpleUserDetailsService은 우리 구문 분석하는 방법을 알고있는 사용자 이름 및 도메인 필드를 적절한 반환 사용자 사용을 인증 할 때를 :
public AuthenticationProvider authProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
SimpleAuthenticationFilter를 사용하고 있으므로 실패한 로그인 시도가 적절하게 처리되도록 자체 AuthenticationFailureHandler 를 구성 합니다.
public SimpleAuthenticationFilter authenticationFilter() throws Exception {
SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationFailureHandler(failureHandler());
return filter;
}
3.4. 로그인 페이지
우리가 사용하는 로그인 페이지는 SimpleAuthenticationFilter에 의해 추출되는 추가 도메인 필드를 수집합니다 .
<form class="form-signin" th:action="@{/login}" method="post">
<h2 class="form-signin-heading">Please sign in</h2>
<p>Example: user / domain / password</p>
<p th:if="${param.error}" class="error">Invalid user, password, or domain</p>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control"
placeholder="Username" required autofocus/>
</p>
<p>
<label for="domain" class="sr-only">Domain</label>
<input type="text" id="domain" name="domain" class="form-control"
placeholder="Domain" required autofocus/>
</p>
<p>
<label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control"
placeholder="Password" required autofocus/>
</p>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button><br/>
<p><a href="/index" th:href="@{/index}">Back to home page</a></p>
</form>
애플리케이션을 실행하고 http : // localhost : 8081 에서 컨텍스트에 액세스하면 Security 페이지에 액세스 할 수있는 링크가 표시됩니다. 링크를 클릭하면 로그인 페이지가 표시됩니다. 예상대로 추가 도메인 필드가 표시됩니다 .
3.5. 요약
첫 번째 예에서는 사용자 이름 필드를 "위조"하여 DaoAuthenticationProvider 및 UsernamePasswordAuthenticationToken 을 재사용 할 수있었습니다 .
그 결과 최소한의 구성과 추가 코드로 추가 로그인 필드에 대한 지원 을 추가 할 수있었습니다 .
4. 사용자 정의 프로젝트 설정
두 번째 접근 방식은 첫 번째 접근 방식과 매우 유사하지만 사소하지 않은 사용 사례에 더 적합 할 수 있습니다.
두 번째 접근 방식의 주요 구성 요소는 다음과 같습니다.
- CustomAuthenticationFilter – UsernamePasswordAuthenticationFilter 의 확장
- CustomUserDetailsService – loadUserbyUsernameAndDomain 메서드를선언하는 사용자 지정 인터페이스
- CustomUserDetailsServiceImpl - 우리의 구현 CustomUserDetailsService
- CustomUserDetailsAuthenticationProvider – AbstractUserDetailsAuthenticationProvider 의 확장
- CustomAuthenticationToken – UsernamePasswordAuthenticationToken 의 확장
- Us er – 추가 도메인 필드를 선언하는 Spring Security에서 제공하는 User 클래스의 확장
- Securi tyConfig – CustomAuthenticationFilter 를 필터 체인에삽입하고Security 규칙을 선언하며 의존성을 연결하는Spring Security 구성
- login.html – 사용자 이름 , 비밀번호 및 도메인 을 수집하는 로그인 페이지
4.1. 사용자 지정 인증 필터
우리에 CustomAuthenticationFilter , 우리는 요청에서 사용자 이름, 암호 및 도메인 필드를 추출합니다 . 이 값은 인증을 위해 AuthenticationProvider 에 전달되는 사용자 지정 AuthenticationToken 의 인스턴스를 만드는 데 사용됩니다 .
public class CustomAuthenticationFilter
extends UsernamePasswordAuthenticationFilter {
public static final String SPRING_SECURITY_FORM_DOMAIN_KEY = "domain";
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException {
// ...
CustomAuthenticationToken authRequest = getAuthRequest(request);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private CustomAuthenticationToken getAuthRequest(HttpServletRequest request) {
String username = obtainUsername(request);
String password = obtainPassword(request);
String domain = obtainDomain(request);
// ...
return new CustomAuthenticationToken(username, password, domain);
}
4.2. Custom UserDetails 서비스
우리 CustomUserDetailsService의 계약이라는 하나의 메소드 정의 loadUserByUsernameAndDomain을.
CustomUserDetailsServiceImpl의 우리가 간단하게 만들 클래스는 우리에게 계약 위임 구현 CustomUserRepository 얻을 수있는 사용자 :
public UserDetails loadUserByUsernameAndDomain(String username, String domain)
throws UsernameNotFoundException {
if (StringUtils.isAnyBlank(username, domain)) {
throw new UsernameNotFoundException("Username and domain must be provided");
}
User user = userRepository.findUser(username, domain);
if (user == null) {
throw new UsernameNotFoundException(
String.format("Username not found for domain, username=%s, domain=%s",
username, domain));
}
return user;
}
4.3. 사용자 정의 UserDetailsAuthenticationProvider
우리 CustomUserDetailsAuthenticationProvider는 확장 AbstractUserDetailsAuthenticationProvider 우리에게 위임을 CustomUserDetailService 검하는 사용자 . 이 클래스의 가장 중요한 기능은 retrieveUser 메서드 의 구현입니다 .
사용자 정의 필드에 액세스 하려면 인증 토큰을 CustomAuthenticationToken으로 캐스팅해야 합니다.
@Override
protected UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
CustomAuthenticationToken auth = (CustomAuthenticationToken) authentication;
UserDetails loadedUser;
try {
loadedUser = this.userDetailsService
.loadUserByUsernameAndDomain(auth.getPrincipal()
.toString(), auth.getDomain());
} catch (UsernameNotFoundException notFound) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials()
.toString();
passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
}
throw notFound;
} catch (Exception repositoryProblem) {
throw new InternalAuthenticationServiceException(
repositoryProblem.getMessage(), repositoryProblem);
}
// ...
return loadedUser;
}
4.4. 요약
두 번째 접근 방식은 처음에 제시 한 간단한 접근 방식과 거의 동일합니다. 자체 AuthenticationProvider 및 CustomAuthenticationToken 을 구현함으로써 사용자 지정 구문 분석 논리로 사용자 이름 필드를 조정할 필요가 없습니다.
5. 결론
이 기사에서는 추가 로그인 필드를 사용하는 Spring Security에서 양식 로그인을 구현했습니다. 이 작업은 두 가지 방법으로 수행했습니다.
- 간단한 접근 방식으로 작성해야하는 코드의 양을 최소화했습니다. 사용자 지정 구문 분석 논리로 사용자 이름 을 조정하여 DaoAuthenticationProvider 및 UsernamePasswordAuthentication 을 재사용 할 수있었습니다.
- 보다 맞춤화 된 접근 방식에서는 AbstractUserDetailsAuthenticationProvider 를 확장 하고 CustomAuthenticationToken 과 함께 자체 CustomUserDetailsService 를 제공 하여 사용자 정의 필드 지원을 제공 했습니다.
항상 그렇듯이 모든 소스 코드는 GitHub 에서 찾을 수 있습니다 .
- https://docs.spring.io/spring-framework/docs/current/reference/html
- https://www.baeldung.com/spring-security-extra-login-fields
'Java' 카테고리의 다른 글
HTTP 호출자를 사용한 Spring Remoting 소개 (0) | 2021.03.17 |
---|---|
Thymeleaf를 사용한 Spring Boot CRUD 애플리케이션 (0) | 2021.03.17 |
Spring 으로 Rest api 만들기 with test code (0) | 2021.03.16 |
Spring Boot Ehcache 예제 (0) | 2021.03.16 |
Spring Security : 유저 Role 확인 방법 (0) | 2021.03.15 |