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 |
인증 흐름
- 인증되지 않은 사용자가 보호된 리스소에 접근
- AuthorizationFilter에 의해 요청이 거부됨 (AccessDeniedException)
- ExceptionTranslationFilter가 인증을 시작함
- 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도 구현하는 것이 권장됨
동작 방식
- 인증이 끝나고 ProviderManager가 Authentication 객체를 반환받음
- 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 인터페이스 구현체
동작 과정
- AuthenticationFilter
- 입력 username, password을 UsernamePasswordAuthenticationToken 형식으로 포장
- 생성한 UsernamePasswordAuthenticationToken을 AuthenticationManager에게 전달
- ProviderManager
- DaoAuthenticationProvider에 인증 위임
- DaoAuthenticationProvider
- UserDetailsService를 사용하여 UserDetails를 조회
- PasswordEncoder를 사용하여 UserDetails 패스워드의 유효성을 검증함
- 검증 성공 → Authentication 객체 리턴 (UsernamePasswordAuthenticationToken)
출처