Spring/Spring Security

[Spring Security] 3-2. Authentication: Username/Password

noahkim_ 2023. 10. 2. 16:37

1. 인증 정보 습득방식

  • Spring Security는 기본적으로 클라이언트로부터 인증 정보를 얻기 위한 세가지 방법을 지원합니다. 
  • 기본적으로 HttpServletRequest 객체로부터 아이디/패스워드 정보를 얻습니다.

 

종류

구분 Form Basic Digest
설명 HTML 폼을 통해 아이디/비밀번호를 입력받아 인증 HTTP 요청 헤더에 Base64로 인코딩된 아이디/비밀번호를 포함
nonce 기반 해시값으로 인증
(비밀번호는 평문 전송 안 함)
요청 방식 POST /login (form-data) Authorization: Basic base64(username:password)
Authorization: Digest ...
초기 트리거 인증 없이 보호된 리소스 접근
→ /login 리다이렉트
인증 없이 접근 시
→ WWW-Authenticate: Basic 응답
인증 없이 접근 시
→ WWW-Authenticate: Digest 응답
사용 필터 UsernamePasswordAuthenticationFilter BasicAuthenticationFilter
DigestAuthenticationFilter
EntryPoint LoginUrlAuthenticationEntryPoint BasicAuthenticationEntryPoint
DigestAuthenticationEntryPoint
보안 수준 HTTPS 필수
비교적 안전
HTTPS 필수
Base64는 인코딩일 뿐 암호화 아님
HTTPS 없어도 설계되었으나 지금은 보안 취약함
사용 여부 ✅ 널리 사용됨 (특히 UI 기반 앱) ✅ 내부 API나 간단한 인증에 사용
❌ 거의 사용 안 함 (비권장)
성공 시 이동 이전 요청 또는 / 그대로 다음 필터 체인 진행
그대로 다음 필터 체인 진행
실패 시 동작 /login?error로 리다이렉트 401 Unauthorized
+ WWW-Authenticate
401 Unauthorized
+ WWW-Authenticate

 

인증 흐름

  1. 인증되지 않은 사용자가 보호된 리스소에 접근
  2. AuthorizationFilter에 의해 요청이 거부됨 (AccessDeniedException)
  3. ExceptionTranslationFilter가 인증을 시작함 
  4. AuthenticationEntryPoint를 사용하여 후처리함

설정

Form

더보기

로그인 경로 설정

http.formLogin(form -> form
    .loginPage("/login")
    .permitAll());
  • 해당 경로로 username/password 추출해서 인증 시도
  • 실패 시 리다이렉트 경로 설정

 

기본 설정

http.formLogin(withDefaults());

 

항목 기본 경로 설명
로그인 폼 페이지 (GET) /login
인증 안 된 사용자가 접근 시 리다이렉트됨
로그인 처리 URL (POST) /login
사용자 아이디/비밀번호 제출하는 곳
성공 시 이동 이전 요청 또는 /
인증 성공 후 원래 요청 있으면 그쪽으로 리다이렉트
실패 시 이동 /login?error
로그인 실패 시 리다이렉트되는 경로

 

Basic

더보기
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
	http
		.httpBasic(withDefaults()); // Basic 인증 명시
	return http.build();
}

 

2. Password Storage

In Memory

  • 사용자 정보를 메모리에 저장하고 인증하는 구조
  • DB 연동 없이 작동함

 

예시) InMemoryUserDetailsManager

더보기
@Bean
public UserDetailsService users() {
	UserDetails user = User.builder()
		.username("user")
		.password("{bcrypt}암호화된비밀번호")
		.roles("USER")
		.build();

	UserDetails admin = User.builder()
		.username("admin")
		.password("{bcrypt}암호화된비밀번호")
		.roles("USER", "ADMIN")
		.build();

	return new InMemoryUserDetailsManager(user, admin);
}
  • UserDetailsManager 인터페이스의 구현체
  • In Memory 기반의 유저 인증을 관리하는 객체
  • password 필드는 {bcrypt} 접두사 + 실제 암호화된 값을 포함해야 함
  • Spring Security는 접두사에 따라 어떤 PasswordEncoder를 사용할지 결정함

 

JDBC

  • DB에서 username/password 조회하는 방식
클래스 인터페이스 설명
JdbcDaoImpl UserDetailsService 구현체.
사용자 정보(DB 조회)를 읽어오는 데 사용
JdbcUserDetailsManager UserDetailsManager 구현체.
JdbcDaoImpl 확장.
사용자 생성/수정/삭제 등 관리 기능 추가

 

Default Schema

예) User Schema

더보기
create table users(
    username varchar_ignorecase(50) not null primary key,
    password varchar_ignorecase(500) not null,
    enabled boolean not null
);

create table authorities (
    username varchar_ignorecase(50) not null,
    authority varchar_ignorecase(50) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);

create unique index ix_auth_username on authorities (username,authority);

 

예) Group Schema

더보기
create table groups (
    id bigint generated by default as identity(start with 0) primary key,
    group_name varchar_ignorecase(50) not null
);

create table group_authorities (
    group_id bigint not null,
    authority varchar(50) not null,
    constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);

create table group_members (
    id bigint generated by default as identity(start with 0) primary key,
    username varchar(50) not null,
    group_id bigint not null,
    constraint fk_group_members_group foreign key(group_id) references groups(id)
);

 

설정

예) DataSource

더보기
@Bean
DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(H2) // 임베디드 DB
        .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION) // 기본 users.ddl
        .build();
}

 

예) JdbcUserDetailsManager

더보기
@Bean
UserDetailsManager users(DataSource dataSource) {
    UserDetails user = User.builder()
        .username("user")
        .password("{bcrypt}...") // 암호화된 비밀번호
        .roles("USER")
        .build();

    UserDetails admin = User.builder()
        .username("admin")
        .password("{bcrypt}...") // 동일한 비밀번호 예시
        .roles("USER", "ADMIN")
        .build();

    JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
    users.createUser(user);
    users.createUser(admin);
    return users;
}

 

UserDetails

  • 유저의 정보를 나타내는 인터페이스
  • UserDetailsService에 의해 UserDetails 인터페이스의 구현체가 반환됨

 

DaoAuthenticationProvider
  • 사용자가 로그인 요청을 할 경우 UserDetailsService를 통해 사용자의 정보를 조회합니다.
  • UserDetails 구현체의 필드 정보를 가지고 입력받은 패스워드와 저장된 패스워드를 비교하여 인증을 시도합니다.
  • 인증에 성공하면 인증된 사용자 정보를 사용하여 Authentication을 생성하여 반환합니다.

 

Credentials Management

  • 사용자 인증 정보를 가능한 한 빨리 메모리에서 지우는 보안 관행
항목 설명
즉시 삭제
인증 직후 password 등 민감 정보를 null 처리해야 함
자동 호출 보장
보통 ProviderManager가 eraseCredentials()를 자동 호출함 (직접 호출할 필요는 없음)
일관된 적용
전 애플리케이션에 일관되게 적용해야 보안 구멍을 막을 수 있음

 

CredentialsContainer

  • 민감한 정보를 가진 객체임을 표시하는 인터페이스
  • 이 인터페이스를 활용해 ProviderManager가 인증 성공 후 해당 정보를 메모리에서 지우는 작업을 자동으로 수행
  • UserDetails를 구현한 클래스에서 CredentialsContainer도 구현하는 것이 권장됨

 

동작 방식
  1. 인증이 끝나고 ProviderManager가 Authentication 객체를 반환받음
  2. Authentication 객체가 CredentialsContainer를 구현했다면 → eraseCredentials() 호출됨

 

 

예) MyUserDetails

더보기
public class MyUserDetails implements UserDetails, CredentialsContainer {
    private String username;
    private String password;

    @Override
    public void eraseCredentials() {
        this.password = null;
    }
}

 

UserDetailsService

  • 사용자명(username)을 기반으로 사용자 정보(UserDetails)를 로딩하는 인터페이스
  • 빈으로 등록하면 AuthenticationProvider에 주입됨
항목 설명
핵심 메서드
UserDetails loadUserByUsername(String username)
반환 객체
UserDetails (username, password, roles, 계정 상태 등)
사용 위치
DaoAuthenticationProvider
인증 방식
username/password 기반 인증 (주로 폼 로그인, Basic 인증 등)

 

예) CustomUserDetailsService

더보기
@Component
public class CustomUserDetailsService implements UserDetailsService {

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		UserEntity user = userRepository.findByUsername(username)
			.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));
		
		return User.builder()
			.username(user.getUsername())
			.password(user.getPassword()) // 암호화된 비밀번호
			.roles("USER") // 또는 user.getRoles()
			.build();
	}
}

 

PasswordEncoder

  • PasswordEncoder라는 객체를 사용하여 안전하게 비밀번호를 저장함
  • 빈으로 등록하여 PasswordEncoder 객체를 커스터마이징할 수 있습니다. 

 

DaoAuthenticationProvider

  • AuthenticationProvider 인터페이스 구현체

 

동작 과정

  1. AuthenticationFilter
    • 입력 username, password을 UsernamePasswordAuthenticationToken 형식으로 포장
    • 생성한 UsernamePasswordAuthenticationToken을 AuthenticationManager에게 전달
  2. ProviderManager
    • DaoAuthenticationProvider에 인증 위임
  3. DaoAuthenticationProvider
    • UserDetailsService를 사용하여 UserDetails를 조회
    • PasswordEncoder를 사용하여 UserDetails 패스워드의 유효성을 검증함
  4. 검증 성공 →  Authentication 객체 리턴 (UsernamePasswordAuthenticationToken)

 

 

출처