Spring Security 공부 중
구직 중이라 정신 없는 요즘... 유효성 검사 때문에 열심히 정규식 짜는 도중 갑자기 생각난 Spring Security. 알게 됐으니 써봐야지.
그 전에 github 주소를 공유하겠다.
https://github.com/tladbstjsdl
열심히 JPA 공부하고 써보고 정규식 짜고 Spring Security도 공부하고 써보고 하느라 진행이 매우 더디다. 면접도 합격할것처럼 분위기는 좋았는데 자꾸 떨어지니 멘탈도 갈려나가고 여러모로 슬픈 상황. 코딩하다가 문득문득 정신이 나갈거같다. 학문의 재미랑 별개로 구직은 참...
쨋든 각설하고 가보자.
프로젝트 구조
Spring Security 설정을 위한 WebSercurityConfig.kt 클래스파일
회원가입 유효성 검사를 위한 validation.js
WebSecurityConfig.kt
import com.dummy.wordbook.member.service.MemberService import org.springframework.boot.autoconfigure.security.servlet.PathRequest import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.builders.WebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.crypto.factory.PasswordEncoderFactories import org.springframework.security.crypto.password.PasswordEncoder @Configuration @EnableWebSecurity class WebSecurityConfig(private val memberService: MemberService) : WebSecurityConfigurerAdapter() { @Bean public fun passwordEncoder(): PasswordEncoder { return PasswordEncoderFactories.createDelegatingPasswordEncoder() } override fun configure(web: WebSecurity?) { web?.ignoring()?.requestMatchers(PathRequest.toStaticResources().atCommonLocations()); } override fun configure(http: HttpSecurity?) { http?.authorizeRequests() ?.antMatchers("/", "/insertForm")?.permitAll() ?.antMatchers("/wordbook", "/wordbooklist")?.authenticated() ?.antMatchers("/notice/insert")?.hasRole("admin") ?.and() ?.formLogin() ?.loginPage("/") ?.defaultSuccessUrl("/") ?.permitAll() ?.usernameParameter("memberId") ?.and() ?.logout() ?.invalidateHttpSession(true) } /*override fun configure(auth: AuthenticationManagerBuilder?) { auth?.userDetailsService(memberService)?.passwordEncoder(passwordEncoder()) }*/ }
위에부터 Encoder로 쓸 Bean 등록, static(js, css 등) 파일 권한 확인 무시, 페이지 접근 권한 및 페이지 설정, 비밀번호 암호화를 위한 설정(각주 처리).
PasswordEncoderFactories.createDelegatingPasswordEncoder 은 기본으로 BCrypt 사용.
맨 아래 auth 쪽은 어떤 기능인지 몰라서 일단 각주처리했다. 아마 특정 페이지 들어갈 때 권한확인 시 쓰는 듯 한데, 없어도 지금까지 구현한 기능은 문제 없이 돌아간다.
Member.kt(추가)
@PrePersist @PreUpdate fun prePersistUpdate() { this.certified = this.certified?: 0 }
insert나 update 실행 시 certified 값이 null로 들어오면 0으로 바꾼 후 실행
값을 일부만 입력하는 기능이 없는 JPA 특성 상 default 값을 쓸 수가 없어서 PrePersist랑 PreUpdate를 사용했다.
MemberController.kt
import com.dummy.wordbook.member.dto.MemberDto import com.dummy.wordbook.member.entity.Member import com.dummy.wordbook.member.service.MemberService import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseBody import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpSession @Controller class MemberController(private val memberService: MemberService, private val passwordEncoder: PasswordEncoder) { @RequestMapping("/") public fun indexPage(m: Model, session: HttpSession): String { if(session.getAttribute("loginMember") == null) { m.addAttribute("memberDto", MemberDto(null, "", "", null, null, null, null, 0)) } return "index" } @PostMapping("/loginConfirm") @ResponseBody public fun loginConfirm(memberDto: MemberDto, session: HttpSession): String { val memberId: String = memberDto.memberId val password: String = memberDto.password memberService.findByMemberId(memberId)?.let { member -> if(passwordEncoder.matches(password, member.password)) { session.setAttribute("loginMember", member) return "loginSuccess" } return "passwordError" } return "memberIdError" } @PostMapping("/insertForm") public fun insertForm(m: Model): String { m.addAttribute("memberDto", MemberDto("", "", "", "", null)) return "insertForm" } @PostMapping("/insertMember") public fun insertMember(req: HttpServletRequest, m: Model): String { val memberId: String = req.getParameter("memberId") memberService.save(Member(memberId, passwordEncoder.encode(req.getParameter("password")), req.getParameter("email"), req.getParameter("phone"), req.getParameter("address"))).run { m.addAttribute("newMember", memberService.findByMemberId(memberId)) } return "insertComplete" } }
비밀번호 보안을 위한 passwordEncoder를 사용하기 위해 WebSecurityConfig.kt 에서 등록한 bean을 생성자 의존성 주입 방식으로 가져옴.
passwordEncoder.matches(password, member.password): 사용자가 입력한 비밀번호(인코딩 전/rawPassword)와 DB에 저장된 비밀번호(인코딩 후)를 알아서 비교해준 후 boolean값을 반환.
save 함수 실행시마다 비밀번호에 encode를 일일히 해야한다. 처음에는 save 함수 정의에서 encode를 하려고 MemberServiceImpl.kt 에서 생성자 주입을 했었는데, WebSecurityConfig.kt 에서 auth 부분에 쓰려고 MemberService를 주입해서 순환참조로 컴파일 에러가 났다(이것이 현대기술인가? 재밌넹). 물론 지금은 쓰질 않아서 그렇게 가능하긴 하지만, 나중에 쓰게 될 수도 있기에 그냥 이대로 가려고 한다.
MemberService.kt
import com.dummy.wordbook.member.entity.Member import org.springframework.security.core.userdetails.UserDetailsService interface MemberService : UserDetailsService { public fun findByMemberId(memberId: String): Member? public fun save(member: Member) }
UserDetailsService를 상속
MemberServiceImpl.kt
import com.dummy.wordbook.member.entity.Member import com.dummy.wordbook.member.entity.MemberRepository import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.UserDetails import org.springframework.stereotype.Service @Service class MemberServiceImpl(private val memberRepository: MemberRepository) : MemberService { override fun findByMemberId(memberId: String): Member? { return memberRepository.findByMemberId(memberId) } override fun save(member: Member) { memberRepository.save(Member(member.memberId, member.password, member.email, member.phone, member.address)) } override fun loadUserByUsername(username: String?): UserDetails { var authority: MutableList<GrantedAuthority> = ArrayList() var member = findByMemberId(username!!)?: Member("", "", "", "", null) member.certified.let { certified -> if(certified!!.toInt() == 2) authority.add(SimpleGrantedAuthority("ROLE_ADMIN")) else authority.add(SimpleGrantedAuthority("ROLE_MEMBER")) } return User(username, member.password, authority) } }
UserDetailsService를 상속받아 loadUserByUsername(String)을 override
return으로 User(String, String, MutableList)
certified가 2면 관리자, 아니면 일반 유저.
Java를 기준으로 만들어진 라이브러리들을 Kotlin으로 가져오니 null 처리가 좀 정신없다. 게다가 Java에서 List로 가져오는 라이브러리가 Kotlin에선 MutableList다. 그냥 List도 있는데, 이건 add 함수가 없다. 덕분에 시간 꽤 날렸다.
지금은 loadUserByUsername을 쓸 일이 없다. 이게 아마 WebSecurityConfig.kt 에서 auth랑 같이 연계되서 쓰는 페이지별 권한 확인용인 듯 하다.
특별히 올릴만한 것은 다 올렸다. 앞으로는 모르는 사람들과 팀 프로젝트를 하게 될 예정이라 이 프로젝트는 거의 못할 듯 하다. 그래도 지금까지 했던 것들을 나중에 보고 기억할 수 있을거라 믿는다.
참고 페이지
Spring Security 사용: https://victorydntmd.tistory.com/328
Author And Source
이 문제에 관하여(Spring Security 공부 중), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@tladbstjsdl/Spring-Security-공부-중저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)