마지막 날입니다. 오늘의 내용은 ... 원래는 05-2 였는데 불필요한 내용 같아서 스킵했습니다. 거의 같은 내용이 나오더라구요
오늘의 내용은 데이터베이스에 사용자를 집어넣는 방법입니다 (강의 자료). 별 건 없고 클래스 두 세 개를 상속해서 넣으면 됩니다.
DB에 계정 & 암호 넣기
: DaoAuthenticationProvider & UserDetails
언제나처럼 강의자료가 일부 혼동을 유발하고는 있지만, 이번에는 그래도 전체적으로 어떻게 흘러가는지에 대한 내용을 파악하는 데에는 큰 도움이 됩니다. 공식 문서에는 상세 객체까지 오가는 그림은 없더라구요.
흐름은 UsernamePasswordAuthenticationFilter 에서 시작됩니다.
- a. 사용자가 ID와 암호를 입력합니다.
- b. SecurityFilter 중 UsernamePasswordAuthenticationFilter 에서 사용자가 입력한 ID와 암호로 UsernamePasswordAuthenticationToken 을 만듭니다.
- (1). 그리고 이걸(*UserPwAuthToken) AuthenticationManager 에게 처리하라고 넘겨줍니다.
- (2) 실제로 처리하는 건 ProviderManager 안에서 관리되고 있는 DaoAuthenticationProvider 입니다.
(이거 실습 때 등록하는 장면이 없었는데 기본으로 등록되어 있던 아니면 서비스를 설정하면 자동으로 추가되던 하나보네요) - (3) MyUserService 에서 DB 조회를 해서 확인합니다. 서비스라는 말은 다른 곳에 User 와 UserRepo 가 있다는 뜻이죠. User도 Spring Security 가 요구하는 부모 클래스를 상속해야 합니다. UserRepo는 그냥 JPA꺼 상속해도 됩니다. (그 말은 JPA가 아니어도 된다는 뜻이겠죠.)
- 이번 강의에서는 User를 만드는 과정에서 겸사겸사 GrantedAuthority 도 필드로 등록합니다. 하지만 이것과 관련된 자세한 설명은 없었고, 이전 강의에서의 샘플이 원래처럼 동작하는 것만을 보여주셨습니다.
- (4) PasswordEncoder 에 대해서는 강의에서는 실습용으로 NoOpPasswordEncoder 를 씁니다. h2 DB의 UI 에서 사용자를 추가하기 위함이었는데요, 그것 외에 왜 그냥 @Bean 으로 만들면 되는지 같은 부분에 대한 자세한 설명은 없었습니다.
- (5) Cred의 Username과 Password가 UserDetails 객체와 Authorities 로 바뀐 UserPwAuthToken 이 반환됩니다. 스레드용 임시 싱글턴인 SecurityContextHolder 에 세팅된다고 하네요. 하긴 그렇겠죠 스레드로컬 공간을 쓰는 (투덜투덜) 디자인이니 (투덜투덜)
해야할 것들
흐름에서 뭘 해야하는지가 나왔습니다.
- GrantedAuthority 를 상속받아 MyAuthority 를 만듭니다. MyUser의 필드로 만들기 위함입니다.
권한은 다음장이니까 다음장에 (아마) 나올거에요.
이거 관련해서는 JPA 강의에서 안 배웠던 게 나옵니다. - UserDetails 를 상속받아 MyUser 를 만듭니다. DB에 저장할 거니까 @Entity 로 만듭니다.
- 멤버로 Set<MyAuthority> authorities; 를 만들어서 권한 정보도 저장할 수 있습니다.
- MyUserRepo 를 만듭니다. JPA 아시죠? ㅎㅎ JpaRepository 를 상속하면 됩니다. 그냥 평범한 JPA에요.
- UserDetailsService 를 상속받아 MyUserService 를 만듭니다. 적당한 메소드 오버라이딩 (이름: loadUserByUsername) 에서 사용자를 조회해서, 없으면 UsernameNotFoundException() 을 던지고 있으면 그걸 반환하면 됩니다.
- 내 앱의 SecurityConfig 의 override configure (auth) 에서 auth.userDetailsService(인젝션받은 MyUserService) 를 하면 끝납니다.
실습
이걸 해야하나... DB 설정 진-짜 귀찮은데... 오늘 벌써 2시간 썼고... 실행은 안 해보고 코드에서 오류가 나는지만 확인해봅시다.
class MyAuthority: GrantedAuthority
@Entity
@IdClass(MyAuthority::class)
class MyAuthority: GrantedAuthority {
@Id
var id: Long = 0
@Id
@Column(name = "authority")
var _authority: String = ""
override fun getAuthority(): String = _authority
}
까먹은건지 처음 나오는건지 Composite Key 를 @IdClass(클래스명) 으로 구성하는 게 나옵니다.
두 번째로는 kotlin 의 한계인데, interface GrantedAuthority 의 메소드를 필드로 오버라이드할 수는 없습니다. 그래서 별도로 우회를 해야한다능... S/O 에서 KT-6653 이래요.
https://stackoverflow.com/q/38395115
class MyUser: UserDetails 와 repository
같은 이유로 password 필드는 이름을 바꿨습니다. 아마 해싱 되어있겠죠?
@Entity
class MyUser: UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0
@OneToMany(fetch = FetchType.EAGER, cascade = [CascadeType.ALL])
@JoinColumn(name="user_id", foreignKey = ForeignKey(name = "user_id"))
var authorities: MutableSet<MyAuthority> = mutableSetOf()
override fun getAuthorities(): MutableCollection<out GrantedAuthority> = authorities
var email: String = ""
override fun getUsername(): String = email
var hashedPassword: String = ""
override fun getPassword(): String = hashedPassword
var enabled: Boolean = false
override fun isAccountNonExpired(): Boolean = enabled
override fun isAccountNonLocked(): Boolean = enabled
override fun isCredentialsNonExpired(): Boolean = enabled
override fun isEnabled(): Boolean = enabled
}
여기서 눈에 띄는 점은 JoinColumn 으로 직접 join 대상을 지정하고 있다는 건데요, ForeignKey 를 지정하는 게 나왔던가? 싶네요.
interface MyUserRepo: JpaRepository<MyUser, Long> {
fun findByEmail(email: String): MyUser?
}
JPARepo 는 뭐 그냥저냥 ㅎㅎ
class MyUserService: UserDetailsService
그럴리는 없지만 username이 null 인 경우도 처리해줬습니다. 역시나 특정 익셉션을 잘 던지는 게 포인트.
@Service
@Transactional
open class MyUserService(
private val userRepo: MyUserRepo
): UserDetailsService {
override fun loadUserByUsername(username: String?): UserDetails {
val email = username ?: throw UsernameNotFoundException("<null>")
return userRepo.findByEmail(email) ?: throw UsernameNotFoundException(email)
}
}
설정
userDetailsService 딱 한 줄이 중요합니다. 나머지는 passwordEncoder bean 인데... bean 으로 선언하면 다른데에 주입이 되겠...죠? 특히 PasswordEncoder 가 필요한 곳... 글쎄요 이걸 확인해보려면 디버깅을 해봐야하는데 실행은 안 해볼거라 (여전히 DB 설정이 귀찮음)
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
open class SecurityConf: WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
생략
}
@Bean
open fun passwordEncoder() = BCryptPasswordEncoder()
@Autowired
lateinit var userService: MyUserService // 강의에서는 constructor injection
override fun configure(auth: AuthenticationManagerBuilder) {
// super.configure(auth)
// auth.inMemoryAuthentication()
// .withUser(
// .username("user1")
// .password("1111")
// .roles("USER")
// .build()
// )
auth.userDetailsService(userService)
}
override fun configure(web: WebSecurity) {
생략
}
}
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다.
패스트캠퍼스: https://bit.ly/37BpXiC
#패스트캠퍼스 #패캠챌린지 #직장인인강 #직장인자기계발 #패스트캠퍼스후기 #한번에끝내는JavaSpring웹개발마스터초격차패키지Online
이걸로 앵간한 웹사이트는 만들 수 있게 되었습니다. 처음부터 만들거면 더 배워서 JWT 같은 걸 쓰겠지만요. Spring 을 쓰지 않더라도 다른 언어로는 이런 게 있을까? 하고 찾아볼 수 있게 되어서 좋은 거 같네요.
아무튼 이걸로 패캠 50일 챌린지도 끝입니다. 아마 후기를 남기라고 할 거 같은 "마지막 미션" 만 남게 됩니다. 다음주 월요일에 노티가 날라오는데, 그 때 까지 쉬기도 하고 1 game 1 month 하기로 했던거도 마저 공부하고 그러고 있어야겠습니다.
내일도 화이팅! (다른 걸 하겠지만요!)
'FastCampus - 한번에 끝내는 Java|Spring 웹 개발 > 04-2 시큐리티' 카테고리의 다른 글
Security 02-05: Basic Auth (2) - 패스트캠퍼스 챌린지 49일차 (0) | 2022.03.13 |
---|---|
Security 02-05: Basic Auth - 패스트캠퍼스 챌린지 48일차 (0) | 2022.03.12 |
Security 02-04: Authentication 메커니즘 - 패스트캠퍼스 챌린지 47일차 (0) | 2022.03.11 |
Security 02: 전체 구조 - 패스트캠퍼스 챌린지 46일차 (0) | 2022.03.10 |
Security 01: 개요 및 맛보기 - 패스트캠퍼스 챌린지 42일차 (0) | 2022.03.06 |