어제 작성한 게시글 작성/수정/조회 기능을 swagger 로 테스트해봤다.
1 게시글 작성 올바르게 했을 때 이런 에러가 떴다.
InvalidDataAccessResourceUsageException:
ERROR: column "title" of relation "post" does not exist
DB 내에서의 속성과 코드에서 선언한 속성에서 예외가 나올 수 있다고 한다.
DB에 찾아가보니 post 테이블의 title 컬럼을 생성을 안했었다. 생성하고 다시 실행하니 정상적으로 게시글 작성이 됐다.
2 전체 게시글 조회
InvalidDataAccessResourceUsageException
column p1_0.id does not exist
id 가 존재하지 않는다고 뜬다.
이 경우 대부분이 컬럼명이나 쿼리문에 오타가 있을 수 있다고 한다.
게시글과 댓글 각각의 id의 앞에 붙어있던 post reply를 떼주고 기본 id 이름으로 설정하니 정상적으로 조회가 됐다.
3 단건 게시글 조회
정상적으로 조회가 된다.
Exception - 백엔드 로그에만 뜬다.
JWT
: json 포맷을 이용하여 사용자에 대한 정보를 저장하는 claim 기반의 토큰
사용자는 발급받은 jat를 http 요청 헤더 중에 authorization 키값에 Beraer + jwt 토큰 값을 넣어 보낸다.
헤더 Header, 내용 Payload, 서명 Signature 세 부분으로 이루어져 있으며 각 부분은 Base64로 인코딩되어 표현한다.
aaaaaa + bbbbbb + cccccc
헤더 내용 서명
❗헤더
토큰의 타입 + 해싱 알고리즘
타입에는 "JWT" 가 들어간다.
{
"typ": "JWT"
"alg": "HS256"
}
❗내용
토큰과 관련된 정보
내용 한 덩어리를 claim 이라하고 이 클레임은 키값의 한 쌍으로 이루어져 있다.
claim 에는 등록된/공개/비공개 claim 이 있다.
등록된 claim 은 토큰에 대한 정보를 담는 데 사용하고
공개 claim 은 공개되어도 상관없는 claim 을 말하며
비공개 calim 은 공개되면 안 되는 claim 으로 client 와 server 간의 통신에 사용된다.
❗서명
토큰을 인코딩하거나 유효성 검증을 할 때 사용
헤더와 내용의 값을 각각 Bse64로 인코딩하고 인코딩한 값을 비밀키를 이용해 헤더에서 정의한 알고리즘으로 해싱을 하고 다시 Base64로 인코딩하여 생성한다.
복호화 디코딩 : 사람이 읽을 수 있는 형태
🚩 Spring Security + JWT 로 로그인/로그아웃 구현하기
참조
: [스프링 부트 3 백엔드 개발자 되기]
: https://colabear754.tistory.com/171
이메일과 비밀번호를 받아서 회원 등록하고 로그인하면 토큰을 반환한다,
의존성 추가
implementation ("org.springframework.boot:spring-boot-starter-security") //스프링시큐리티
implementation ("io.jsonwebtoken:jjwt:0.9.1") // 자바 JWT 라이브러리
implementation ("javax.xml.bind:jaxb-api:2.3.1") // xml 문서와 자바 객체 간 매핑 자동화
*Spring Security : 스프링 기반의 어플리케이션의 보안을 담당하는 하위 프레임워크
❗Users 엔티티 생성
스프링 시큐리티의 UserDetails 를 상속받아서 getPassword, getUsername 등의 메소드를 구현화한다.
UserDetails는 Spring Security에서 사용자 정보를 저장하는 인터페이스로, 실제 보안 목적으로 직접 사용되지는 않습니다.
대신에 사용자 정보를 저장하고 나중에 Authentication 객체로 캡슐화됩니다.
이를 통해 보안과는 관련 없는 사용자 정보(이메일 주소, 전화번호 등)를 편리하게 저장할 수 있습니다.
Users Entity
@Entity
@Table(name = "users")
class Users(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
@Column(name = "user")
var name: String,
@Column(name = "email", unique = true)
var email: String,
@Column(name = "password")
var userPassword: String
): UserDetails {
// 사용자의 권한을 반환(컬렉션 형태)
override fun getAuthorities(): MutableCollection<out GrantedAuthority?>? {
return List.of(SimpleGrantedAuthority("user"))
}
override fun getPassword(): String {
return userPassword
}
// 사용자를 식별할 수 있는 이름 반환
override fun getUsername(): String {
return email
}
// 계정이 만료되었는지 확인
override fun isAccountNonExpired(): Boolean {
return true
}
// 계정이 잠금되었는지 확인
override fun isAccountNonLocked(): Boolean {
return true
}
// 비밀번호가 만료되었는지 확인
override fun isCredentialsNonExpired(): Boolean {
return true
}
// 계정이 사용가능한지 확인
override fun isEnabled(): Boolean {
return true
}
}
우리가 엔티티 생성할 때 붙이는 @Id 나 @Column 어노테이션들은 jakrta.persistence 에서 임포트해온다.
getAuthorities()
사용자의 권한을 반환하는 역할을 한다. 스프링 시큐리티에서는 사용자에게 부여된 권한을 GrantedAuthoriry 객체로 표현한다. 그리고 이 권한 정보를 컬렉션으로 반환한다.
❗UsersRepoitory 생성
interface UserRepository: JpaRepository<Users, Long> {
fun findByEmail(email: String)
}
❗UserDetailsService 생성
스프링 시큐리티의 UserDetailsService 인터페이스를 상속받아 구현한다. 이 인터페이스의 loadUserByUsername() 메소드를 오버라이딩하여 사용자 정보를 가져오는 로직을 작성한다. 사용자가 로그인하기 위해 이메일을 입력하면 레포지토리에서 이 이메일이 존재하는지 확인하고 해당하는 사용자정보를 가져온다.
사용자의 데이터를 로드하는 핵심 인터페이스.
사용자 이름을 기반으로 사용자를 찾습니다.
* @param username 데이터가 필요한 사용자를 식별하는 사용자 이름
* @return 완전히 채워진 사용자 레코드 (절대 <code>null</code>이 아님)
* @throws UsernameNotFoundException 사용자를 찾을 수 없거나 사용자에게 부여된 GrantedAuthority가 없는 경우 */
UserDetailServiceImpl
@Service
class UserDetailServiceImpl(
private val userRepository: UserRepository
): UserDetailsService {
// 사용자가 입력한 이메일이 존재하는지 검사하고 사용자 정보 반환
override fun loadUserByUsername(username: String): UserDetails {
return userRepository.findByEmail(username) ?: throw UserEmailNotFoundException(username)
}
}
반환타입은 UserDetails 로 나와야하는데 자꾸 Unit 으로 나와서 해결 못 했다.
절대 null 이면 안되니 예외처리 해준게 잘한거 아닌가?
❗JwtTokenProvider 생성
jwt토큰을 발급, 인증 정보 조회, 정보 추출 하는 클래스로 스프링 시큐리티의 UserDetailsService 를 생성자로 받는다.
UserDetailsService 는 UserDetails 객체를 반환한다.
JwtTokenPrivider
// jwt 토큰을 발급, 인증 정보 조회, 회원 정보 추출
@Component
class JwtTokenProvider(
private val userDetailsService: UserDetailsService
) {
private var secretKey = "testsecretkey"
// 토큰 유효시간
private val tokenValidTime = 30*60*1000L
// secretKey를 Base64 로 인코딩
@PostConstruct // 객체의 생성 및 초기화 단계에서 특정 메서드를 호출
protected fun init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.toByteArray())
}
// JWT 토큰 생성
fun makeToken(userPK: String): String {
// JWT payload 에 사용자의 고유 식별값 저장
val claims: Claims = Jwts.claims().setSubject(userPK)
// 추가적으로 사용자 고유 식별값 저장 {key:value} 형태
claims["userPk"] = userPK
val now = Date()
return Jwts.builder()
.setHeaderParams(mapOf("typ" to "JWT")) // 헤더 typ:JWT
.setClaims(claims) // 클레임 저장
.setIssuedAt(now) // 토큰 발행 시간(현재시간)
.setExpiration(Date(now.time + tokenValidTime)) // 토큰 만료 시간
.signWith(io.jsonwebtoken.SignatureAlgorithm.HS256, secretKey) // 비밀키를 이용해 hs256방식으로 암호화
.compact() // jwt 를 문자열로 변환하여 반환
}
// JWT 토큰 유효성 검사 + 만료기간 확인
fun isValidToken(token: String): Boolean {
try {
Jwts.parser()
.setSigningKey(secretKey) // 서명에 필요한 비밀키로 토큰 복호화
.parseClaimsJws(token) // 받아온 토큰이 유효하지 않으면 예외 던짐
return true
} catch (e: Exception) {
return false
}
}
// JWT 토큰에서 인증 정보 조회
fun getAuthentication(token: String): Authentication {
val userDetails = userDetailsService
.loadUserByUsername(getUserPk(token)) // 사용자의 고유 식별값을 추출해서 해당 사용자의 인증 정보를 가져옴
// User~~ 스프링시큐리티 구현체로 사용자의 인증 정보를 나타냄
return UsernamePasswordAuthenticationToken(userDetails, "", userDetails.authorities)
}
}
userDetailsService 에서 가져온 유저 정보는 스프링 시큐리티의 User 객체형태로 반환된다.
목표 달성을 다 못했다...로그인/로그아웃 기능 그냥 쓱 훑어볼 때는 이런거구나 하고 이해했는데 하나하나 쳐보니 에러가 이곳저곳에서 떠서 반의 반도 못 나간 것 같다. 하루가 너무 짧다. 48시간이면 좋겠다. 새로운걸 배우는 건 좋은데 해야 할 것도 많고 건강관리도 해야하고 잘하고 싶은데 내 역량에 달리는거 아닌가 싶다.
'왕초보일지' 카테고리의 다른 글
240111 TIL | (3) | 2024.01.11 |
---|---|
240110 TIL | 스프링시큐리티 로그인 구현하는 중 (1) | 2024.01.10 |
240108 TIL | 7주차 팀프로젝트 (3) | 2024.01.08 |
240105 TIL | Todo과제 (1) | 2024.01.05 |
240104 TIL | (2) | 2024.01.04 |