Back-end/Springboot

[Spring Security + Ldaps] 신규 서비스 AD 인증 연동

moguogu 2026. 2. 1. 11:13

1. 개요 

신규 사내 시스템을 만들면서 B/E를 새로 구축하게 되었다. 이때 로그인 및 사용자 인증은 Ldaps를 통해 운영된다.

따라서 Spring Security 기반에 Ldaps를 거쳐 인증/인가가 되어야한다.

연동 과정을 차례차례 정리해보겠다.


2. Architecture

Client
  ↓
Spring Security Filter Chain
  ↓
AuthenticationManager
  ↓
LdapAuthenticationProvider
  ↓
LDAPS (Active Directory)

 

 


3. 상세 조건

- 전제 조건

 

LDAP 서버 정보

  • 호스트: ad.company.com
  • 포트: 636 (LDAPS)
  • Base DN: 예) DC=company,DC=com

유저를 찾는 방식

  • AD면 보통 sAMAccountName (계정아이디)
  • 검색 필터 예: (sAMAccountName={0})

권한(ROLE)은?

  • 인증만 LDAP으로 동작하고, 관리자 권한은 일부 인원들 따로 정의하여 권한을 분리한다
  • ROLE_USER / ROLE_ADMIN 두 가지 권한으로 분리한다

로그인 방식

  • REST방식으로 로그인 하고, JWT claim에 Role정보를 넣어 발급함
  • Admin 권한을 받을 대상자가 변동이 거의 없으므로 우선 yml로 목록 관리함

 

  1. /auth/login에서 username/password 받음
  2. Spring Security가 LDAP로 인증
  3. 성공하면:
    1. admin 목록에 있으면 ROLE_ADMIN
    2. 아니면 ROLE_USER
  4. 그 role을 JWT claim에 넣어서 발급
  5. 이후 요청은 JWT만 보고 인가

 

<AI 이미지>

 


4. 코드 

1) 의존성 추가(Gradle)

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-ldap'
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly   'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly   'io.jsonwebtoken:jjwt-jackson:0.12.5'

 

2) Application.yml설정

ldap:
  url: "ldaps://ad.company.com:636"
  base-dn: "DC=company,DC=com"
  username: "managerAuthUsername"
  password: "managerAuthPassword"

security:
  admins:
    - "honggildong"
    - "honggileun"
    
jwt:
  secret: "${JWT_SECRET}" 
  issuer: "my-service"
  access-token-minutes: 60

 

 

3) LdapConfig 설정

@Configuration
class LdapAuthConfig {

  @Bean
  LdapContextSource ldapContextSource(
      @Value("${ldap.url}") String url,
      @Value("${ldap.base-dn}") String baseDn,
      @Value("${ldap.username}") String managerDn,
      @Value("${ldap.password}") String managerPassword
  ) {
    LdapContextSource cs = new LdapContextSource();
    cs.setUrl(url);
    cs.setBase(baseDn);
    cs.setUserDn(managerDn);
    cs.setPassword(managerPassword);
    cs.afterPropertiesSet();
    return cs;
  }
}

 

4) WebSecurity Config 설정

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    @Bean
    public AuthenticationManager authenticationManager(
            HttpSecurity http,
            LdapAuthenticationProvider ldapAuthenticationProvider
    ) throws Exception {
        AuthenticationManagerBuilder builder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        builder.authenticationProvider(ldapAuthenticationProvider);
        return builder.build();
    }

    @Bean
    public SecurityFilterChain filterChain(
            HttpSecurity http,
            JwtTokenProvider jwtTokenProvider
    ) throws Exception {

        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/login", "/health").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            // JWT 검증 필터 추가
            .addFilterBefore(new JwtAuthFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

 

 

5) LdapUserDetails 설정

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.Objects;

public class LdapUserDetails implements UserDetails {

  private final String username;
  private final String displayName; // cn 같은 값
  private final String email;       // mail 같은 값
  private final List<GrantedAuthority> authorities;

  public LdapUserDetails(String username, String displayName, String email,
                         List<GrantedAuthority> authorities) {
    this.username = Objects.requireNonNull(username);
    this.displayName = displayName;
    this.email = email;
    this.authorities = authorities;
  }

  public String getDisplayName() { return displayName; }
  public String getEmail() { return email; }

  @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
  @Override public String getPassword() { return null; } // 비번 저장 안 함
  @Override public String getUsername() { return username; }

  @Override public boolean isAccountNonExpired() { return true; }
  @Override public boolean isAccountNonLocked() { return true; }
  @Override public boolean isCredentialsNonExpired() { return true; }
  @Override public boolean isEnabled() { return true; }
}

 

6) JwtAuthFilter

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        String path = request.getRequestURI();
        return path.equals("/auth/login") || path.equals("/health");
    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        String header = request.getHeader(HttpHeaders.AUTHORIZATION);

        // Authorization 헤더 없으면 그냥 다음 필터로 (인가 단계에서 막힘)
        if (!StringUtils.hasText(header) || !header.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }

        String token = header.substring("Bearer ".length()).trim();

        try {
            // 1) 유효성 검증(서명/만료/issuer 등)
            if (!jwtTokenProvider.validateToken(token)) {
                unauthorized(response, "INVALID_OR_EXPIRED_TOKEN");
                return;
            }

            // 2) 토큰에서 Authentication 복원 (username + roles -> GrantedAuthorities)
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);

            filterChain.doFilter(request, response);

        } catch (Exception e) {
            SecurityContextHolder.clearContext();
            unauthorized(response, "INVALID_TOKEN");
        }
    }

    private void unauthorized(HttpServletResponse response, String code) throws IOException {
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"code\":\"" + code + "\"}");
    }
}

 

7) JwtTokenProvider

public class JwtTokenProvider {
  public String generateToken(String username, Set<String> roles) { ... }
  public boolean validateToken(String token) { ... }
  public Authentication getAuthentication(String token) { ... }
}

 

'Back-end > Springboot' 카테고리의 다른 글

[Spring boot + MongoDB + MySQL] DB 2개 연동하기  (0) 2022.07.26