Netplix 구독형 멤버십 프로젝트로 배우는 SpringSecurity
[Ch 3. UserDetails와 사용자 관리] - 01. UserDetails 살펴보기
강의를 바탕으로 실습 내용을 정리하였습니다.
목차
1. UserDetailsService 구현
InMemoryUserDetailsManager 를 활용해서 UserDetailsService 를 구현해보자
- 이를 위해 먼저 User 를 구현해야 함
User 클래스
- username, password, authority 는 모두 final 으로 설정하여 값을 변경할 수 없도록 지정
- 간단한 예제를 위해서 authority 는 하나만 설정
package fast.campus.fcss01.user;
...
public class User implements UserDetails {
private final String username;
private final String password;
private final String authority;
public User(String username, String password, String authority) {
this.username = username;
this.password = password;
this.authority = authority;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> authority);
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
}
인메모리 기반 UserDetailsService 구현
UserDetailsService 를 구현하는 InMemoryUserDetailsService 를 생성
- 인메모리에서 사용자를 조회하기 위해 먼저 사용자 List 객체를 생성 (
List<UserDetails>
) - loadUserByUsername 메소드에서 users 를 순회하며 username 으로 검색
- 만약 존재하지 않는다면 UsernameNotFoundException 을 던지도록 구현
package fast.campus.fcss01.user;
...
public class InMemoryUserDetailsService implements UserDetailsService {
private final List<UserDetails> users;
public InMemoryUserDetailsService(List<UserDetails> users) {
this.users = users;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return users.stream()
.filter(user -> user.getUsername().equals(username))
.findFirst()
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}
UserDetailsService 와 PasswordEncoder 빈 등록
설정 클래스를 통해 UserDetailsService 와 PasswordEncoder 를 빈으로 등록
- 3명의 임시 유저를 등록
- InMemoryUserDetailsService 를 UserDetailsService 로 지정
- PasswordEncoder 로는 NoOpPasswordEncoder 활용
package fast.campus.fcss01.config;
...
@Configuration // 클래스를 구성 클래스로 구분
public class SecurityConfig {
@Bean // 반환되는 값을 스프링 컨텍스트에 반영
public UserDetailsService userDetailsService() {
UserDetails danny = User.withUsername("danny.kim")
.password("12345")
.build();
UserDetails steve = User.withUsername("steve.kim")
.password("23456")
.build();
UserDetails harris = User.withUsername("harris.kim")
.password("34567")
.build();
List<UserDetails> users = List.of(danny, steve, harris);
return new InMemoryUserDetailsService(users);
}
@Bean
public PasswordEncoder passwordEncoder() {
// PasswordEncoder 로 NoOpPasswordEncoder를 활용
return NoOpPasswordEncoder.getInstance();
}
}
API 테스트 해보기
간단한 컨트롤러 생성
package fast.campus.fcss01.controller;
...
@RestController
public class HelloController {
@GetMapping("/api/v1/hello")
public String hello() {
return "Hello, Spring Security";
}
}
테스트를 위해 build.gradle에서 “jpa” 의존성을 주석처리함
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
// implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
데이터베이스를 사용하지 않고 InMemoryUserDetailsService
로 사용자 인증을 처리하기 위해 주석처힘
username 또는 비밀번호가 틀렸을 때
- 401 Unauthorized
정상적인 케이스일 때
- danny.kim // 12345 입력
- 200 OK 응답
- “Hello, Spring Security” 반환
2. UserDetailsManager 구현
일반적인 애플리케이션에는 사용자를 관리하는 기능이 필요함
- 회원가입을 통해 새로운 사용자 추가
- 내 정보에서 사용자 정보 수정
- 회원 탈퇴를 위한 사용자 삭제
이런 기능을 위해서는 UserDetailsManager 가 필요함
- 인터페이스로써 구현체가 필요함
사용자 세부 정보 서비스에서는 MySQL 데이터베이스와 연결을 하여 사용자를 DB 에서 조회함
암호 인코더는 기본을 사용
JdbcUserDetailsManager 이용
데이터베이스에서 사용자를 관리하는 경우에는 JdbcUserDetailsManager 를 활용할 수 있음
- 데이터베이스는 MySQL 을 사용
- 사용자를 위한 users 테이블과 권한 관리를 위한 authorities 테이블을 생성
- users 테이블에는 사용자 ID, 암호, 활성화 여부를 저장하는 3개의 필드가 존재함
- authorities 테이블에는 사용자 ID 와 그 사용자에게 부여된 권한을 나타내는 authority 필드가 존재함
스프링부트에서는 schema.sql 과 data.sql 파일을 통해 초기 테이블을 설정할 수 있음
프로젝트 의존성
Lombok, Spring Web, Spring Security, Spring Data JPA, MySQL Driver 의존성을 추가함
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
도커를 활용하여 로컬에 MySQL 띄우기
도커 설치 확인
$ docker -v
도커 실행 툴 OrbStack 다운로드
$ brew install orbstack
MySQL 도커 이미지 다운로드
$ docker pull mysql:latest
다운로드한 도커 이미지 확인
$ docker images
MySQL 도커 컨테이너 생성 및 실행
$ docker run --name spring-security-mysql -e MYSQL_ROOT_PASSWORD=1234 -d -p 3306:3306 mysql:latest
나는 윈도우 환경이라 WSL2에서 mysql 컨테이너를 실행했다.
Datagrip 으로 데이터베이스 조회
추가 -> Data Source -> MySQL 선택
host: localhost
port: 3306
username: root
password: admin
정보 입력 후 완료
SQL Console에서 데이터베이스 생성
Database name 으로 spring 설정
application.properties 설정
spring.datasource, spring.jpa, spring.sql 관련 설정 추가
- driver-class-name=com.mysql.cj.jdbc.Driver => MySQL 드라이버 설정
- url=jdbc:mysql://localhost:3306/spring => 로컬의 3306 포트의 spring 데이터베이스로 연결
- username=root => 기본값인 root 로 설정
- password=admin => 도커 설정 시 입력한 패스워드인 1234 로 설정
- hibernate.dialect => MySQLDialect 로 설정
- ddl-auto=create-drop => 애플리케이션 실행 후 종료 시 테이블 삭제
- init.mode=always => 애플리케이션 실행 시 sql 파일 실행
- data, schema-locations 으로 sql 파일 위치 선정
spring.application.name=fcss-01
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/spring
spring.datasource.username=root
spring.datasource.password=1234
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.sql.init.mode=always
spring.sql.init.data-locations=classpath:data/data.sql
spring.sql.init.schema-locations=classpath:data/schema.sql
ddl-auto=create-drop 가 적용되지 않는 문제 발생
설정대로 라면 애플리케이션을 실행하고 중지하면 테이블이 삭제되어야 하는데,
다음 실행 시 데이터가 그대로 남아있고 실행할 때마다 데이터가 쌓임.
schema.sql
resources 아래에 data 디렉토리 생성
- data 디렉토리 하위에 schema.sql 파일 생성
- users 테이블과 authorities 테이블 생성
CREATE TABLE IF NOT EXISTS `spring`.`users` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(100) NOT NULL,
`password` VARCHAR(100) NOT NULL,
`enabled` INT NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE IF NOT EXISTS `spring`.`authorities` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(100) NOT NULL,
`authority` VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`)
);
data.sql
resources/data 디렉토리 하위에 data.sql 파일 생성
- authorities 와 users 테이블에 각각 1개의 row 를 입력
INSERT INTO `spring`.`authorities` VALUES (NULL, 'danny.kim', 'WRITE');
INSERT INTO `spring`.`users` VALUES (NULL, 'danny.kim', '12345', TRUE);
데이터 조회
Datagrip 에서 데이터 조회 시도
- data.sql 을 통해 입력한 데이터 1건 조회 성공
설정 클래스 생성
config 패키지 생성 후 하위에 SecurityConfig 클래스 생성
- DataSource 를 파라미터로 주입받음
- JdbcUserDetailsManager 를 반환하도록 설정
package fast.campus.fcss01.config;
...
@Configuration // 클래스를 구성 클래스로 구분
public class SecurityConfig {
@Bean // 반환되는 값을 스프링 컨텍스트에 반영
public UserDetailsService userDetailsService(DataSource dataSource) {
return new **JdbcUserDetailsManager**(dataSource);
}
@Bean
public PasswordEncoder passwordEncoder() {
// PasswordEncoder 로 NoOpPasswordEncoder를 활용
return NoOpPasswordEncoder.getInstance();
}
}
포스트맨을 활용하여 테스트
데이터베이스에 존재하는 유저 정보를 정확하게 입력하면 조회 성공
- danny.kim // 12345
만약 정보를 잘못 입력하면 401 Unauthorized 응답을 받음
GitHub
https://github.com/lpromotion/fcss-01/commit/10d92f596287d15bd3a0b6f13fe681223c89dcf6
https://github.com/lpromotion/fcss-01/commit/880508e2e716ff6790b6c4613d34ec3c36467b5d
'Course > Spring Security' 카테고리의 다른 글
[netplix-security-a] AuthenticationProvider 구현 (0) | 2024.10.03 |
---|---|
[netplix-security-a] AOP를 활용하여 비밀번호 암호화하기 (1) | 2024.10.03 |
[netplix-security-a] PasswordEncoder 구현 (0) | 2024.10.03 |
[netplix-security-a] UserDetails와 영속성 엔티티의 분리된 구현 (0) | 2024.09.29 |
[netplix-security-a] 기본 구성 재정의 (0) | 2024.09.29 |
댓글