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로 목록 관리함
- /auth/login에서 username/password 받음
- Spring Security가 LDAP로 인증
- 성공하면:
- admin 목록에 있으면 ROLE_ADMIN
- 아니면 ROLE_USER
- 그 role을 JWT claim에 넣어서 발급
- 이후 요청은 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 |
|---|