팀프로젝트<뉴스피드 만들기>(3일차)

2024. 5. 29. 23:11TIL

🕕

팀 프로젝트 (뉴스피드 만들기)

💡 프로젝트 기간:  2024-05-27~2024-06-03

 

 

프로젝트 진행과정

 

튜터 피드백 수용 

확장성 위해 삭제시간 별도로 저장

제약조건을 @Valid 어노테이션 사용하지 않고 내부 로직으로 해결하기

검색 tag는 하나만 가능하게 제약 걸기

비밀번호 저장할때 암호화하기

로그인 부분 인증 구현하기

 

 

인증 부분 구현하기

 

@RestController
@RequestMapping("/auth")
class AuthenticationController(
    private val authService: AuthService // 컨트롤러에서 서비스 연결
) {

    @PostMapping("/register")
    fun register(@RequestBody signupRequest: SignUpRequest): ResponseEntity.BodyBuilder {
        authService.register(signupRequest)
        return ResponseEntity.status(HttpStatus.CREATED)
    }

    @PostMapping("/login")
    fun login(@RequestBody loginRequest: LoginRequest): ResponseEntity<String> {
        val token = authService.login(loginRequest)
        //authService에서 인증이 성공하면 토큰이 반환되므로 token 변수에 token정보가 담기고 인증이 실패하면 token 변수에 null이 담김
        return if (token != null) {
            ResponseEntity.ok(token) //이 부분에서 인증이 된 상태, 컨트롤러를 따로 만드는게 아닌 기존의 memberController에 추가하기?
        } else {
            ResponseEntity.status(401).body("인증 실패로 해당 토큰이 존재하지 않습니다")
        }
    }
}

 

 

@Service
class AuthService(
    private val authenticationManager: AuthenticationManager, // spring security에서 제공하는 인터페이스(인증을 관리하고 수행)
    private val jwtTokenProvider: JwtTokenProvider, // JWT토큰 만드는 클래스 주입
    private val passwordEncoder: PasswordEncoder, // 비밀번호 암호화
    private val memberSecurityRepository: MemberSecurityRepository, //리포지토리 연결
    private val memberRepository: MemberRepository
) {

    //이메일이랑 비밀번호 받아서 로그인 하는 함수
    fun login(loginRequest: LoginRequest): String? {
        val authentication: Authentication = authenticationManager.authenticate(//데이터베이스와의 비교는 Spring Security가 내부적으로 처리하며 그 설정은 SecurityConfig안에 이
            UsernamePasswordAuthenticationToken(loginRequest.email, loginRequest.password)
        ) //authenticationManager.authenticate를 이용해서 사용자 인증을 시도하고 매개변수로 UsernamePasswordAuthenticationToken 객체를 생성해서 이메일이랑 비밀번호를 전달함
// 즉 authetication이라는 변수에 사용자의 인증 정보가 담겨져 있는 상태

        // if(loginRequest.password==) 식으로 데이터베이스의 이메일 비밀번호와 일치하는지를 통한 것이 아닌 authenticationManager.authenticate()함수를 이용해서 인증을 함
        if (authentication.isAuthenticated) { // 성공적으로 인증되었으면 true
            return jwtTokenProvider.generateToken(loginRequest.email) // 성공적으로 인증되었다면 이메일을 기반으로 JWT토큰을 생성해서 반환함
        }
        return null //인증 실패했으면 null반환
    }


    fun register(signUpRequest: SignUpRequest){ //회원가입할때 사용자를 등록하는 함수
        val memberSecurity = MemberSecurity(email = signUpRequest.email, password = passwordEncoder.encode(signUpRequest.password)) // membersecurity dto에서 입력받은 email이랑 암호화된 비밀번호를 해당 변수에 저장
        memberSecurityRepository.save(memberSecurity) //데이터베이스에 암호화된 비밀번호랑 이메일을 저장함
    }


}

 

 

 

이 클래스는 UserDetailService를 상속받기 때문에 요녀석이 내부적으로 데이터베이스에서 사용자 정보를 가져오는 서비스를 담당한다

@Service //@service어노테이션이 있지만 컨트롤러에서 사용되는게 아니라 Spring Security에서 사용자 인증과 권한 부여 과정에서 내부적으로 사용된다
class GetUserDetailsService(
    private val memberSecurityRepository: MemberSecurityRepository
) : UserDetailsService { // UserDetailsService 요녀석이 데이터베이스에서 사용자 정보를 가져오는 서비스

    override fun loadUserByUsername(email: String): UserDetails {
        val user = memberSecurityRepository.findByEmail(email)
            ?: throw UsernameNotFoundException("해당 이메일이 존재하지 않음")
        return User(
            user.email,
            user.password,
            emptyList()
        )
    }
}

 

 

@configureGlobal함수의 userDetailService를 통해서 데이터베이스에 있는 정보를 가져오고 

@Configuration
@EnableWebSecurity
class SecurityConfig(
    private val jwtTokenProvider: JwtTokenProvider,
    private val userDetailsService: GetUserDetailsService,
    private val passwordEncoder: PasswordEncoder
) {

    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http.csrf { it.disable() }
            .sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
            .authorizeHttpRequests {
                it.requestMatchers("/auth/**").permitAll()
                    .anyRequest().authenticated()
            }
            .addFilterBefore(JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter::class.java)
        return http.build()
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder()
    }

    @Bean
    fun authenticationManager(authenticationConfiguration: AuthenticationConfiguration): AuthenticationManager {
        return authenticationConfiguration.authenticationManager
    }

    @Autowired
    fun configureGlobal(auth: AuthenticationManagerBuilder) { //AuthenticationManagerBuilder는 사용자 인증 정보를 담고있는 빌더 클래스
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder) //userDetailsService는 데이터베이스에 있는 정보를 가져옴, .passwordEncoder(passwordEncoder) 요 부분이 사용자가 입력한 비밀번호랑 데이터베이스에 있는 비밀번호랑 맞는지의 로직
    } //요 userDetailService를 구현하고 있는것이 GetUserDetailsService임
    //요 함수는 AuthenticationManagerBuilder를 사용해서 사용자 인증 정보를 전역적으로 구성하는 함수다
}

 

 

토큰 생성

@Component
class JwtTokenProvider(
    //yml파일에 있는 정보 가져다 쓰기
    @Value("\${jwt.secret}") private val secretKey: String,
    @Value("\${jwt.validity}") private val validityInMilliseconds: Long,
    private val userDetailsService: UserDetailsService // UserDetailService는 Spring Security에서 제공하는 인터페이스(사용자의 상세 정보를 로드하는데 사용됨)
) {

    //토큰을 생성하는 함수
    fun generateToken(email: String): String {
        val now = Date()
        val validity = Date(now.time + validityInMilliseconds)

        return Jwts.builder() //구성요소 설정
            .setSubject(email) // 이메일이 토큰의 식별자로서 사용
            .setIssuedAt(now) // 토큰 발행 시간(현재시간)
            .setExpiration(validity) // 토큰 만료시간 현재 1시간으로 뒤로 설정되어있음
            .signWith(SignatureAlgorithm.HS256, secretKey) // secretKey를 이용해서 토큰에 서명을 해서 무결성을 보장, HS256은 대칭 키 알고리즘
            .compact() // 최종적으로 이 구성들로 토큰을 생성하고 문자열 형태로 반환함
    }


    // 이 토큰이 유효한지 검증하는 함수
    fun validateToken(token: String): Boolean {
        return try {
            Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token) //시크릿 키로 토큰의 서명을 검증하고 토큰을 파싱해서 해당 토큰의 클레임(정보)를 추출함 ,파싱은 토큰에 포함된 정보를 열어보는것
            true // 예외가 발생하지 않으면 이 토큰은 유효한 토큰이므로 true 반환
        } catch (e: Exception) {
            false // 예외발생했으므로 이 토큰은 유효하지 않은 토큰이니 false 반환
        }
    }

    //이 토큰에서 이메일을 추출하는 함수 **요 함수는 AuthService의  login함수에 이미 정의되있으므로 중복된 기능이다  삭제?**
    fun getEmailFromToken(token: String): String {
        val claims: Claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).body // 토큰을 파싱하고(열고) 본문인 body를 가져옴
        return claims.subject // 그 body중에 email이 들어있는 subject를 반환함
    }
}

 

// 클라이언트가 서버로 토큰을 보낼때 작동하는 클래스
// Spring Security 에 필요한 구성요소 이기때문에 별도의 서비스에 등록하지 않고 독립적으로 사용
//UsernamePasswordAuthenticationFilter는 Spring Security가 제공하는 인증 필터 중 하나인데 이를 상속한 상태
@Component
class JwtTokenFilter(private val jwtTokenProvider: JwtTokenProvider) : UsernamePasswordAuthenticationFilter() {


    // 사용자가 토큰과 함께 요청을 보내면은 그 요청에서 토큰을 꺼내는 함수, JWT는 Authorization 이라는 헤더에 토큰 정보가 담겨져있다
    private fun resolveToken(request: HttpServletRequest): String? {
        val bearerToken = request.getHeader("Authorization") //HTTP 요청의 헤더에서 Authorization 값을 가져와서 bearerToken에다가 저장함
        return if (bearerToken != null && bearerToken.startsWith("Bearer ")) { // Bearer로 시작하면은 Bearer를 제거하고 순수 토큰 문자열을 반환
            bearerToken.substring(7)
        } else null
    }
}

 

이로써 전체적인 인증에 대한 부분의 골격은 갖추어진 상태이다.

다음날은 각각의 기능을 연결해보고 테스트 해봐야 한다