Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 인증 구현 #23

Merged
merged 20 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ out/

### Kotlin ###
.kotlin
/infra/infra-security/src/main/resources/application-security.yml
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ subprojects {
useJUnitPlatform()
}
}

tasks.register("prepareKotlinBuildScriptModel"){}
}
18 changes: 11 additions & 7 deletions data/src/main/kotlin/com/kw/data/domain/member/Member.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,14 @@ import jakarta.persistence.*
import java.time.LocalDateTime

@Entity
class Member(username: String, nickname: String, email: String) : Base() {
class Member(email: String) : Base() {
@Id
@Column(name = "id", nullable = false, updatable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null

@Column(name = "username", nullable = false)
val username: String = username

@Column(name = "nickname", nullable = false)
var nickname: String = nickname
protected set
@Column(name = "nickname")
var nickname: String? = null

@Column(name = "email", nullable = false, updatable = false)
val email: String = email
Expand All @@ -32,6 +29,13 @@ class Member(username: String, nickname: String, email: String) : Base() {
var deletedAt: LocalDateTime? = null
protected set

var memberRoles : MutableList<MemberRoleType> = mutableListOf(MemberRoleType.ROLE_USER)

enum class MemberRoleType {
ROLE_USER,
ROLE_ADMIN
}

enum class Provider {
GOOGLE,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.kw.data.domain.member.repository

import com.kw.data.domain.member.Member
import org.springframework.data.jpa.repository.JpaRepository

interface MemberRepository : JpaRepository<Member, Long> {
fun findMemberByEmail(email : String) : Member?
}
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version: '3'

services:
redis:
image: redis:latest
ports:
- "6379:6379"
13 changes: 13 additions & 0 deletions infra/infra-redis/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar

val jar: Jar by tasks
val bootJar: BootJar by tasks

// 실행가능한 jar로 생성하는 옵션, main이 없는 라이브러리에서는 false로 비활성화함
bootJar.enabled = false
// 외부에서 의존하기 위한 jar로 생성하는 옵션, main이 없는 라이브러리에서는 true로 비활성화함
jar.enabled = true

dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-redis")
}
Binary file not shown.
7 changes: 7 additions & 0 deletions infra/infra-redis/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.kw.infraredis.config

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
import org.springframework.data.redis.serializer.StringRedisSerializer


@Configuration
class RedisConfig(@Value("\${spring.data.redis.host}")
val host: String,

@Value("\${spring.data.redis.port}")
val port: Int) {

@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
return LettuceConnectionFactory(host, port)
}

@Bean
fun redisTemplate(connectionFactory: RedisConnectionFactory?): RedisTemplate<String, Any> {
val redisTemplate = RedisTemplate<String, Any>()
redisTemplate.keySerializer = StringRedisSerializer()
redisTemplate.valueSerializer = GenericJackson2JsonRedisSerializer()
redisTemplate.connectionFactory = connectionFactory
return redisTemplate
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.kw.infraredis.repository

import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.core.ValueOperations
import org.springframework.stereotype.Repository
import java.util.concurrent.TimeUnit


@Repository
class RedisRefreshTokenRepository(val redisTemplate: RedisTemplate<String, Any>) {
fun save(refreshToken: String, memberId: Long) {
val valueOperations: ValueOperations<String, Any> = redisTemplate.opsForValue()
valueOperations[refreshToken] = memberId
redisTemplate.expire(refreshToken, REFRESH_TOKEN_EXPIRE_LONG, TimeUnit.SECONDS)
}

fun delete(refreshToken: String) {
redisTemplate.delete(refreshToken)
}

fun findByRefreshToken(refreshToken: String?): Long? {
val valueOperations: ValueOperations<String, Long> = redisTemplate.opsForValue() as ValueOperations<String, Long>
return valueOperations[refreshToken!!]
}

companion object {
private const val REFRESH_TOKEN_EXPIRE_LONG = 259200L
}
}

5 changes: 5 additions & 0 deletions infra/infra-redis/src/main/resources/application-redis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
spring:
data:
redis:
port: 6379
host: localhost
28 changes: 28 additions & 0 deletions infra/infra-security/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar

val jar: Jar by tasks
val bootJar: BootJar by tasks

// 실행가능한 jar로 생성하는 옵션, main이 없는 라이브러리에서는 false로 비활성화함
bootJar.enabled = false
// 외부에서 의존하기 위한 jar로 생성하는 옵션, main이 없는 라이브러리에서는 true로 비활성화함
jar.enabled = true

dependencies {
implementation(project(":data"))
implementation(project(":infra:infra-redis"))

implementation("org.springframework.boot:spring-boot-starter-web")

implementation("org.springframework.boot:spring-boot-starter-data-jpa")

// security
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
implementation("io.jsonwebtoken:jjwt-impl:0.12.3")
implementation("io.jsonwebtoken:jjwt-jackson:0.12.3")

// redis
implementation("org.springframework.boot:spring-boot-starter-data-redis")
}
Binary file not shown.
7 changes: 7 additions & 0 deletions infra/infra-security/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.kw.infrasecurity.config

import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories


@Configuration
@EnableRedisRepositories(basePackages = ["com.kw"])
@ComponentScan(basePackages = ["com.kw"])
class SecurityModuleConfig


Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.kw.infrasecurity.config

import com.kw.infrasecurity.jwt.JwtAccessDeniedHandler
import com.kw.infrasecurity.jwt.JwtAuthenticationEntryPoint
import com.kw.infrasecurity.jwt.JwtAuthenticationFilter
import com.kw.infrasecurity.oauth.OAuth2SuccessHandler
import com.kw.infrasecurity.oauth.OAuth2UserService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.authentication.logout.LogoutFilter

@Configuration
@EnableWebSecurity
class WebSecurityConfig(val jwtAuthenticationFilter: JwtAuthenticationFilter,
val jwtAccessDeniedHandler: JwtAccessDeniedHandler,
val jwtAuthenticationEntryPoint: JwtAuthenticationEntryPoint,
val oAuth2SuccessHandler: OAuth2SuccessHandler,
val oAuth2UserService: OAuth2UserService) {

@Bean
fun filterChain(http : HttpSecurity) : SecurityFilterChain {
http.invoke {
csrf { disable() }
anonymous { disable() }
formLogin { disable() }
httpBasic { disable() }
logout { disable() }
sessionManagement { SessionCreationPolicy.STATELESS }

addFilterAfter<LogoutFilter>(jwtAuthenticationFilter)
exceptionHandling {
authenticationEntryPoint = jwtAuthenticationEntryPoint
accessDeniedHandler = jwtAccessDeniedHandler
}

oauth2Login {
authenticationSuccessHandler = oAuth2SuccessHandler
userInfoEndpoint {
userService = oAuth2UserService
}
}

authorizeHttpRequests {
authorize(anyRequest, permitAll)
}
}

return http.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.kw.infrasecurity.jwt

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.web.access.AccessDeniedHandler
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerExceptionResolver

@Component
class JwtAccessDeniedHandler() : AccessDeniedHandler {

private lateinit var resolver: HandlerExceptionResolver

constructor (@Qualifier("handlerExceptionResolver") resolver: HandlerExceptionResolver) : this() {
this.resolver = resolver
}

override fun handle(
request: HttpServletRequest?,
response: HttpServletResponse?,
accessDeniedException: AccessDeniedException?
) {
resolver.resolveException(request!!, response!!, null, accessDeniedException!!)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.kw.infrasecurity.jwt

import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.security.core.AuthenticationException
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.stereotype.Component
import org.springframework.web.servlet.HandlerExceptionResolver

@Component
class JwtAuthenticationEntryPoint() : AuthenticationEntryPoint {

private lateinit var resolver: HandlerExceptionResolver

constructor (@Qualifier("handlerExceptionResolver") resolver: HandlerExceptionResolver) : this() {
this.resolver = resolver
}

override fun commence(
request: HttpServletRequest?,
response: HttpServletResponse?,
authException: AuthenticationException?
) {
resolver.resolveException(
request!!, response!!, null,
(request.getAttribute("exception") as Exception)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.kw.infrasecurity.jwt

import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.access.AccessDeniedException
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import org.springframework.util.StringUtils
import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtAuthenticationFilter(private val tokenProvider: JwtTokenProvider) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
if (SecurityContextHolder.getContext().authentication == null) {
val accessToken = resolveToken(request)
if(accessToken != null) {
try {
tokenProvider.validateAccessToken(accessToken)
val authentication = tokenProvider.getAuthentication(accessToken)
SecurityContextHolder.getContext().authentication = authentication
} catch (e : Exception) {
request.setAttribute("exception", e)
}
}
}
else {
request.setAttribute("exception", AccessDeniedException("요청이 거부되었습니다"))
}
filterChain.doFilter(request, response)
}

private fun resolveToken(request: HttpServletRequest) : String? {
val authorizationHeader = request.getHeader("Authorization")
return if (StringUtils.hasText(authorizationHeader) && authorizationHeader.startsWith("Bearer")) {
authorizationHeader.substring(7)
} else null
}
}
Loading
Loading