CustomUserDetailsManager과 CustomUserDetails 만들기
JPA를 활용해서 직접 사용자 정보를 관리해 보자.
>> UserDetailsManager 인터페이스 구현.
1) JPA, SQLite 관련 의존성 추가
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.xerial:sqlite-jdbc:3.41.2.2'
runtimeOnly 'org.hibernate.orm:hibernate-community-dialects:6.2.4.Final'
// application.yaml
spring:
datasource:
url: jdbc:sqlite:db.sqlite
driver-class-name: org.sqlite.JDBC
username: sa
password: password
jpa:
hibernate:
ddl-auto: create
database-platform: org.hibernate.community.dialect.SQLiteDialect
show-sql: true
2) User 엔티티 추가 (Security 내부의 User와 헷갈리기 때문에 UserEntity로 설정.
// UserEntity class
@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
private String email;
private String phone;
}
3) JpaRepository 생성
// UserRepository interface
@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByUsername(String username);
boolean existsByUsername(String username);
}
4) UserDetailsManager 인터페이스를 구현하는 CustomUserDetailsManager 클래스 만들기.
- UserDetailsManager는 UserDetailsService를 상속받는 인터페이스이다.
- 결국 타고타고 UserDetailsService를 구현한 구현체라는 것이다. 그래서 loadUserByUsername이 정상 동작 해야 한다.
>> loadUserByUsername : Spring Security 내부에서 사용자 인증 과정에서 활용하는 메서드임!
![](https://blog.kakaocdn.net/dn/bsya4S/btsD0ImRMKG/U4D0OAYZwHeNBb3rKnNfe0/img.png)
- UserDetailsManager : new users를 생성하고, 이미 있는 유저를 업데이트 할 수 있는 기능을 가진 UserDetailsService의 확장.
![](https://blog.kakaocdn.net/dn/CxmYU/btsD0Mv5mzR/XAWYWzF9OcXKkg2QNlJnc1/img.png)
- UserDetailsService : 프레임워크 내에서 User DAO로 사용된다. 유저의 정보를 load 하는 interface
그러면 CustomUserDetailsManager 만들어보자!!
- 필요한 것들을 모두 오버라이딩했다. 여기서 일단은 createUser, loadUserByUsername, userExists 세 가지를 구현해 보기로 하였다.
// CustomUserDetailsManager class
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomUserDetailsManager implements UserDetailsManager {
//repository를 사용해야하기 때문에 DI 해주기
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
@Override
public void createUser(UserDetails user) {}
@Override
public boolean userExists(String username) {
return false;
}
@Override
public void updateUser(UserDetails user) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED);
}
@Override
public void deleteUser(String username) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED);
}
@Override
public void changePassword(String oldPassword, String newPassword) {
throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED);
}
}
- 하나씩 살펴보자!
1) loadUserByUsername(String username)
// CustomUserDetailsManager class
//formLogin 등 Spring Security 내부에서 인증을 처리할 때 사용하는 메서드.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// repository에서 username으로 찾아오기
Optional<UserEntity> optionalUser = userRepository.findByUsername(username);
// username이 없다면 예외처리
if (optionalUser.isEmpty()) {
throw new UsernameNotFoundException("username not found");
}
// 있다면 엔티티 가져오기
UserEntity userEntity = optionalUser.get();
// 리턴을 UserDetails로 해야하기 때문에 UserDetails 형태로 바꿔주기
UserDetails userDetails = User.withUsername(userEntity.getUsername())
.password(userEntity.getPassword())
.build();
return userDetails;
}
** 근데, 커스텀 하는김에 UserDetails도 커스텀할 수 있다는 사실~!~! 아시나요~! ㅋ ㅎ ㅋ ㅎ 전 몰랐슴다
여기에 대한 설명은 아래에 접어두겠다.
똑같은 원리로 UserDetails도 interface로 core user information을 제공한다.
- 설명을 보자면 Spring Security에서 구현체들이 바로 사용되지는 않는다. 그저 사용자의 정보를 저장할 뿐 > 이후 Authentication object로 캡슐화될 것이라는 것.
![](https://blog.kakaocdn.net/dn/bUq3wT/btsDZj2xJe3/5AJJOiakjKFhU6L3ak5hv1/img.png)
- 그리고 얘를 구현한 구현체!는 User라고 적혀있다. 그럼 User를 살펴보자
core user information을 model 한다!! UserDetails에 있었던 메서드들을 오버라이딩하여 구현해 둔 것.
그렇지만, 개발자들이 자신만의 UserDetails 구현체를 만들 수도 있다고 한다. 그것이 CustomUserDetails겠죠!?
![](https://blog.kakaocdn.net/dn/bbVxew/btsDZl0oBU4/bKPB0kZcREWIqlu2959ZIK/img.png)
![](https://blog.kakaocdn.net/dn/B4ArI/btsD2JFGBPe/AikwNU93YVHIx86qurkIsK/img.png)
- 간단하게 만들어본 CustomUserDetails. :: username, password 제외하고 id, email, phone도 가져올 수 있도록 Getter도 붙여주었다. >> 똑같이 UserDetails 형태가 필요한 곳에서 사용 가능하다.
(참고로 나는 바보처럼 밑에 있는 isAccounrt관련 boolean값들을 기본으로 되어있던 return false로 놔두고 계속 시도해서 로그인 시도가 계속 막혔다... ㅎㅋ 바보인증)
// CustomUserDetails class
//UserEntity를 바탕으로 Spring Security 내부에서
// 사용자 정보를 주고받기 위한 객체임을 나타내는 interface UserDetails의 커스텀 구현체
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {
@Getter
private Long id;
private String username;
private String password;
@Getter
private String email;
@Getter
private String phone;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Set.of();
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.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;
}
}
** 커스텀한 UserDetails로 해본 버전!!
// CustomUserDetailsManager class
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// repository에서 username으로 찾아오기
Optional<UserEntity> optionalUser = userRepository.findByUsername(username);
// username이 없다면 예외처리
if (optionalUser.isEmpty()) {
throw new UsernameNotFoundException("username not found");
}
// 있다면 엔티티 가져오기
UserEntity userEntity = optionalUser.get();
// 리턴을 UserDetails로 해야하기 때문에 UserDetails 형태로 바꿔주기
UserDetails userDetails = CustomUserDetails.builder()
.id(userEntity.getId())
.username(userEntity.getUsername())
.password(userEntity.getPassword())
.email(userEntity.getEmail())
.phone(userEntity.getPhone())
.build();
return userDetails;
}
2) userExists(String username)
- 이번에는 username으로 사용자가 존재하는지를 확인하는 메서드를 구현해보자.
얘는 간단하게 JpaRepository로 구현한 것을 가져오면 된다.
// CustomUserDetailsManager class
@Override
public boolean userExists(String username) {
return userRepository.existsByUsername(username);
}
3) createUser(UserDetails user)
- 이번에는 UserDetails형태로 넘어온 사용자 정보를 통해 등록하는 과정을 구현해 보자. (단, UserDetails는 비밀번호 암호화가 되어있는 상태라고 가정하고 진행한다.)
// CustomUserDetailsManager class
@Override
public void createUser(UserDetails user) {
// 만약 등록하려는 사용자의 유저네임이 겹치면 bad request
if (this.userExists(user.getUsername())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
// 겹치지 않으면 등록 과정을 진행한다.
UserEntity newUser = UserEntity.builder()
.username(user.getUsername())
.password(user.getPassword())
.build();
userRepository.save(newUser);
}
- 이렇게 하면 유저의 유저네임과 비밀번호가 저장이 된다.
음, 그러나, 이메일과 휴대폰번호는 저장이 되지 않는다. 왜냐하면 넘어온 UserDetails는 구현체가 User이기 때문이다.
그러면 이메일과 휴대폰번호까지 포함해서 저장하고 싶다면!?
=> CustomUserDetails를 사용하는 것이다.
- CustomUserDetails 사용 ver. :: 받아온 UserDetails를 CustomUserDetails로 클래스캐스팅 해주어야 한다.
@Override
public void createUser(UserDetails user) {
// 만약 등록하려는 사용자의 유저네임이 겹치면 bad request
if (this.userExists(user.getUsername())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
// 겹치지 않으면 등록 과정을 진행한다.(try-catch로 Exception 처리)
try {
CustomUserDetails userDetails = (CustomUserDetails) user; //클래스 캐스팅해줌.
UserEntity newUser = UserEntity.builder()
.username(userDetails.getUsername())
.password(userDetails.getPassword())
.email(userDetails.getEmail())
.phone(userDetails.getPhone())
.build();
userRepository.save(newUser);
} catch (ClassCastException e) { //ClassCasting 예외발생시 에러로깅, 서버에러 예외처리
log.error("Failed Cast to: {}", CustomUserDetails.class);
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
+ 테스트용으로 사용자를 하나 추가해 준다. (Constructor)
// CustomUserDetailsManager class
public CustomUserDetailsManager(UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
CustomUserDetails userDetails = CustomUserDetails.builder()
.id(1L)
.username("user1")
.password(passwordEncoder.encode("password1"))
.email("email@naver.com")
.phone("01011112222")
.build();
createUser(userDetails);
}
>> 짠 ! 결과가 잘 나왔다~~~~~~
>> 새로 회원가입을 해도 잘 나온다. (phone, email은 입력하지 않아서 nulll로 나온다!)
난 있는 대로 쓰고, 주는 대로 먹는 찐딴데.. 커스텀을 여기서 하네 ^.^ 헷
그래도 재밌다 내 멋대로 만드는 유저디테일 ㅋㅎ
'Programming > Spring, SpringBoot' 카테고리의 다른 글
HandlerInterceptor & Filter / SecurityFilterChain (0) | 2024.01.27 |
---|---|
Spring의 Dispatcher Servlet (3) | 2024.01.26 |
Spring Security... 그리고 인터페이스 (0) | 2024.01.25 |
Testing (1) | 2024.01.24 |
Exception & Validation (0) | 2024.01.23 |