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에서 제공하는 구현을 재사용하는 데 초점을 맞출 것입니다. 특히, DaoAuthenticationProviderUsernamePasswordToken 은 "즉시 사용할 수있는"상태로 재사용 됩니다.

주요 구성 요소는 다음과 같습니다.

  • SimpleAuthenticationFilterUsernamePasswordAuthenticationFilter 의 확장
  • SimpleUserDetailsServiceUserDetailsService 의 구현
  • Us er 추가 도메인 필드를 선언하는 Spring Security에서 제공하는 User 클래스의 확장
  • Securi tyConfigSimpleAuthenticationFilter 를 필터 체인에삽입하고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. 요약

첫 번째 예에서는 사용자 이름 필드를 "위조"하여 DaoAuthenticationProviderUsernamePasswordAuthenticationToken 을 재사용 할 수있었습니다 .

그 결과 최소한의 구성과 추가 코드로 추가 로그인 필드에 대한 지원추가 할 수있었습니다 .

4. 사용자 정의 프로젝트 설정

두 번째 접근 방식은 첫 번째 접근 방식과 매우 유사하지만 사소하지 않은 사용 사례에 더 적합 할 수 있습니다.

두 번째 접근 방식의 주요 구성 요소는 다음과 같습니다.

  • CustomAuthenticationFilterUsernamePasswordAuthenticationFilter 의 확장
  • CustomUserDetailsServiceloadUserbyUsernameAndDomain 메서드를선언하는 사용자 지정 인터페이스
  • CustomUserDetailsServiceImpl - 우리의 구현 CustomUserDetailsService
  • CustomUserDetailsAuthenticationProviderAbstractUserDetailsAuthenticationProvider 의 확장
  • CustomAuthenticationTokenUsernamePasswordAuthenticationToken 의 확장
  • Us er 추가 도메인 필드를 선언하는 Spring Security에서 제공하는 User 클래스의 확장
  • Securi tyConfigCustomAuthenticationFilter 를 필터 체인에삽입하고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. 요약

두 번째 접근 방식은 처음에 제시 한 간단한 접근 방식과 거의 동일합니다. 자체 AuthenticationProviderCustomAuthenticationToken 을 구현함으로써 사용자 지정 구문 분석 논리로 사용자 이름 필드를 조정할 필요가 없습니다.

5. 결론

이 기사에서는 추가 로그인 필드를 사용하는 Spring Security에서 양식 로그인을 구현했습니다. 이 작업은 두 가지 방법으로 수행했습니다.

  • 간단한 접근 방식으로 작성해야하는 코드의 양을 최소화했습니다. 사용자 지정 구문 분석 논리로 사용자 이름조정하여 DaoAuthenticationProvider 및 UsernamePasswordAuthentication재사용 할 수있었습니다.
  • 보다 맞춤화 된 접근 방식에서는 AbstractUserDetailsAuthenticationProvider확장 하고 CustomAuthenticationToken 과 함께 자체 CustomUserDetailsService제공 하여 사용자 정의 필드 지원을 제공 했습니다.

항상 그렇듯이 모든 소스 코드는 GitHub 에서 찾을 수 있습니다 .