728x90

 

개요

앞서 1편에서 기본 파일 구조와 기본 세팅을 했고, 2편에서 security 보안 설정을 했다.

이번 3편에서는 jwt 패키지에 대해서 작성할 것이다. 

일단.. 엔티티, 레포지토리, 서비스를 먼저 만들어 주자.

refreshToken, blackListToken 관리하기 위한 엔티티, 레포지토리, 서비스 생성

 

<- jwt의 대략적인 파일 구조

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

1. 엔티티 생성

  • RefreshToken
@Entity
@Table(name = "refresh_token")
@Getter
@Setter
public class RefreshToken {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // 리프레시 토큰의 고유 식별자

    @Column(name = "user_id")
    private Long userId; // 이 리프레시 토큰과 연관된 사용자의 식별자

    private String value; // 리프레시 토큰의 실제 값 -> 이 값은 서버가 클라이언트의 요청을 인증하는 데 사용
}
  • 위 엔티티는 주로 액세스 토큰의 재발급을 위한 리프레시 토큰을 저장하는 데 사용된다.

Q. 리프레시 토큰이 뭔데?

A. 리프레시 토큰은 일반적으로 액세스 토큰보다 긴 유효 기간을 가지도록 설정하며 엑세스 토큰이 만료가 되면 클라이언트는 리프레시 토큰을 사용하여 새로운 액세스 토큰을 요청한다.

즉, 서버는 리프레시 토큰을 검증하고, 유효한 경우 새로운 액세스 토큰을 발급한다.

 

  • JwtBlacklist
@Entity
@Table(name = "jwt_blacklist")
@Getter
@Setter
@NoArgsConstructor
public class JwtBlacklist {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // 블랙리스트 항목의 고유 식별자

    private String token; // 블랙리스트에 포함된 JWT 문자열

    @Column(name = "expiration_time")
    private Date expirationTime; // 블랙리스트에 있는 JWT의 만료 시간

    public JwtBlacklist(String token, Date expirationTime) {
        this.token = token;
        this.expirationTime = expirationTime;
    }
}
  • 사용자가 로그아웃하거나 토큰이 만료된 경우, 해당 토큰을 블랙리스트에 추가하여(이 때 token 필드에 JWT 문자열을 저장) 더 이상 사용되지 않도록 한다.
  • JWT 인증 필터에서 요청이 들어올 때, 토큰이 블랙리스트에 있는지 확인하여 유효성을 검사한다. (token 필드를 사용하여 검증 수행)
  • 블랙리스트에 있는 토큰은 서버에서 인식할 수 없으며, 인증 요청 시 해당 토큰이 블랙리스트에 포함되어 있으면 인증을 거부한다.

Q. 왜 블랙리스트를 써야하나?

  • 사용자가 로그아웃을 요청했을 때, 클라이언트 측에서는 캐시에 저장된 jwt를 제거하지만 서버에서는 jwt를 무력화할 수단이 없다.
  • 때문에 서버에서는 Jwt에 대한 블랙 리스트를 만들어 Jwt의 유효기간이 만료될 때 까지 Redis와 같은 DB에서 관리하는것을 말한다.
  • 만약 사용자가 Jwt를 이용하여 인가를 요청했을 시, Black List의 조회를 통해 사용자가 로그아웃한 Jwt인지 아닌지를 판별하는 것이다.

 

 

 

 

2. Repository 설정

  • RefreshTokenRepository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
    Optional<RefreshToken> findByValue(String value); // 리프레시 토큰 값으로 RefreshToken 엔티티 조회
    
    boolean existsByValue(String token); // 리프레시 토큰 값이 데이터베이스에 존재하는지 여부

    Optional<RefreshToken> findByUserId(Long userId); // 사용자 ID로 RefreshToken 엔티티 조회
}
  • JwtBlacklistRepository
public interface JwtBlacklistRepository extends JpaRepository<JwtBlacklist, Long> {
    boolean existsByToken(String token); // JWT 토큰이 블랙리스트에 존재하는지 여부 확인
}

각 메서드에 대해서 주석으로 설명함

 

 

 

3. Service 설정

  • RefreshTokenService
@Service
@RequiredArgsConstructor
public class RefreshTokenService {

    private final RefreshTokenRepository refreshTokenRepository;
	
    // 리프레시 토큰 추가
    @Transactional
    public RefreshToken addRefreshToken(RefreshToken refreshToken) {
        return refreshTokenRepository.save(refreshToken);
    }

	// 리프레시 토큰 조회
    @Transactional
    public Optional<RefreshToken> findRefreshToken(String refreshToken) {
        return refreshTokenRepository.findByValue(refreshToken);
    }

	// 리프레시 토큰 삭제
    public void deleteRefreshToken(String refreshToken) {
        refreshTokenRepository.findByValue(refreshToken).ifPresent(refreshTokenRepository::delete);
    }

	// 사용자 기반 리프레시 토큰 삭제
    public void deleteRefreshToken(Long userId) {
        refreshTokenRepository.findByUserId(userId).ifPresent(refreshTokenRepository::delete);
    }

	// 리프레시 토큰 유효성 검증
    public boolean isRefreshTokenValid(String refreshToken) {
        return refreshTokenRepository.existsByValue(refreshToken);
    }
}
  • RefreshTokenService는 리프레시 토큰(Refresh Token)을 관리하는 데 관련된 기능을 제공한다.
  • 리프레시 토큰은 사용자가 인증된 상태를 유지하도록 돕는 역할을 하고 새로운 액세스 토큰을 발급받기 위해 사용된다.

관련 메서드

  • 리프레시 토큰 추가 (addRefreshToken)
    • RefreshToken 객체를 데이터베이스에 저장하여 새로운 리프레시 토큰을 추가한다.
    • 이 메서드는 리프레시 토큰을 발급하고 저장하는 데 사용된다.
  • 리프레시 토큰 조회 (findRefreshToken)
    • 주어진 리프레시 토큰 값을 사용하여 데이터베이스에서 해당 리프레시 토큰을 찾는다.
    • 리프레시 토큰이 유효한지 확인하고 관련 정보를 반환하는 데 사용된다.
  • 리프레시 토큰 삭제 (deleteRefreshToken)
    • 주어진 리프레시 토큰 값을 사용하여 데이터베이스에서 해당 리프레시 토큰을 삭제한다.
    • 사용자가 로그아웃하거나 토큰을 무효화할 때 사용된다.
  • 사용자 기반 리프레시 토큰 삭제 (deleteRefreshToken(Long userId))
    • 특정 사용자 ID에 연관된 리프레시 토큰을 삭제한다.
    • 사용자가 로그아웃하거나 리프레시 토큰을 관리할 때 사용된다.
  • 리프레시 토큰 유효성 검증 (isRefreshTokenValid)
    • 주어진 리프레시 토큰이 데이터베이스에 존재하는지 확인하여 유효성을 검증한다.
    • 유효한 리프레시 토큰인지 여부를 반환한다.

 

  • JwtBlacklistService
@Service
@RequiredArgsConstructor
public class JwtBlacklistService {

    private final JwtBlacklistRepository jwtBlacklistRepository;
    
    // 블랙리스트에 추가
    public void save(JwtBlacklist blacklist) {
        jwtBlacklistRepository.save(blacklist);
    }
    
    // 블랙리스트 저장
    public void addToBlacklist(String token, Date expirationDate) {
        JwtBlacklist blacklist = new JwtBlacklist(token, expirationDate);
        jwtBlacklistRepository.save(blacklist);
    }

	// 토큰 블랙리스트 확인
    public boolean isTokenBlacklisted(String token) {
        return jwtBlacklistRepository.existsByToken(token);
    }
}
  • JwtBlacklistService는 블랙리스트에 관리되는 JWT의 기능을 제공하여, 사용자가 로그아웃하거나 특정 JWT를 무효화할 때 사용하는 서비스이다.
  • 블랙리스트는 특정 JWT가 더 이상 유효하지 않다는 것을 기록한다.

관련 메서드

  • 블랙리스트에 추가 (addToBlacklist)
    • 주어진 JWT와 만료 시간을 사용하여 새로운 JwtBlacklist 항목을 생성하고, 이를 데이터베이스에 저장한다.
    • 이 메서드는 특정 JWT를 블랙리스트에 추가하여 더 이상 유효하지 않게 만든다.
  • 블랙리스트 저장 (save):
    • JwtBlacklist 객체를 데이터베이스에 저장한다.
    • 이 메서드는 블랙리스트 항목을 생성하고 저장하는 데 사용된다..
  • 토큰 블랙리스트 확인 (isTokenBlacklisted):
    • 주어진 JWT가 블랙리스트에 포함되어 있는지 확인한다.
    • 이 메서드는 JWT가 블랙리스트에 있는 경우 true를 반환하여, 해당 토큰이 더 이상 유효하지 않음을 알린다.

 

 

 

 

글이 길어져서 jwt 관련해서는 4편에서 이어서 쓰겠다. 4편에서....

728x90
728x90

 

개요

1편에서 기본적인 세팅은 끝이 났다. 이제 본격적으로 Security와 JWT를 사용하기 위한 코드를 짜볼 것이다.

2편에서는 SecurityConfig에 대해서 알아보자.

 

 

<- 대략적인 파일 구조

 

 

 

 

 

 

SecurityConfig.java

/**
 * 웹 애플리케이션의 보안을 설정
*/

@Configuration // 이 클래스가 Spring의 설정 클래스를 나타낸다는 것을 의미
@EnableWebSecurity // Spring Security를 활성화
@RequiredArgsConstructor // final로 선언된 필드를 위한 생성자 자동 생성
public class SecurityConfig {

    private final CustomUserDetailsService customUserDetailsService; // 사용자 세부 정보 로드 서비스
    private final JwtTokenizer jwtTokenizer; // JWT 토큰을 생성하고 검증하는 도구
    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; // 인증 실패 시 처리할 커스텀 엔트리 포인트
    private final JwtBlacklistService jwtBlacklistService; // 블랙리스트에 등록된 JWT를 관리하는 서비스
    private final RefreshTokenService refreshTokenService; // 리프레시 토큰을 관리하는 서비스

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    	// 특정 URL 패턴에 대한 접근 권한 설정
        http
                .authorizeRequests(authorize -> authorize
                        .requestMatchers("/userregform", "/loginform", "/login", "/css/**", "/js/**", "/files/**", "/", "/api/login", "/api/**").permitAll() // 이 주소로 시작되면 인증 필요 없음
                        .requestMatchers("/posts/**", "/{username}", "/about/{username}", "/ws","/chat").permitAll() // 이 주소로 시작되면 인증 필요 없음
                        .requestMatchers("/admin/**", "/admin").hasRole("ADMIN") // 관리자 역할을 가진 사용자만 접근
                        .requestMatchers("/comments/post/**", "/likes/post/**").authenticated() // 인증된 사용자만 접근
                        .anyRequest().authenticated() // 그 외의 모든 요청은 인증된 사용자만 접근
                )
                // UsernamePasswordAuthenticationFilter 앞에 JWT 인증 필터를 추가하여 JWT를 통한 인증을 처리
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenizer, jwtBlacklistService, refreshTokenService), UsernamePasswordAuthenticationFilter.class)
                .formLogin(form -> form.disable()) // 기본 폼 로그인 비활성화
                .sessionManagement(sessionManagement -> sessionManagement
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션을 사용하지 않음
                // CSRF 보호 비활성화
                .csrf(csrf -> csrf.disable())
                // http 기본 인증(헤더) 비활성화
                .httpBasic(httpBasic -> httpBasic.disable())
                // CORS (Cross-Origin Resource Sharing)를 구성 및 허용 
                .cors(cors -> cors.configurationSource(configurationSource()))
                // 사용자 정의 인증 진입 지점을 처리하기 위한 예외 처리를 구성
                .exceptionHandling(exception -> exception
                        .authenticationEntryPoint(customAuthenticationEntryPoint)
                );
        return http.build(); // SecurityFilterChain을 빌드 후 반환
    }

    /**
     * 모든 origin, header, method를 허용하는 CORS 구성 소스 정의
     */
    @Bean
    public CorsConfigurationSource configurationSource() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin("*"); //모든 도메인 허용
        config.addAllowedHeader("*"); //모든 HTTP 메서드 허용
        config.addAllowedMethod("*");
        config.setAllowedMethods(List.of("GET", "POST", "DELETE")); // 명시적으로 GET, POST, DELETE 메서드 허용
        source.registerCorsConfiguration("/**", config);
        return source;
    }

    /**
     * BCryptPasswordEncoder 빈 정의
     * 비밀번호를 암호화하는 데 사용
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 

어노테이션에 대한 설명이나 기본적인 건 주석을 안할려고 했지만... 그래도 공부되고 좋으니깐^-^

 

Q. 여기서 세션을 사용하지 않은 이유는 ??

기본적으로 JWT의 정의를 살펴보면,  

- 서버가 클라이언트의 상태를 저장하지 않고, 클라이언트가 요청할 때마다 JWT를 서버로 전달하여 인증을 수행한다.

- 클라이언트의 모든 요청은 JWT에 의해 독립적으로 인증되며, 서버는 JWT를 검증하여 요청의 유효성을 확인한다. 이로 인해 서버는 클라이언트 상태를 유지할 필요가 없다.

즉, 

 

 

1. JWT 는 세션 기반 인증 방식의 한계를 극복하기 위해 설계된 것이다.

2. JWT 는 페이로드에 사용자 정보와 권한을 포함하고 서버는 JWT 를 검증하여 인증을 처리한다. 

    즉, 서버는 추가적인 상태 정보를 유지할 필요가 없다. 

3. 그렇기에 filterChain에서 세션을 비활성화 시킨 것이다.

 

 

User, Role, Enum 엔티티 설정

  • User.java
@Entity
@Getter@Setter
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    @Column(name = "user_name", nullable = false, unique = true)
    private String userName;

    @Column(name = "user_nick", nullable = false, unique = true)
    private String userNick;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String password;
}

유저 엔티티이다. 

 

  • Role.java
@Entity
@Table(name = "roles")
@Getter@Setter
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long roleId;

    @Enumerated(EnumType.STRING)
//    @Column(nullable = false, unique = true, length = 50)
    private RoleName roleName;

}

역할 엔티티이다. 이때 역할 이름은 enum 타입으로 지정했다.

 

  • RoleName.enum
public enum RoleName {
    ROLE_ADMIN,
    ROLE_USER
}

 

 

 

 

 

 

3편에서... jwt를 사용하기에 앞서 필요한 엔티티와 레포지토리, 서비스를 작성하고 4편부터 본격적으로 jwt 를 사용할 것이다.

728x90
728x90

 

개요

개인 프로젝트를 진행하는 중 시큐리티 부분은 아주 중요하고 헷갈리기 때문에 정리해놓기 위해 포스팅 하기로 했다.

gradle 의존성을 추가 한 후 -> 프로젝트 파일 구조에 맞게 패키지와 관련 클래스를 작성한 후 -> 프로젝트에 필요한 환경변수를 세팅하면 기본적인 작업은 끝난다. 

 

 

나의 환경

Window 11

intelliJ

java 21 

spring Boot 3.3.0

spring Security 6 

jwt 0.11.5

 

 

프로젝트 파일 구조

 

<- 다음과 같은 구조로 프로젝트를 진행하는데
이 중 시큐리티+jwt를 사용하기에 필요한 패키지는 

"config, controller, domain, dto, repository, security, service" 이다.

이 패키지 중 만들 클래스들은 연두색으로 표시한 부분만 만들면 된다.

 

 

 

 

의존성 설치

// security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'

// lombok
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// 타임리프에서 스프링시큐리티를 쓸수있게.
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6:3.1.2.RELEASE'
implementation 'org.jsoup:jsoup:1.14.3'

// jwt & json
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

// gson - json 메시지를 다루기 위한 라이브러리
implementation 'com.google.code.gson:gson'

// validation사용
implementation 'jakarta.validation:jakarta.validation-api:3.0.2'

 

필요한 환경변수 세팅

 

jwt secretKey와 refreshKey에 대해서 .yml 파일에 작성해준다. 

시크릿 키를 얻는 방법은 아래에 적어놨다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

PowerShell의 Get-Random cmdlet을 사용하여 64바이트의 랜덤 데이터를 생성하고 이를 16진수로 변환하여 시크릿 키를 얻는다.

# 64바이트의 랜덤 데이터 생성
$bytes = @(0..63 | ForEach-Object { Get-Random -Maximum 256 })

# 16진수로 변환
$hex = $bytes.ForEach({ "{0:x2}" -f $_ }) -join ""

# 결과 출력
$hex

 

참고로 맥에서는 다음과 같이 하면 시크릿 키를 얻을 수 있다.

openssl rand -hex 64

 

 

 

2편에서.. Security 설정에 대해 알아볼 것이다.

728x90

+ Recent posts