TIL

24_02_06 TIL

nakgopsae 2024. 2. 6. 23:12

- 오늘의 계획 - 

 

주특기 플러스 주차 todoList 요구사항대로 완성하기

 

- 오늘 한 것 - 

 

Spring security + jwt 강의를 보고 머리속으로 정리를 하여 적용 시켜보았다 

 

Spring Security 적용을 위해 gradle에 라이브러리 등등을 추가

implementation("org.springframework.boot:spring-boot-starter-security")
implementation("io.jsonwebtoken:jjwt-api:0.12.3")

runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.3")

 

아직 잘 모르니 일단은 강의대로 따라 해보았고 현 상태로 swagger를 실행 시키면 login 화면이 뜬다

 

spring security는 기본적으로 제공되는 filter들이 여러가지 있고 이것들이 filterChain으로 적용된다

 

인증을 위한 필터를 적용하기 위해서는 customFilter를 적용할껀데 순서는 아직 다 외우지 않았으니 시키는대로 필터 작성을 해본다 

@Configuration
@EnableWebSecurity//http 기반으로 통신을 할 때 관련 보안기능을 설정 을 할때 다는 어노테이션
@EnableMethodSecurity
class SecurityConfig(
    private val jwtAuthenticationFilter: JwtAuthenticationFilter,
    private val authenticationEntryPoint: AuthenticationEntryPoint,
    private val accessDeniedHandler:CustomAccessDeniedHandler,
) {
    @Bean
    fun filterChain(http:HttpSecurity):SecurityFilterChain{
        return http
            .httpBasic{it.disable()}
            .formLogin{it.disable()}
            .csrf{it.disable()}
            .authorizeHttpRequests{
                it.requestMatchers(
                    "/users/Login",
                    "/users/signUp",
                    "/swagger-ui/**",
                    "/v3/api-docs/**",
                    //위에 경로를 인증에서 제외한다
                ).permitAll()
                 .anyRequest().authenticated()
            }
            .addFilterBefore(jwtAuthenticationFilter,UsernamePasswordAuthenticationFilter::class.java)
            .exceptionHandling{
                it.authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler)
            }// 테스트할때 주석 쳐보고 할 것
            .build()
    }
}

 

이렇게 하면 앞에서 login화면 등이 꺼지고 다시 정상적으로 sawgger가 원래대로 작동한다

 

jwt 토큰 발급을 위한 코드를 작성한다

@Component
class JwtPlugin {
    companion object{
        const val SECRET = "PO4c8z41Hia5gJG3oeuFJMRYBB4Ws4aZ"
        const val ISSUER = "team.sparta.com"
        const val ACCESS_TOKEN_EXPIRATION_HOUR:Long = 168
    }//yml에 빼주면 되는데 지금 작성 안되있어서 패쓰
    fun validateToken(jwt:String): Result<Jws<Claims>>{
        return kotlin.runCatching {
            val key = Keys.hmacShaKeyFor(SECRET.toByteArray(StandardCharsets.UTF_8))

            Jwts.parser().verifyWith(key).build().parseSignedClaims(jwt)
        }
    }

    fun generateAccessToken(subject:String,email:String,role:String):String{
        return generateToken(subject,email,role,Duration.ofHours(ACCESS_TOKEN_EXPIRATION_HOUR))
    }

    private fun generateToken(subject:String,email:String,role:String,expirationPeriod:Duration):String{

        val claims:Claims = Jwts.claims()
            .add(mapOf("role" to role,"email" to email))
            .build()

        val key = Keys.hmacShaKeyFor(SECRET.toByteArray(StandardCharsets.UTF_8))
        val now = Instant.now()


        return Jwts.builder()
            .subject(subject)
            .issuer(ISSUER)
            .issuedAt(Date.from(now))
            .expiration(Date.from(now.plus(expirationPeriod)))
            .claims(claims)
            .signWith(key)
            .compact()
    }
}

 

 

비밀번호 암호화

@Configuration
class PasswordEncoderConfig {
    @Bean
    fun passwordEncoder():PasswordEncoder{
        return BCryptPasswordEncoder()
    }
}
// 
//service
@Transactional
    override fun signUp(signUpDto: SignUpDto): UserResponseDto {
        if (userRepository.existsByEmail(signUpDto.email)) throw IllegalStateException("가입된 이메일")
        //return UserEntity.toUserResponse(userRepository.save(UserEntity.toUserEntity(signUpDto,passwordEncoder)))
        val role = when(signUpDto.role){
            "ADMIN" -> UserRole.ADMIN
            "USER" -> UserRole.USER
            else -> throw IllegalArgumentException("Invalid role")
        }
        return userRepository.save(
            UserEntity(
                name = signUpDto.name,
                email = signUpDto.email,
                password = passwordEncoder.encode(signUpDto.password),
                role = role
            )
        ).toUserResponse()
    }

 

위에 encoder 파일을 생성해주고 회원가입 서비스에 encoder를 주입한 다음 저장할때 사용해주면 암호화되서 db에 저장이 된다

 

로그인 

requestDto 에서 이름 이메일 역할을 받고 서비스에서 받은 값과 데이터베이스에 저장된 값을 비교해서 토큰을 리턴 해주면 된다 

override fun login(loginDto: LoginDto): LoginResponseDto {
    val userFind = userRepository.findByEmail(loginDto.email) ?: throw IllegalStateException("사용자를 찾을 수 없습니다")

    if (userFind.role.name == loginDto.role && passwordEncoder.matches(loginDto.password, userFind.password)) {
        return LoginResponseDto(
            accessToken = jwtPlugin.generateAccessToken(
                subject = userFind.userId.toString(),
                email = userFind.email,
                role = userFind.role.name
            )
        )
    } else {
        throw InvalidCredentialException("닉네임 또는 패스워드를 확인해주세요")
    }
 }

 

여기서 발급 받은 토큰을 authorize를 통해 로그인 할수 있다

 

 JWT 인증 구현

jwt를 요청 header에서 추출해서 사용한다 - > 검증에 성공할시 Authentication객체에 인증 됐다는걸 표기 하고 SecurityContext에 저장한다 

 

 

JwtAuthenticationFilter 생

@Component
class JwtAuthenticationFilter(
    private val jwtPlugin: JwtPlugin
):OncePerRequestFilter() {

    companion object {
        private val BEARER_PATTERN = Regex("^Bearer (.+?)$")
    }

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val jwt = request.getBearerToken()

        if(jwt != null) {
            jwtPlugin.validateToken(jwt)
                .onSuccess {
                    val userId = it.payload.subject.toLong()
                    val role = it.payload.get("role", String::class.java)
                    val email = it.payload.get("email", String::class.java)
                    val principal = UserPrincipal(
                        id = userId,
                        email = email,
                        roles = setOf(role)
                    )
                    val authentication = JwtAuthenticationToken(
                        principal = principal,
                        details = WebAuthenticationDetailsSource().buildDetails(request)

                    )
                    SecurityContextHolder.getContext().authentication =authentication
                }

        }
        filterChain.doFilter(request,response)

    }
    private fun HttpServletRequest.getBearerToken(): String? {
        val headerValue = this.getHeader(HttpHeaders.AUTHORIZATION) ?: return null
        return BEARER_PATTERN.find(headerValue)?.groupValues?.get(1)
    }
}

 

jwt정보를 추출해서 정보를 저장한다 

 

UserPrincipal 객체 

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") }
    )

    //부생성자
}

 

유저정보를 담는 객체

 

JwtAuthenticationToken 클래스 생성

class JwtAuthenticationToken(
    private val principal: UserPrincipal,
    details:WebAuthenticationDetails
):AbstractAuthenticationToken(principal.authorities) {

    init {
        super.setAuthenticated(true)
        super.setDetails(details)
    }
    //초기화 -

    override fun getCredentials() = null

    override fun getPrincipal() = principal
    override fun isAuthenticated(): Boolean {
        return true
    }
}

 

 

Authentication 객체를 생성 

인증을 구현하고 객체를 생성해서 context에 넣어주고 filterChain에 적용한다 

 

일단은 이렇게 이해하기로 했고 todoList에 적용 시켜서 동작이 잘 되는거 까지 확인했다 

 

프로젝트 주간에 팀원들이 설정 해놓은걸 그냥 어노테이션만 넣어서 사용했는데 생각보다 어려웠고 자세한 부분은 스킵을 많이 했기 때문에 조금더 깊게 파야할거같다

 

'TIL' 카테고리의 다른 글

24_02_13 TIL  (0) 2024.02.13
24_02_08 TIL  (0) 2024.02.08
24_01_29 TIL  (1) 2024.01.29
24_01_26 TIL  (1) 2024.01.26
24_01_23 TIL  (0) 2024.01.23