hyeonga_code

Project_03_Spring Security, JWT 구현 본문

Project_HYEONGARL

Project_03_Spring Security, JWT 구현

hyeonga 2024. 5. 30. 06:59
반응형

 

 

Project_04_Spring Web Layer

Spring Web Layer1. Web Layer뷰 템플릿 영역입니다.( @Controller, JSP, Freemarker ) 외부 요청과 응답에 대한 전반적인 영역입니다.( @Filter, 인터셉터, @ControllerAdvice ) 2. Service Layer@Service에 사용되는 서비스 영

hyeonga493.tistory.com

 

요약
User 테이블 설정(Entity, Dto, Repository)
Service, Controller 구현
SpringSecurity + JWT 구현
TokenProviderTest
AddUser() / UserController
Login() / AuthController


1. api 모듈에 프로젝트 패키지 생성하기

현재 사용할 패키지들을 먼저 생성한다. (controller, dto, entity, error, repository, service)

 

 

2. User Entity

package com.hyeongarl.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

/*
    @Table(name="")
        데이터베이스의 name에 지정된 이름의 테이블과 매핑된다.
    @Entity
        JPA 엔티티임을 명시하여 데이터베이스 테이블의 레코드와 매핑
    @Getter, @Setter
        롬복 라이브러리의 어노테이션으로 모든 필드에 대해 자동으로 getter/setter를 생성
    @NoArgsConstructor
        롬복 라이브러리의 어노테이션으로 매개변수가 없는 기본 생성자를 자동으로 생성
 */
@Table(name="users")
@Entity
@Getter
@Setter
@NoArgsConstructor
public class User {
    /*
        @Id
            테이블의 기본 키
        @GeneratedValue(strategy = GenerationType.IDENTITY)
            기본 피의 생성 전략을 지정
            IDENTITY : 데이터베이스가 기본 키 생성을 담당
            기본적으로 AUTO_INCREMENT를 사용
        @Column(name="user_id", updatable = false)
            데이터베이스의 name과 일치하는 컬럼과 매핑된다.
            필드가 수정될 수 없음을 의미
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="user_id", updatable = false)
    private Long userId;

    /*
        @Email
            유효한 이메일 형식을 규정
        @Column(name="user_email", nullable = false, unique = true)
            데이터베이스의 name과 일치하는 컬럼과 매핑된다.
            null 값을 가질 수 없음을 의미
            유일한 값을 가져야 함을 의미
     */
    @Email
    @Column(name="user_email", nullable = false, unique = true)
    private String userEmail;

    @Column(name = "user_password", nullable = false, length=25)
    private String password;

    @Column(name="user_regdate", updatable = false)
    private LocalDateTime userRegdate;

    @Builder
    public User(String userEmail, String password) {
        this.userEmail = userEmail;
        this.password = password;
        this.userRegdate = LocalDateTime.now();
    } //end User()
}

 

서버 실행 시 테이블 생성 쿼리가 실행되는 것을 확인할 수 있다.

 

 

 

3. UserRepository  /interface

package com.hyeongarl.repository;

import com.hyeongarl.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

 

 

4. UserService

package com.hyeongarl.service;

import com.hyeongarl.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

/*
    @Service
        Spring Framework에 의해 관리되는 서비스 클래스임을 명시
    @RequiredArgsConstructor
        롬복 라이브러리에서 final 필드에 대한 생성자를 자동으로 생성
 */
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
}

 

@RequiredArgsConstructor 의 역할

public UserService(UserRepository userRepository) {
	this.userRepository = userRepository;
}

 

 

5. DTO (UserRequestDto/UserResponseDto)

엔티티는 데이터베이스에 값을 저장하고 불러오는 역할이고 DTO는 클라이언트에서 서버로 데이터를 가지고 이동하는 역할이다.

이전 프로젝트에서는 dto를 하나만 작성해서 작업했었는데 클라이언트에게 입력받는 값과 클라이언트에게 반환하는 값이 다를 수 있어 request/response로 나누어 작업한다.

 

클라이언트에게 입력을 받고 반환하는 값을 데이터베이스에서 사용하기 위해 엔티티로 변환해야 하는 코드를 각 dto에 작업하여 dto 내에서 수정사항이 있는 경우 바로 수정할 수 있도록 작업했다.

// RequestDto
package com.hyeongarl.dto;

import com.hyeongarl.entity.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class UserRequestDto {
    private String userEmail;
    private String password;

    public User toEntity() {
        return User.builder()
                .userEmail(userEmail)
                .password(password)
                .build();
    } //end toEntity()
} //end class


// ResponseDto
package com.hyeongarl.dto;

import com.hyeongarl.entity.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserResponseDto {
    private String userEmail;
    private String password;
    private LocalDateTime userRegdate;

    public static UserResponseDto fromEntity(User user) {
        return UserResponseDto.builder()
                .userEmail(user.getUserEmail())
                .password(user.getPassword())
                .userRegdate(user.getUserRegdate())
                .build();
    } //end fromEntity()
} //end class

 

 

 

6. UserController

package com.hyeongarl.controller;

import com.hyeongarl.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {
    private final UserService   userService;
}

 

현재는 토큰 기반 인증 서비스를 구현하지 않았다.

 

SpringSecurity + JWT 적용하기

 

1. build.gradle 의존성 추가

> 기존 코드

더보기
plugins {	// Gradle에서 지원하는 플러그인
	id 'java'
	id 'org.springframework.boot' version '3.2.5'
	id 'io.spring.dependency-management' version '1.1.5'
	id "io.freefair.lombok" version "8.6"

	/*
        1) id 'java'
            java 코드 컴파일, 테스트, 애플리케이션 패키징
        2) id 'org.springframework.boot' version '3.2.5'
            SpringBoot 애플리케이션을 실행, 실행 가능한 JAR 또는 WAR패키징
        3) id 'io.spring.dependency-management' version '1.1.5'
            일관된 방식으로 종속성을 관리하는 데 사용
            섹션을 사용하여 프로젝트의 모든 종속성에 적용해야 하는 종속성 버전을 정의하여 충돌을 방지
        4) id "io.freefair.lombok" version "8.6"
            롬복 사용 (gradle에서 관리하므로 플러그인으로 설정 가능)
    */
}

// 프로젝트 그룹 ID 설정 (패키지 이름 지정 및 종속성 관리에 사용)
group = 'com.heyongarl'

// 프로젝트의 버전을 설정
version = '1.0-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
	/*
        toolchain VS sourceCompatibility
        toolchain
            Gradle 자체를 실행하는 데 사용되는 JDK와 별개로
            프로젝트를 컴파일, 테스트, 실행하는 데 사용할 JDK 버전을 지정할 수 있다.
        sourceCompatibility
            소스 코드의 Java 버전 호환성을 명시하는 데 사용
    */
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-web-services'
	implementation 'org.springframework.boot:spring-boot-starter-validation'

	// JPA
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

	// MySQL JPA
	runtimeOnly 'com.mysql:mysql-connector-j'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	/*
        1) implementation 'org.springframework.boot:spring-boot-starter-web'
            Spring MVC를 사용하여 웹 애플리케이션을 빌드하기 위한 종속성
        2) implementation 'org.springframework.boot:spring-boot-starter-web-services'
            웹 서비스 제작을 지원
        3) implementation 'org.springframework.boot:spring-boot-starter-validation'
            Java Bean의 유효성을 검사하기 위한 종속성 (@NotNull, @Size)
        4) implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
            Spring Data JPA를 포함한 JPA 기반 데이터 엑세스에 대한 지원을 제공
        5) runtimeOnly 'com.mysql:mysql-connector-j'
            MySQL 데이터베이스에 연결하는 데 필요한 MySQL JDBC 드라이버
        6) testImplementation 'org.springframework.boot:spring-boot-starter-test'
            JUnit, Jamcrest, Mockito를 포함한 Spring Boot 애플리케이션 테스트를 위한 종속성
    */
}

tasks.named('test') {
	useJUnitPlatform()
}

 

Spring Security 관련 라이브러리와 JWT 관련 라이브러리를 추가한다.

dependencies {	
    // Spring Security
	implementation 'org.springframework.boot:spring-boot-starter-security:2.7.17'
	testImplementation 'org.springframework.security:spring-security-test:5.7.11'

	// JWT Token
	implementation 'io.jsonwebtoken:jjwt:0.9.1'
    
	/*
        7) implementation 'org.springframework.boot:spring-boot-starter-security:2.7.17'
        	보안과 관련된 기능을 제공하는 스프링 보안 모듈에 대한 스타터 의존성
        	인증(Authentication)과 권한 부여(Authorization)을 위한 기능을 제공
        	웹 애플리케이션의 보안을 강화하는데 사용
        8) testImplementation 'org.springframework.security:spring-security-test:5.7.11'
        	Spring Security 테스트에 필요한 라이브러리를 포함하는 의존성
        	인증 및 권한 부여를 테스트하거나 보안 관련 기능을 검증할 수 있음
        9) implementation 'io.jsonwebtoken:jjwt:0.9.1'
        	JWT를 생성하고 처리하기 위한 라이브러리
    */
}

 

> 전체 코드

더보기
plugins {	// Gradle에서 지원하는 플러그인
	id 'java'
	id 'org.springframework.boot' version '3.2.5'
	id 'io.spring.dependency-management' version '1.1.5'
	id "io.freefair.lombok" version "8.6"

	/*
        1) id 'java'
            java 코드 컴파일, 테스트, 애플리케이션 패키징
        2) id 'org.springframework.boot' version '3.2.5'
            SpringBoot 애플리케이션을 실행, 실행 가능한 JAR 또는 WAR패키징
        3) id 'io.spring.dependency-management' version '1.1.5'
            일관된 방식으로 종속성을 관리하는 데 사용
            섹션을 사용하여 프로젝트의 모든 종속성에 적용해야 하는 종속성 버전을 정의하여 충돌을 방지
        4) id "io.freefair.lombok" version "8.6"
            롬복 사용 (gradle에서 관리하므로 플러그인으로 설정 가능)
    */
}

// 프로젝트 그룹 ID 설정 (패키지 이름 지정 및 종속성 관리에 사용)
group = 'com.heyongarl'

// 프로젝트의 버전을 설정
version = '1.0-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
	/*
        toolchain VS sourceCompatibility
        toolchain
            Gradle 자체를 실행하는 데 사용되는 JDK와 별개로
            프로젝트를 컴파일, 테스트, 실행하는 데 사용할 JDK 버전을 지정할 수 있다.
        sourceCompatibility
            소스 코드의 Java 버전 호환성을 명시하는 데 사용
    */
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-web-services'
	implementation 'org.springframework.boot:spring-boot-starter-validation'

	// JPA
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

	// MySQL JPA
	runtimeOnly 'com.mysql:mysql-connector-j'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// Spring Security
	implementation 'org.springframework.boot:spring-boot-starter-security:2.7.17'
	testImplementation 'org.springframework.security:spring-security-test:5.7.11'

	// JWT Token
	implementation 'io.jsonwebtoken:jjwt:0.9.1'

	/*
        1) implementation 'org.springframework.boot:spring-boot-starter-web'
            Spring MVC를 사용하여 웹 애플리케이션을 빌드하기 위한 종속성
        2) implementation 'org.springframework.boot:spring-boot-starter-web-services'
            웹 서비스 제작을 지원
        3) implementation 'org.springframework.boot:spring-boot-starter-validation'
            Java Bean의 유효성을 검사하기 위한 종속성 (@NotNull, @Size)
        4) implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
            Spring Data JPA를 포함한 JPA 기반 데이터 엑세스에 대한 지원을 제공
        5) runtimeOnly 'com.mysql:mysql-connector-j'
            MySQL 데이터베이스에 연결하는 데 필요한 MySQL JDBC 드라이버
        6) testImplementation 'org.springframework.boot:spring-boot-starter-test'
            JUnit, Jamcrest, Mockito를 포함한 Spring Boot 애플리케이션 테스트를 위한 종속성
        7) implementation 'org.springframework.boot:spring-boot-starter-security:2.7.17'
        	보안과 관련된 기능을 제공하는 스프링 보안 모듈에 대한 스타터 의존성
        	인증(Authentication)과 권한 부여(Authorization)을 위한 기능을 제공
        	웹 애플리케이션의 보안을 강화하는데 사용
        8) testImplementation 'org.springframework.security:spring-security-test:5.7.11'
        	Spring Security 테스트에 필요한 라이브러리를 포함하는 의존성
        	인증 및 권한 부여를 테스트하거나 보안 관련 기능을 검증할 수 있음
        9) implementation 'io.jsonwebtoken:jjwt:0.9.1'
        	JWT를 생성하고 처리하기 위한 라이브러리
    */
}

tasks.named('test') {
	useJUnitPlatform()
}

 

2. 다시 빌드하기 

 

 

3. JWT 관련 파일을 생성할 패키지를 생성

src / main  / java / com / hyeongarl / config / jwt

 

 

4. application.properties 

Jwt 발급자 정보와 시크릿 키를 설정

# JwtProperties
jwt.issuer=hyeongarl_issuer
jwt.secretKey=my_jwt_secretKey_202478451236978512



5. TokenProvider

package com.hyeongarl.config.jwt;

import com.hyeongarl.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Date;
import java.util.Set;

/**
 * JWT 토큰 생성
 * 유효성 검사
 * 인증 정보 추출 및 사용자 ID 추출
 */
@RequiredArgsConstructor
@Service
public class TokenProvider {
    private final JwtProperties jwtProperties;

    public String generatedToken(User user, Duration expiredAt) {
        Date now = new Date();
        return Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)   // 헤더 typ: JWT
                .setIssuer(jwtProperties.getIssuer())           // properties에서 설정한 issuer
                .setIssuedAt(now)                               // iat : 현재 시간
                .setExpiration(new Date(now.getTime() + expiredAt.toMillis())) // exp : expiry 값
                .setSubject(user.getUserEmail())                // sub : 사용자 이메일
                .claim("id", user.getUserId())                  // 클레임 id : 사용자 id
                // 서명 : 비밀값, 해시값을 HS256방식으로 암호화
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
                .compact();
    } //end generatedToken()

    // JWT 토큰 유효성 검증 메소드
    public boolean validatedToken(String token) {
        try {
            Jwts.parser()
                    .setSigningKey(jwtProperties.getSecretKey())    // 비밀값으로 복호화
                    .parseClaimsJws(token);
            return true;
        } catch (Exception e) {     // 복호화 과정 중 에러 발싱 시 유효하지 않은 토큰
            return false;
        }
    } //end validToken()

    // 토큰 기반 인증 정보 가져오는 메소드
    public Authentication getAuthentication(String token) {
        Claims claims = getClaims(token);
        Set<SimpleGrantedAuthority> authorities = Set.of(new SimpleGrantedAuthority("ROLE_USER"));

        return new UsernamePasswordAuthenticationToken(
                new org.springframework.security.core.userdetails.User(claims.getSubject(), "", authorities), token, authorities);
    } //end getAuthentication()

    // 토큰 기반 사용자 ID 가져오는 메소드
    public Long getUserId(String token) {
        Claims claims = getClaims(token);
        return claims.get("id", Long.class);
    } //end getUserId()

    private Claims getClaims(String token) {
        return Jwts.parser()
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody();
    } //end getClaims()
}



6. SecurityFilterChain

package com.hyeongarl.config;

import com.hyeongarl.config.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

/**
     configure              : 스프링 시큐리티 기능 비활성화
     filterChain            : 특정 HTTP 요청에 대한 웹 기반 보안 구성
     authenticationManager  : 인증 관리자 관련 설정
     bCryptPasswordEncoder  : 비밀번호 인코더로 사용할 빈 등록
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityFilterChain {
    private final TokenProvider tokenProvider;
    private static String secretKey = "my-secret-key-777";

    @Bean   // 스프링 시큐리티 기능 비활성화(인증, 인가 서비스를 모든 곳에 적용하지 않는다.)
    public WebSecurityCustomizer configure() {
        return (web) -> web.ignoring()
                .requestMatchers("h2-console/**")
                .requestMatchers(new AntPathRequestMatcher("/static/**"));
    } //end configure()

    @Bean   // 특정 HTTP 요청에 대한 웹 기반 보안 구성 (인증, 인가, 로그인, 로그아웃 관련 설정)
    public org.springframework.security.web.SecurityFilterChain filterChain(HttpSecurity http) 
    	throws Exception {
        TokenAuthenticationFilter tokenFilter = new TokenAuthenticationFilter(tokenProvider);

        return http.authorizeHttpRequests(
                        // 특정 경로에 대한 엑세스 설정
                        auth -> auth.requestMatchers(       // 특정 요청과 일치하는 url에 대한 엑세스 설정
                                        new AntPathRequestMatcher("/login"),
                                        new AntPathRequestMatcher("/signup"),
                                        new AntPathRequestMatcher("/user")
                                ).permitAll()            // 누구나 접근 가능하게 설정
                                .anyRequest()            // 위에서 설정한 url 이외의 요청에 대해 설정
                                .authenticated()         // 별도의 인가는 필요하지 않으나 인증이 성공한 상태만 접근이 가능
                )
//                .formLogin(formLogin -> formLogin      // 폼 기반 로그인 설정
//                        .loginPage("/login")           // 로그인 페이지 경로 설정
//                        .defaultSuccessUrl("/")        // 로그인시 이동할 경로 설정
//                )
//                .logout(logout -> logout               // 로그아웃 설정
//                        .logoutSuccessUrl("/login")    // 로그아웃 성공 시 이동할 경로 설정
//                        .invalidateHttpSession(true)   // 로그아웃 이후 세션 삭제 여부 결정
//                )
                .csrf(AbstractHttpConfigurer::disable)      // CSRF 공격 방지를 위해서는 활성화하는게 좋음
                .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class)  // JWT 필터 추가
                .build();
    } //end filterChain()

    @Bean   // 인증 관리자 관련 설정 (사용자 정보를 가져올 서비스를 재정의, 인증 방법을 설정할 때 사용)
    public AuthenticationManager authenticationManager(HttpSecurity http,
                                                       BCryptPasswordEncoder bCryptPasswordEncoder,
                                                       UserDetailsService userDetailsService) throws Exception {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService); 
        	// 사용자 정보 서비스 설정(userDetailService를 상속받은 클래스여야 한다.)
        authProvider.setPasswordEncoder(bCryptPasswordEncoder); 
        	// 비밀번호를 암호화하기 위한 인코더를 설정
        return new ProviderManager(authProvider);
    } //end authenticationManager()

    @Bean   // 비밀번호 인코더로 사용할 빈 등록
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    } //end bCryptPasswordEncoder()
} //end class



7. TokenAuthenticationFiler

package com.hyeongarl.config;

import com.hyeongarl.config.jwt.TokenProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

/**
 * 들어오는 요청을 가로채 Authorization 헤더에서
 * JWT 토큰 추출
 * 유효성 검사
 * 토큰이 유효한 경우 인증 정보를 사용하여 Spring Security 컨텍스트를 설정
 */
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final TokenProvider tokenProvider;

    // JwtProperties 대신
    private final static String HEADER_AUTHORIZATION = "Authorization";
    private final static String TOKEN_PREFIX = "Bearer";

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        //요청 헤더의 Authorization 키 값 조회
        final String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);

        // 가져온 값에서 접두사 제거
        String token = getAccessToken(authorizationHeader);

        // 가져온 토큰이 유효한지 확인하고 유효한 경우 인증 정보 설정
        if(tokenProvider.validatedToken(token)) {
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    } //end doFilterInternal()

    private String getAccessToken(String authorizationHeader) {
        if(authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)) {
            return authorizationHeader.substring(TOKEN_PREFIX.length());
        }
        return null;
    } //end getAccessToken()
} //end class

 

 

 

8. User Entity 수정

> 기존 코드

더보기
package com.hyeongarl.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.LocalDateTime;

/*
    @Table(name="")
        데이터베이스의 name에 지정된 이름의 테이블과 매핑된다.
    @Entity
        JPA 엔티티임을 명시하여 데이터베이스 테이블의 레코드와 매핑
    @Getter, @Setter
        롬복 라이브러리의 어노테이션으로 모든 필드에 대해 자동으로 getter/setter를 생성
    @NoArgsConstructor
        롬복 라이브러리의 어노테이션으로 매개변수가 없는 기본 생성자를 자동으로 생성
 */
@Table(name="users")
@Entity
@Getter
@Setter
@NoArgsConstructor
public class User {
    /*
        @Id
            테이블의 기본 키
        @GeneratedValue(strategy = GenerationType.IDENTITY)
            기본 피의 생성 전략을 지정
            IDENTITY : 데이터베이스가 기본 키 생성을 담당
            기본적으로 AUTO_INCREMENT를 사용
        @Column(name="user_id", updatable = false)
            데이터베이스의 name과 일치하는 컬럼과 매핑된다.
            필드가 수정될 수 없음을 의미
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="user_id", updatable = false)
    private Long userId;

    /*
        @Email
            유효한 이메일 형식을 규정
        @Column(name="user_email", nullable = false, unique = true)
            데이터베이스의 name과 일치하는 컬럼과 매핑된다.
            null 값을 가질 수 없음을 의미
            유일한 값을 가져야 함을 의미
     */
    @Email
    @Column(name="user_email", nullable = false, unique = true)
    private String userEmail;

    @Column(name = "user_password", nullable = false, length=25)
    private String password;

    @Column(name="user_regdate", updatable = false)
    private LocalDateTime userRegdate;

    @Builder
    public User(String userEmail, String password) {
        this.userEmail = userEmail;
        this.password = password;
        this.userRegdate = LocalDateTime.now();
    } //end User()
}

UserDetails 클래스를 상속받고 메소드를 오버라이드한다.

> 수정코드

package com.hyeongarl.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;

/*
    @Table(name="")
        데이터베이스의 name에 지정된 이름의 테이블과 매핑된다.
    @Entity
        JPA 엔티티임을 명시하여 데이터베이스 테이블의 레코드와 매핑
    @Getter, @Setter
        롬복 라이브러리의 어노테이션으로 모든 필드에 대해 자동으로 getter/setter를 생성
    @NoArgsConstructor
        롬복 라이브러리의 어노테이션으로 매개변수가 없는 기본 생성자를 자동으로 생성
 */
@Table(name="users")
@Entity
@Getter
@Setter
@NoArgsConstructor
public class User implements UserDetails {
    /*
        @Id
            테이블의 기본 키
        @GeneratedValue(strategy = GenerationType.IDENTITY)
            기본 피의 생성 전략을 지정
            IDENTITY : 데이터베이스가 기본 키 생성을 담당
            기본적으로 AUTO_INCREMENT를 사용
        @Column(name="user_id", updatable = false)
            데이터베이스의 name과 일치하는 컬럼과 매핑된다.
            필드가 수정될 수 없음을 의미
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="user_id", updatable = false)
    private Long userId;

    /*
        @Email
            유효한 이메일 형식을 규정
        @Column(name="user_email", nullable = false, unique = true)
            데이터베이스의 name과 일치하는 컬럼과 매핑된다.
            null 값을 가질 수 없음을 의미
            유일한 값을 가져야 함을 의미
     */
    @Email
    @Column(name="user_email", nullable = false, unique = true)
    private String userEmail;

    @Column(name = "user_password", nullable = false, length=25)
    private String password;

    @Column(name="user_regdate", updatable = false)
    private LocalDateTime userRegdate;

    @Builder
    public User(String userEmail, String password) {
        this.userEmail = userEmail;
        this.password = password;
        this.userRegdate = LocalDateTime.now();
    } //end User()

    @Override   // 권한 반환
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of();
    }

    @Override   // 사용자의 id 반환
    public String getUsername() {
        return userEmail;
    }

    @Override   // 계정 만료 여부 반환
    public boolean isAccountNonExpired() {
        // 만료되었는지 확인하는 로직 필요
        return true;    // 만료 x
    }

    @Override   // 계정 잠금 여부 반환
    public boolean isAccountNonLocked() {
        // 잠금되었는지 확인하는 로직 필요
        return true; // 잠금 x
    }

    @Override   // 비밀번호 만료 여부 반환
    public boolean isCredentialsNonExpired() {
        // 만료되었는지 확인하는 로직 필요
        return true; // 만료 x
    }

    @Override   // 계정 사용 가능 여부 반환
    public boolean isEnabled() {
        // 사용 가능한지 확인하는 로직 필요
        return true;   // 사용 가능
    }
}

 

 

9. UserService 수정

UserDetailsService를 상속

> 기존 코드

더보기
package com.hyeongarl.service;

import com.hyeongarl.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

/*
    @Service
        Spring Framework에 의해 관리되는 서비스 클래스임을 명시
    @RequiredArgsConstructor
        롬복 라이브러리에서 final 필드에 대한 생성자를 자동으로 생성
 */
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
}

 

> 수정 코드

package com.hyeongarl.service;

import com.hyeongarl.entity.User;
import com.hyeongarl.error.UserAlreadyExistException;
import com.hyeongarl.error.UserNotFoundException;
import com.hyeongarl.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

/*
    @Service
        Spring Framework에 의해 관리되는 서비스 클래스임을 명시
    @RequiredArgsConstructor
        롬복 라이브러리에서 final 필드에 대한 생성자를 자동으로 생성
 */
/**
     save()             : 신규 등록
     findById()         : 아이디로 사용자 조회
     loadUserByUsername : 사용자 이메일을 사용하여 정보 가져오기
 */
@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
    private final UserRepository userRepository;
    // BCrypt 해시 알고리즘을 사용하여 비밀번호 암호화
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public User save(User user) {
        if(userRepository.existsByUserEmail(user.getUserEmail())) {
            throw new UserAlreadyExistException();
        }

        return userRepository.save(User.builder()
                .userEmail(user.getUserEmail())
                .password(bCryptPasswordEncoder.encode(user.getPassword()))
                .build());
    } //end save()

    public User findById(Long userId) {
        return userRepository.findById(userId)
                .orElseThrow(UserNotFoundException::new);
    } //end findById()

    @Override
    public UserDetails loadUserByUsername(String userEmail) throws UsernameNotFoundException {
        return userRepository.findByUserEmail(userEmail)
                .orElseThrow(UserNotFoundException::new);
    } //end loadUserByUsername()
} //end class

 

UserAlreadyExistException, UserNotFoundException은 이후 작성

 

 

10. UserRepository 수정

> 기존 코드

더보기
package com.hyeongarl.repository;

import com.hyeongarl.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

 

> 수정 코드

package com.hyeongarl.repository;

import com.hyeongarl.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUserEmail(String userEmail);

    @Query("SELECT CASE " +
            "WHEN COUNT(u) > 0 THEN true " +
            "ELSE false END FROM User u " +
            "WHERE u.userEmail = :userEmail")
    boolean existsByUserEmail(String userEmail);
}

 

@Query로 직접 쿼리문을 작성할 수 있다.

SELECT CASE 
	WHEN COUNT(u) > 0 THEN true
	ELSE false
	END
FROM User u 
WHERE u.userEmail = "userEmail";

이미 등록된 이메일 정보가 있다면 true를 반환

 

 

11.  UserController 수정

package com.hyeongarl.controller;

import com.hyeongarl.dto.UserRequestDto;
import com.hyeongarl.dto.UserResponseDto;
import com.hyeongarl.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {
    private final UserService userService;

    @PostMapping
    public UserResponseDto addUser(UserRequestDto userRequest) {
        return UserResponseDto.fromEntity(userService.save(userRequest.toEntity()));
    }
}

 

 

12. UserNotFoundException

package com.hyeongarl.error;

import org.springframework.http.HttpStatus;

public class UserNotFoundException extends BaseException {
    private static final String DEFAULT_MESSAGE = "사용자를 찾을 수 없습니다.";
    private static final String DEFAULT_CODE = "400005";

    public UserNotFoundException() {
        super(new ErrorResponse(DEFAULT_MESSAGE, DEFAULT_CODE), HttpStatus.NOT_FOUND);
    } //end UserNotFoundException()

    public UserNotFoundException(String message) {
        super(new ErrorResponse(message, DEFAULT_CODE), HttpStatus.NOT_FOUND);
    } //end UserNotFoundException()
} //end class

 

 

13. UserAlreadyExistsException

package com.hyeongarl.error;

import org.springframework.http.HttpStatus;

public class UserAlreadyExistException extends BaseException {
    private static final String DEFAULT_MESSAGE = "이미 사용중인 이메일입니다.";
    private static final String DEFAULT_CODE = "400006";

    public UserAlreadyExistException() {
        super(new ErrorResponse(DEFAULT_MESSAGE, DEFAULT_CODE), HttpStatus.CONFLICT);
    }

    public UserAlreadyExistException(String message) {
        super(new ErrorResponse(message, DEFAULT_CODE), HttpStatus.CONFLICT);
    }
}

 

 

 

JwtFactory

package com.hyeongarl.config.jwt;

import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Builder;
import lombok.Getter;

import java.time.Duration;
import java.util.Date;
import java.util.Map;

import static java.util.Collections.emptyMap;

/**
 * Jwt 토큰 서비스를 테스트하는데 사용할 모킹용 객체
 */
@Getter
public class JwtFactory {
    private String subject = "test@email.com";
    private Date issuedAt = new Date();
    private Date expiration = new Date(issuedAt.getTime() + Duration.ofDays(14).toMillis());
    private Map<String, Object> claims = emptyMap();

    @Builder
    public JwtFactory(String subject, Date issuedAt, Date expiration, Map<String, Object> claims) {
        this.subject = subject;
        this.issuedAt = issuedAt;
        this.expiration = expiration;
        this.claims = claims;
    } // end JwtFactory()

    public static JwtFactory withDefaultValues() {
        return JwtFactory.builder().build();
    } //end withDefaultValues()

    public String createToken(JwtProperties jwtProperties) {
        return Jwts.builder()
                .setSubject(subject)
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                .setIssuer(jwtProperties.getIssuer())
                .setIssuedAt(issuedAt)
                .setExpiration(expiration)
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
                .compact();
    }
}

 

 

 

TokenProviderTest

package com.hyeongarl.config.jwt;

import com.hyeongarl.entity.User;
import com.hyeongarl.repository.UserRepository;
import io.jsonwebtoken.Jwts;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.Duration;
import java.util.Date;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * TokenProvider 클래스 테스트
 */
@SpringBootTest
public class TokenProviderTest {
    @Autowired
    private TokenProvider tokenProvider;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private JwtProperties jwtProperties;

    @Test
    @DisplayName("generateToken()")
    void generateTokenTest() {
        User testUser = userRepository.save(User.builder()
                .userEmail("user12345@email.com")
                .password("1234")
                .build());

        String token = tokenProvider.generatedToken(testUser, Duration.ofDays(14));

        Long userId = Jwts.parser()
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody()
                .get("id", Long.class);

        assertThat(userId).isEqualTo(testUser.getUserId());
    }

    @Test
    @DisplayName("validateToken()")
    void validateToken_invalidTest() {
        String token = JwtFactory.builder()
                .expiration(new Date(new Date().getTime() - Duration.ofDays(7).toMillis()))
                .build()
                .createToken(jwtProperties);

        boolean result = tokenProvider.validatedToken(token);

        assertThat(result).isFalse();
    }

    @Test
    @DisplayName("validateToken()")
    void validateToken_validTest() {
        String token = JwtFactory.withDefaultValues().createToken(jwtProperties);

        boolean result = tokenProvider.validatedToken(token);

        assertThat(result).isTrue();
    }

    @Test
    @DisplayName("getAuthentication()")
    void getAuthenticationTest() {
        String userEmail = "user@email.com";
        String token = JwtFactory.builder()
                .subject(userEmail)
                .build()
                .createToken(jwtProperties);

        Authentication authentication = tokenProvider.getAuthentication(token);

        assertThat(((UserDetails) authentication.getPrincipal()).getUsername()).isEqualTo(userEmail);
    }

    @Test
    @DisplayName("getUserId()")
    void getUserIdTest() {
        Long userId = 1L;
        String token = JwtFactory.builder()
                .claims(Map.of("id", userId))
                .build()
                .createToken(jwtProperties);

        Long userIdByToken = tokenProvider.getUserId(token);

        assertThat(userIdByToken).isEqualTo(userId);
    }
}

 

> TokenProviderTest 실행

 

 

 

 

UserControllerTest

 

Project_07_Controller Test (TestRestTemplate)

목표.Controller 코드를 테스트하는 ControllerTest 코드를 작성하고자했다.  문제점.유사한 환경의 다른 사람의 작업을 참고하려다 보니, 이리저리 혼합되어 이도저도 아닌 코드가 되어버렸다.결국 @

hyeonga493.tistory.com

 

package com.hyeongarl.controller;

import com.hyeongarl.dto.UserRequestDto;
import com.hyeongarl.dto.UserResponseDto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;

/*
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
        SpringBoot 애플리케이션을 테스트하기 위한 통합 테스트를 설정한다.
        테스트시 무작위 포트가 할당된다.
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerTest {

    // RESTful 서비스 호출을 위한 의존성 주입
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    @DisplayName("사용자 등록")
    void testAddUser() {
        UserRequestDto userRequest = new UserRequestDto("addUser@email.com", "addUserPassword");

        ResponseEntity<UserResponseDto> responseEntity
                = restTemplate.postForEntity("/user", userRequest, UserResponseDto.class);

        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED);

        UserResponseDto responseBody = responseEntity.getBody();
        assertThat(responseBody).isNotNull();
        assertThat(responseBody.getUserEmail()).isEqualTo("addUser@email.com");
    }
}

 

> UserControllerTest / testAddUser() 실행

 

 

 

 

 

UserServiceTest

package com.hyeongarl.service;

import com.hyeongarl.dto.UserRequestDto;
import com.hyeongarl.entity.User;
import com.hyeongarl.error.UserAlreadyExistException;
import com.hyeongarl.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

/*
    @ExtendWith(MockitoExtension.class)
        JUnit5의 확장 모델을 활용하여 Mockito를 사용할 수 있다.
        MockExtension : Mockito에 대한 설정을 초기화 및 @Mockito 처리
 */
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    /*
        @Mock
            Mockito를 사용해 가짜 객체를 생성
     */
    @Mock
    private UserRepository userRepository;

    @Mock
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    /*
        @InjectMocks
            실제 객체로 만들어 필요한 의존성 객체를 주입
     */
    @InjectMocks
    private UserService userService;

    @Test
    @DisplayName("save_success")
    void saveUser_success() {
        UserRequestDto userRequest = new UserRequestDto("adduser@email.com", "adduserPassword");

        when(userRepository.existsByUserEmail(userRequest.toEntity().getUserEmail())).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(userRequest.toEntity());

        User user = userService.save(userRequest.toEntity());
        assertNotNull(user);
        assertEquals(userRequest.toEntity().getUserEmail(), user.getUserEmail());
        verify(userRepository, times(1)).save(any(User.class));
    }

    @Test
    @DisplayName("save_fail_invalid")
    void saveUser_fail_invalidUser() {
        UserRequestDto userRequest = new UserRequestDto("adduser_invalid", "adduserPassword");

        User user = userService.save(userRequest.toEntity());

        assertNull(user);
        verify(userRepository, times(1)).save(any(User.class));
    }

    @Test
    @DisplayName("save_fail_alreadyExist")
    void saveUser_fail_alreayExist() {
        UserRequestDto userRequest = new UserRequestDto("exist@email.com", "existPassword");

        when(userRepository.existsByUserEmail(userRequest.toEntity().getUserEmail())).thenReturn(true);

        assertThrows(UserAlreadyExistException.class, () -> userService.save(userRequest.toEntity()));

        verify(userRepository, never()).save(any(User.class));
    }
}

 

> UserServiceTest 실행

 

 

 

 

AuthControllerTest

package com.hyeongarl.controller;

import com.hyeongarl.dto.UserRequestDto;
import com.hyeongarl.dto.UserResponseDto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AuthControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    @DisplayName("login_success")
    void testLogin_success() {
        UserRequestDto userRequest = new UserRequestDto("login@email.com", "loginPassword");

        ResponseEntity<UserResponseDto> saveUser
                = restTemplate.postForEntity("/user", userRequest, UserResponseDto.class);
        ResponseEntity<String> responseEntity = restTemplate.postForEntity("/login", userRequest, String.class);

        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isNotNull();
    }
}

 

 

testLogin_success()가 동작하는 흐름을 알고 싶어서  System.out.println()으로 찍어봤다.

>>> SecurityFilterChain > bCryptPasswordEncoder
>>> SecurityFilterChain > filterChain
>>> SecurityFilterChain > configure
>>> SecurityFilterChain > authenticationManager
>>> TokenAuthenticationFilter > doFilterInternal
>>> TokenAuthenticationFilter > getAccessToken
>>> TokenProvider > validatedToken
>>> UserController > addUser
>>> UserResponseDto > fromEntity
>>> TokenAuthenticationFilter > doFilterInternal
>>> TokenAuthenticationFilter > getAccessToken
>>> TokenProvider > validatedToken
>>> AuthController > login
>>> TokenProvider > generatedToken

 

> 실행

 

실제 서버를 돌려서 postman으로 실행 시 인증 토큰이 발급되는 것을 확인할 수 있다.

 

 

-- 실패

package com.hyeongarl.controller;

import com.hyeongarl.config.jwt.TokenProvider;
import com.hyeongarl.dto.UserRequestDto;
import com.hyeongarl.dto.UserResponseDto;
import com.hyeongarl.service.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.ArrayList;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AuthControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Mock
    private UserService userService;

    @Mock
    private TokenProvider tokenProvider;

    @Mock
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @InjectMocks
    private AuthController authController;

    @BeforeEach
    public void setup() {
        MockitoAnnotations.openMocks(this);
        when(bCryptPasswordEncoder.matches(anyString(), anyString())).thenReturn(true);
    }

    @Test
    @DisplayName("login_fail_not_match_password")
    void testLogin_fail_not_match_password() {
        UserRequestDto userRequest = new UserRequestDto("test@example.com", "password");

        User user = new User("test@example.com", 
        			"$2a$10$D9aU8XQ6QXphF9PL5bR73ODNjZxK9UwGtJsYx.3vM8/7qZ1RzN.kC", 
                    new ArrayList<>());

        ResponseEntity<UserResponseDto> saveUser
                = restTemplate.postForEntity("/user", userRequest, UserResponseDto.class);

        when(userService.loadUserByUsername(anyString())).thenReturn(user);
        when(bCryptPasswordEncoder.matches(user.getPassword(), 
        					userRequest.getPassword())).thenReturn(false);

        UserRequestDto wrongUser = new UserRequestDto("test@example.com", "123454");

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<UserRequestDto> request = new HttpEntity<>(userRequest, headers);

        ResponseEntity<String> response 
        	= restTemplate.postForEntity("/login", wrongUser, String.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
        assertThat(response.getBody()).isEqualTo("비밀번호가 일치하지 않습니다.");
    }

}

 

 

testLogin_fail_not_match_password

 

비밀번호가 일치하지 않는 경우를 테스트하기 위해 계속 찾아봤는데 계속해서 오류가 발생했다.

안되는 코드를 가지고 몇시간을..

I/O error on POST request for "http://localhost:50295/login": cannot retry due to server authentication, in streaming mode org.springframework.web.client.ResourceAccessException: I/O error on POST request for "http://localhost:50295/login": cannot retry due to server authentication, in streaming mode

 

실제 서버를 돌려서 테스트하면 잘 동작한다.

반응형