🚩 스프링 시큐리티 인증 403 예외처리
인증이 안된 상태로 시도할 때 401이 떠야하는데 403이 뜬다.
=> AuthenticationEntryPoint 를 custom 해준다.
@Component
class CustomAuthenticationEntryPoint(): AuthenticationEntryPoint {
override fun commence(
request: HttpServletRequest,
response: HttpServletResponse,
authException: AuthenticationException
) {
response.status = HttpServletResponse.SC_UNAUTHORIZED
response.contentType = MediaType.APPLICATION_JSON_VALUE
response.characterEncoding = "UTF-8"
val objectMapper = ObjectMapper()
val jsonString = objectMapper.writeValueAsString(ErrorResponse("JWT verification failed"))
response.writer.write(jsonString)
}
}
SecurityFilterChain 에 authenticationEntryPoint 로 등록
@Bean fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http
.httpBasic { it.disable() }
.formLogin { it.disable() }
.csrf { it.disable() }
.authorizeHttpRequests{
it.requestMatchers(
"/login","/signup","/swagger-ui/**", "/v3/api-docs/**"
).permitAll().anyRequest().authenticated()
}
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
.exceptionHandling{
it.authenticationEntryPoint(customAuthenticationEntryPoint)
}
.build()
}
}
🚩 인가
RBAC / ABAC
spring security role hierarchical
AOP 기반 인가
SecurityConfig 에 @EnableMethodSecuity 등록
Controller 에서 @PreAuthrize 를 통해 SpEL , SecurityExpressonRoot 의 methods와 properties 사용 가능
@PreAuthorize("#user.name == principal.name")
fun doSomething1(user: User): Unit { ... }
@PreAuthorize("hasRole('ADMIN') or hasRole('STUDENT')")
fun doSomething2(course: Course): Unit { ... }
우리가 인증정보를 Authentication 객체에 담을 때
data class UserPrincipal(
val id: Long,
val email: String,
val authorities: Collection<GrantedAuthority>
) {
constructor(id: Long, email: String, roles: Set<String>) : this(
id,
email,
roles.map { SimpleGrantedAuthority("ROLE_$it") }
)
}
역할을 GrandtedAuthority 객체로 앞에 "ROLE_" 을 붙여서 저장했다.
이 Authority는 역할과 권한 모두를 포함할 수 있는데
이 둘을 구분하기 위해 시큐리티에서는 앞에 ROLE_ 이 붙어있으면 이게 역할 Authority 라고 자동으로 판단해준다.
그러므로 담을 땐 ROLE_ 을 붙여서 담고 hasRole 로 확인을 할 때는 ROLE_ 을 떼고
🚩스프링 시큐리티 인가 403 예외처리
=> AccessDeniedHandler 를 custom 해준다.
@Component
class CustomAccessDeniedHandler: AccessDeniedHandler {
override fun handle(
request: HttpServletRequest,
response: HttpServletResponse,
accessDeniedException: AccessDeniedException
) {
response.status = HttpServletResponse.SC_FORBIDDEN
response.contentType = MediaType.APPLICATION_JSON_VALUE
response.characterEncoding = "UTF-8"
val objectMapper = ObjectMapper()
val jsonString = objectMapper.writeValueAsString(ErrorResponse("No permission to API"))
response.writer.write(jsonString)
}
}
accessDeniedHandler 등록
.exceptionHandling{
it.authenticationEntryPoint(customAuthenticationEntryPoint)
it.accessDeniedHandler(accessDeniedHandler)
}
인증을 안한 상태로 course 를 작성하려고 할 때 / 학생 토큰으로 작성하려고 할 때
🚩 QueryDSL
val user: User = queryFactory.selectFrom(user)
.where(user.email.eq("test@gmail.com"))
.fetchOne()
빌드
plugins {
...
kotlin("kapt") version "1.8.22" // 추가!
}
val queryDslVersion = "5.0.0" // 버전 명시 해야 함!!!
dependencies {
...
implementation("com.querydsl:querydsl-jpa:$queryDslVersion:jakarta") // 추가!
kapt("com.querydsl:querydsl-apt:$queryDslVersion:jakarta") // 추가!
...
}
Emtity 추가하거나 변경할 때 <complieKotlin> 을 통해 Qclass 업데이트
JpaQueryFactory 생성하기
abstract class QueryDslSupport {
@PersistenceContext
protected lateinit var entityManager: EntityManager
protected val queryFactory: JPAQueryFactory
get() {
return JPAQueryFactory(entityManager)
}
}
@PersistenceContext
entity manager 를 빈으로 주입할 때
각 엔티티querydslrepository
@Repository //jpa외부에서 만드는 거라 직접 등록해줘야 함
class QueryDslCourseRepository: QueryDslSupport() {
private val course = QCourse.course
fun searchCourseListByTitle(title: String): List<Course> {
return queryFactory.selectFrom(course).where(course.title.containsIgnoreCase(title)).fetch()
}
}
=> 서비스단에서 주입받아서 사용
기존 Repository(JpaRepository)와 QueryDslRepository 합치기
서비스단에서 주입받아서 사용하는건 CourseRepository
이 CourseRepository 가 상속받는 건 JpaRepository 와 CustomCourseRepository
CustomCourseRepository 의 구현체는 CourseRepositoryImpl
CourseRepositoryImpl 은 QueryDslSupprt 도 받아와서 여기서 동적쿼리를 만든다.
@Repository // JPA 외부에서 만드는거라 직접 등록해줘야 함
class CourseRepositoryImpl(): CustomCourseRepository, QueryDslSupport()
interface CourseRepository: JpaRepository<Course, Long>, CustomCourseRepository
interface CustomCourseRepository {fun searchCourseListByTitle(title: String): List<Course>}
=> 서비스단에서 CourseRepository 를 통해 CustomCourseRepository 를 구현한 CourseRepositoryImpl 의 메소드를 사용할 수 있다.
- 동적쿼리
여러 검색 조건을 받을 수도 있고 안 받을 수도 있을 때
Boolean builder 활용
fun serarchUser(email: String?, nickname: String?): List<User> {
val builder = new BooleanBuilder()
email?.let { builder.and(user.email.contains(it)) }
nickname?.let { builder.and(user.nickname.contains(it) }
return queryFactory.selectFrom(user).where(builder).fetch()
}
없다면 넘어가고 있다면 let 으로 builder 에 넣어줌
where 가변 인자 활용 (or 조건은 활용 불가)
fun serarchUser(email: String?, nickname: String?): List<User> {
return queryFactory
.selectFrom(user)
.where(
userEmailContains(email),
userNicknameContains(nickname)
)
.fetch()
}
private fun userEmailContains(email: String?) {
return email?.let { user.email.contains(it) }
}
private fun userNicknameContains(nickname: String?) {
return nickname?.let { user.nickname.contains(it) }
}
- QueryProjection
결과를 DTO로 받을 수 있다.
Projections bean / Projections constructor / @QueryProjection 활용
- 벌크 수정 / 삭제
// 벌크 수정
val updatedCount = queryFactory
.update(user)
.set(user.nickname, "TEST_NICKNAME")
.where(user.id.lt(10L))
.execute()
// 벌크 삭제
val updatedCount = queryFactory
.delete(user)
.where(user.id.lt(10L))
.execute()
이넘 클래스 컨트롤러에서 바로 활용
// companion object {
// @JvmStatic
// @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
// fun parse(name: String?): CourseStatus? =
// name?.let { EnumUtils.getEnumIgnoreCase(CourseStatus::class.java, it.trim())}
//
// }
🚩 QueryDSL활용
목록을 조회할 때 pagination 활용, 정렬 기준, 마감상태 필터링
Pageble 인터페이스
인자로 받으니까 자동으로 request 를 받는다.
구현체인 PageRequest / PageImpl 를 만들어줘도 된다.
PageImpl
override fun findByPageableAndStatus(pageable: Pageable, courseStatus: CourseStatus?): Page<Course> {
val whereClause = BooleanBuilder()
courseStatus?.let { whereClause.and(course.status.eq(courseStatus)) }
val totalCount = queryFactory.select(course.count()).from(course).where(whereClause).fetchOne() ?: 0L
val query = queryFactory.selectFrom(course)
.where(whereClause)
.offset(pageable.offset)
.limit(pageable.pageSize.toLong())
if(pageable.sort.isSorted) { // sort 기준이 들어와있으면
when(pageable.sort.first()?.property) { // 하나만 들어왔을 때
"id" -> query.orderBy(course.id.asc())
"title" -> query.orderBy(course.title.asc())
else -> query.orderBy(course.id.asc())
}
} else {
query.orderBy(course.id.asc())
}
val contents = query.fetch()
return PageImpl(contents, pageable, totalCount)
}
querydsl 정말 편한 건 알겠는데 어렵다 🥲🥲🥲
일단 todo 저번에 했던거 정리하고 ->시큐리티 추가 -> QueryDSL 강의 빠르게 한 번 더 보기
저번에 만든 todo 지금보니까 너무 이상하다
'왕초보일지' 카테고리의 다른 글
240119 TIL | (1) | 2024.01.19 |
---|---|
240118 TIL | (0) | 2024.01.18 |
240116 TIL | (0) | 2024.01.16 |
240113 TIL | (1) | 2024.01.13 |
240112 TIL | Co-Ha 내가 구현한 기능 공부, 기록 (1) | 2024.01.12 |