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

좋은 웹페이지 즐겨찾기