코드 그라데이션

[중간점검] 스프링 3버전 업그레이드 이후 전체 코드 점검(3.0.1) /3.05 본문

Spring/Security

[중간점검] 스프링 3버전 업그레이드 이후 전체 코드 점검(3.0.1) /3.05

완벽한 장면 2023. 12. 26. 23:54

build.gradle과 gradle-wrapper.properties는 앞쪽에 있으므로 우선 생략

자카르타로 바뀜

 

cf. 3.0.5 버전으로 업그레이드 하려면

그냥 버전만 업그레이드 해주면 된다(시큐리티 설정 안 건드려도 되나봄)

Config

CorsConfig

package inflearn.freejwt.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

    // CORS 필터 빈을 정의하는 메서드
    @Bean
    public CorsFilter corsFilter() {
        // URL 기반의 CORS 구성을 관리하는 객체 생성
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        // CORS 구성 설정 객체 생성
        CorsConfiguration config = new CorsConfiguration();

        // 요청에서 자격 증명(자격 증명 쿠키, 인증 등)을 허용
        config.setAllowCredentials(true);

//        // 모든 출처(Origin)를 허용
//        config.addAllowedOrigin("*");
        config.addAllowedOriginPattern("*");

        // 모든 HTTP 헤더를 허용
        config.addAllowedHeader("*");

        // 모든 HTTP 메서드(GET, POST, PUT, DELETE 등)를 허용
        config.addAllowedMethod("*");

        // "/api/**" 패턴에 해당하는 엔드포인트에 대해 CORS 구성을 등록
        source.registerCorsConfiguration("/api/**", config);

        // CORS 필터를 생성하고 반환
        return new CorsFilter(source);
    }
}

 

SecurityConfig

package inflearn.freejwt.config;


import inflearn.freejwt.jwt.JwtAccessDeniedHandler;
import inflearn.freejwt.jwt.JwtAuthenticationEntryPoint;
import inflearn.freejwt.jwt.JwtSecurityConfig;
import inflearn.freejwt.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.filter.CorsFilter;

@EnableWebSecurity // 기본적인 웹 시큐리티 설정을 활성화하겠다.
@EnableMethodSecurity // 추가
@Configuration
@RequiredArgsConstructor
public class SecurityConfig { // extend 삭제

    private final TokenProvider tokenProvider;
    private final CorsFilter corsFilter;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /**
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception
     *
     * 많은 부분들을 추가
     * : 토큰을 사용하기 때문에 csrf 설정은 disable
     *  Exception을 핸들링할 때 만들었던 클래스들을 추가한다.
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        // token을 사용하는 방식이기 때문에 csrf를 disable한다.
        httpSecurity.csrf().disable()
                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                // h2-console을 위한 설정
                .and()
                .headers()
                .frameOptions()
                .sameOrigin()


                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session을 사용하지 않음.

                .and()
                .authorizeHttpRequests()
                .requestMatchers("/api/hello", "/api/authenticate", "/api/signup").permitAll()
                .requestMatchers(PathRequest.toH2Console()).permitAll()
                .anyRequest().authenticated()

                .and()
                .apply(new JwtSecurityConfig(tokenProvider));

        return httpSecurity.build();
    }
}

 

 


Controller

AuthController

package inflearn.freejwt.controller;

import inflearn.freejwt.dto.LoginDto;
import inflearn.freejwt.dto.TokenDto;
import inflearn.freejwt.jwt.JwtFilter;
import inflearn.freejwt.jwt.TokenProvider;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class AuthController {

    private final TokenProvider tokenProvider;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;


    @PostMapping("/authenticate")
    public ResponseEntity<TokenDto> authorize(@Valid @RequestBody LoginDto loginDto) {

        // 사용자가 제공한 로그인 정보(username과 password)를 사용하여
        // UsernamePasswordAuthenticationToken을 생성
        UsernamePasswordAuthenticationToken authenticationToken
                = new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());

        // AuthenticationManagerBuilder를 사용하여
        // 생성된 authenticationToken을 인증
        Authentication authentication
                = authenticationManagerBuilder.getObject().authenticate(authenticationToken);

        // 인증에 성공한 경우, SecurityContextHolder에 현재 인증 정보를 설정
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // TokenProvider를 사용하여 JWT(JSON Web Token)를 생성
        String jwt = tokenProvider.createToken(authentication);

        // HTTP 응답 헤더에 JWT 토큰을 추가
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(JwtFilter.AUTHORIZATION_HEADER, "Bearer " + jwt);

        // JWT 토큰을 포함한 TokenDto를 응답으로 반환
        return new ResponseEntity<>(new TokenDto(jwt), httpHeaders, HttpStatus.OK);
    }
}

 

UserController

package inflearn.freejwt.controller;

import inflearn.freejwt.dto.UserDto;
import inflearn.freejwt.service.UserService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class UserController {

    private final UserService userService;

    /**
     * 사용자 등록을 처리하는 엔드포인트.
     *
     * @param userDto 등록할 사용자 정보를 포함한 UserDto 객체
     * @return ResponseEntity<UserDto> 등록된 사용자 정보를 포함한 ResponseEntity
     */
    @PostMapping("/signup")
    public ResponseEntity<UserDto> signup(@Valid @RequestBody UserDto userDto) {
        return ResponseEntity.ok(userService.signup(userDto));
    }

    /**
     * 현재 사용자의 정보를 조회하는 엔드포인트.
     *
     * @param request HttpServletRequest 객체
     * @return ResponseEntity<UserDto> 현재 사용자 정보를 포함한 ResponseEntity
     */
    @GetMapping("/user")
    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    public ResponseEntity<UserDto> getMyUserInfo(HttpServletRequest request) {
        return ResponseEntity.ok(userService.getMyUserWithAuthorities());
    }

    /**
     * 리디렉션 테스트를 위한 엔드포인트.
     *
     * @param response HttpServletResponse 객체
     * @throws IOException 입출력 예외 발생 시
     */
    @PostMapping("/test-redirect")
    public void testRedirect(HttpServletResponse response) throws IOException {
        response.sendRedirect("/api/user");
    }

    /**
     * 특정 사용자의 정보를 조회하는 엔드포인트.
     *
     * @param username 조회할 사용자의 이름
     * @return ResponseEntity<UserDto> 조회된 사용자 정보를 포함한 ResponseEntity
     */
    @GetMapping("/user/{username}")
    @PreAuthorize("hasAnyRole('ADMIN')")
    public ResponseEntity<UserDto> getUserInfo(@PathVariable String username) {
        return ResponseEntity.ok(userService.getUserWithAuthorities(username));
    }
}

 

 


DTO

AuthorityDto

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class AuthorityDto {

    private String authorityName;
}

 

ErrorDto

package inflearn.freejwt.dto;

import org.springframework.validation.FieldError;

import java.util.ArrayList;
import java.util.List;

// ErrorDTO 클래스는 API 또는 웹 애플리케이션에서 예외 또는 오류 정보를 표현하는 데 사용됩니다.
public class ErrorDto {
    // 상태 코드를 나타내는 변수. HTTP 상태 코드와 관련이 있으며, 클라이언트에게 반환됨.
    private final int status;

    // 클라이언트에게 오류의 원인을 설명하는 데 사용.
    private final String message;

    // 필드 오류(FieldError) 객체들을 저장하는 리스트.
    // 필드 오류는 양식 유효성 검사와 관련이 있으며, 특정 필드에 대한 오류를 저장.
    private List<FieldError> fieldErrors = new ArrayList<>();



    // ErrorDTO 클래스의 생성자. 상태 코드와 메시지를 전달하여 객체를 초기화.
    public ErrorDto(int status, String message) {
        this.status = status;
        this.message = message;
    }

    public int getStatus() {
        return status;
    }


    public String getMessage() {
        return message;
    }

    // 필드 오류(FieldError)를 추가하는 메서드입
    // 이 메서드를 사용하여 특정 필드에 대한 오류를 ErrorDTO 객체에 추가할 수 있다.
    public void addFieldError(String objectName, String path, String message) {
        FieldError error = new FieldError(objectName, path, message);
        fieldErrors.add(error);
    }

    public List<FieldError> getFieldErrors() {
        return fieldErrors;
    }
}

 

LoginDto

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginDto {

    @NotNull
    @Size(min = 3, max = 50) // 유효성 검사
    private String username;

    @NotNull
    @Size(min = 3, max = 100)
    private String password;

}

 

 

TokenDto

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TokenDto {

    private String token;
}

 

 

UserDto

package inflearn.freejwt.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import inflearn.freejwt.entity.User;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.*;

import java.util.Set;
import java.util.stream.Collectors;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserDto {

    @NotNull
    @Size(min = 3, max = 50)
    private String username;

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @NotNull
    @Size(min = 3, max = 100)
    private String password;

    @NotNull
    @Size(min = 3, max = 50)
    private String nickname;


    // 추가: UserDto 클래스에 있는 authorityDtoSet 필드를 선언.
    private Set<AuthorityDto> authorityDtoSet;

    // 정적 메서드인 from(User user)를 정의.
    public static UserDto from(User user) {
        // 만약 입력으로 주어진 user가 null이면 null을 반환.
        if (user == null) return null;

        // UserDto 객체를 빌더 패턴을 사용하여 생성.
        return UserDto.builder()
                // User 객체의 username 값을 UserDto 객체의 username에 설정.
                .username(user.getUsername())
                // User 객체의 nickname 값을 UserDto 객체의 nickname에 설정.
                .nickname(user.getNickname())
                // User 객체의 authorities (권한) 집합을 스트림으로 변환한 후,
                // 각 권한을 AuthorityDto 객체로 매핑하여 새로운 Set으로 수집.
                .authorityDtoSet(user.getAuthorities().stream()
                        .map(authority -> AuthorityDto.builder().authorityName(authority.getAuthorityName()).build())
                        .collect(Collectors.toSet()))
                // UserDto 객체를 빌드하여 반환.
                .build();
    }
}

 


Entity

Authority

package inflearn.freejwt.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.*;

@Entity
@Table(name = "authority")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Authority {

    @Id
    @Column(name = "authority_name", length = 50)
    private String authorityName; // 권한 이름
}

 

User

package inflearn.freejwt.entity;

import jakarta.persistence.*;
import lombok.*;

import java.util.Set;

@Entity
@Table(name = "'user'")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long userId;

    @Column(name = "username", length = 50, unique = true)
    private String username;


    @Column(name = "password")
    private String password;

    @Column(name = "nickname", length = 50)
    private String nickname;


    @Column(name = "activated")
    private boolean activated;

    @ManyToMany
    @JoinTable( // 다대다를 일대다, 다대일 관계로 재정의했다는 뜻.
            name = "user_authority",
            joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")})
    private Set<Authority> authorities;

}

 

 


 

Exception

DuplicateMemberException

// 사용자 정의 예외 클래스인 DuplicateMemberException 을 정의.
public class DuplicateMemberException extends RuntimeException {

    // 기본 생성자
    public DuplicateMemberException() {
        super();
    }

    // 메시지와 원인 예외를 받는 생성자
    public DuplicateMemberException(String message, Throwable cause) {
        super(message, cause);
    }

    // 메시지를 받는 생성자
    public DuplicateMemberException(String message) {
        super(message);
    }

    // 원인 예외를 받는 생성자
    public DuplicateMemberException(Throwable cause) {
        super(cause);
    }
}

 

 


Handler

MethodArgumentNotValidExceptionHandler

package inflearn.freejwt.handler;

import inflearn.freejwt.dto.ErrorDto;
import inflearn.freejwt.exception.DuplicateMemberException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;


// 이 어노테이션은 클래스가 예외 처리기로 사용될 때 우선순위를 지정.
// Ordered.HIGHEST_PRECEDENCE는 가장 높은 우선순위를 나타내며,
// 이 클래스가 다른 예외 처리기보다 먼저 실행됨을 의미.
@Order(Ordered.HIGHEST_PRECEDENCE)
@ControllerAdvice
public class MethodArgumentNotValidExceptionHandler {

    // @ControllerAdvice 어노테이션이 지정된 클래스는 전역 예외 처리를 위한 클래스임을 알려준다.
    @ControllerAdvice
    public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

        // @ResponseStatus 어노테이션은 해당 메서드가 호출될 때 HTTP 응답 상태 코드를 CONFLICT(409)로 설정하도록 지정한다.
        @ResponseStatus(HttpStatus.CONFLICT)
        // @ExceptionHandler 어노테이션은 특정 예외 타입(DuplicateMemberException)을 처리하기 위한 메서드를 지정한다.
        @ExceptionHandler(value = { DuplicateMemberException.class })
        // @ResponseBody 어노테이션은 해당 메서드가 HTTP 응답 본문에 데이터를 직렬화하여 반환함을 나타낸다.
        @ResponseBody
        // ResponseEntityExceptionHandler 클래스를 확장하여 예외를 처리하는 메서드입니다.
        // 이 메서드는 RuntimeException을 받고, 해당 예외와 WebRequest를 사용하여 ErrorDto를 생성하고 반환.
        protected ErrorDto badRequest(RuntimeException ex, WebRequest request) {
            // ErrorDto 객체를 생성하고 HTTP 응답 상태 코드와 예외 메시지를 설정하여 반환.
            return new ErrorDto(HttpStatus.CONFLICT.value(), ex.getMessage());
        }
    }

}

 

 

RestResponseEntityException

package inflearn.freejwt.handler;

import inflearn.freejwt.dto.ErrorDto;
import inflearn.freejwt.exception.DuplicateMemberException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import static org.springframework.http.HttpStatus.FORBIDDEN;

// @ControllerAdvice 어노테이션이 지정된 클래스는 전역 예외 처리를 위한 클래스.
@ControllerAdvice
public class RestResponseEntityException extends ResponseEntityExceptionHandler {

    // @ResponseStatus 어노테이션은 해당 메서드가 호출될 때 HTTP 응답 상태 코드를 CONFLICT(409)로 설정하도록 지정.
    @ResponseStatus(HttpStatus.CONFLICT)
    // @ExceptionHandler 어노테이션은 특정 예외 타입(DuplicateMemberException)을 처리하기 위한 메서드를 지정.
    @ExceptionHandler(value = { DuplicateMemberException.class })
    // @ResponseBody 어노테이션은 해당 메서드가 HTTP 응답 본문에 데이터를 직렬화하여 반환함.
    @ResponseBody
    // ResponseEntityExceptionHandler 클래스를 확장하여 예외를 처리하는 메서드.
    // 이 메서드는 RuntimeException을 받고, 해당 예외와 WebRequest를 사용하여 ErrorDto를 생성하고 반환.
    protected ErrorDto conflict(RuntimeException ex, WebRequest request) {
        // ErrorDto 객체를 생성하고 HTTP 응답 상태 코드와 예외 메시지를 설정하여 반환.
        return new ErrorDto(HttpStatus.CONFLICT.value(), ex.getMessage());
    }

    protected ErrorDto forbidden(RuntimeException ex, WebRequest request) {
        return new ErrorDto(FORBIDDEN.value(), ex.getMessage());
    }
}

 

 


jwt

JwtAccessDeniedHandler

package inflearn.freejwt.jwt;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;


import java.io.IOException;

// 필요한 권한이 존재하지 않는 경우에 403 에러 리턴하기 위함
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_FORBIDDEN);
    }
}

 

 

JwtAuthenticationEntryPoint

package inflearn.freejwt.jwt;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

// 유효한 자격증명을 제공하지 않고 접근하려고 할 때 401 Unauthorized 에러를 리턴할 클래스
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED); // 401에러
    }
}

 

 

JwtFilter

package inflearn.freejwt.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;


import java.io.IOException;

public class JwtFilter extends GenericFilterBean {

    private static final Logger logger = LoggerFactory.getLogger(JwtFilter.class);

    // Authorization 헤더의 이름
    public static final String AUTHORIZATION_HEADER = "Authorization";

    private TokenProvider tokenProvider;

    public JwtFilter(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }

    /**
     *
     * @param servletRequest  The request to process
     * @param servletResponse The response associated with the request
     * @param filterChain    Provides access to the next filter in the chain for this
     *                 filter to pass the request and response to for further
     *                 processing
     *
     * @throws IOException
     * @throws ServletException
     *
     * 실제 필터링 로직은 이 메서드 내부에 작성
     * 이것은 토큰의 인증정보를 SecurityContext에 저장하는 역할을 수행
     */
    @Override
    public void doFilter(ServletRequest servletRequest,
                         ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String jwt = resolveToken(httpServletRequest);
        String requestURI = httpServletRequest.getRequestURI();

        // 추출한 JWT 토큰이 유효한지 검증
        if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
            // JWT 토큰을 사용하여 사용자 인증 정보 생성
            Authentication authentication = tokenProvider.getAuthentication(jwt);
            // 정상 토큰이면 Spring Security의 SecurityContextHolder에 인증 정보 저장
            SecurityContextHolder.getContext().setAuthentication(authentication);

            // 로깅
            logger.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
        } else {
            // 유효한 JWT 토큰이 없을 경우 로깅
            logger.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI);
        }
        // 다음 필터 또는 요청 처리로 전달
        filterChain.doFilter(servletRequest, servletResponse);
    }


    /**
     *
     * @param request
     * @return
     * Request Header에서 토큰 정보를 꺼내오기 위한 resolveToken 메서드
     */
    // Authorization 헤더에서 JWT 토큰 추출
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

}

 

 

JwtSecurityConfig

package inflearn.freejwt.jwt;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * TokenProvider, JwtFilter를 SecurityConfig에 적용할 때 사용할 클래스
 */
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    private final TokenProvider tokenProvider;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        JwtFilter customFilter = new JwtFilter(tokenProvider);
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

 

 

TokenProvider

package inflearn.freejwt.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;

@Component
public class TokenProvider implements InitializingBean {

    private final Logger logger = LoggerFactory.getLogger(TokenProvider.class);

    private static final String AUTHORITIES_KEY = "auth";

    private final String secret;
    private final long tokenValidityInMilliseconds;

    private Key key;


    public TokenProvider(
            @Value("${jwt.secret}") String secret,
            @Value("${jwt.token-validity-in-seconds}") long tokenValidityInSeconds) {
        this.secret = secret;
        this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000;
    }

    @Override
    public void afterPropertiesSet() {
        // JWT 서명을 위한 Key 생성
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }
    /**
     * InitializingBean을 implements 해서 afterPropertiesSet을 오버라이드 한 이유는
     * 빈이 생성 되고 주입을 받은 후에 secret 값을 Base64 Decode 해서 key 변수에 할당하기 위함.
     */


    /**
     *
     * @param authentication
     * @return
     *
     * Authentication 객체의 권한정보를 이용해서 토큰을 생성하는 createToken 메서드 추가
     */
    public String createToken(Authentication authentication) {
        // 사용자 권한을 JWT에 추가
        String authorities = authentication.getAuthorities()
                .stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();
        Date validity = new Date(now + this.tokenValidityInMilliseconds); // 토큰 만료 시간

        // JWT 토큰 생성
        return Jwts.builder()
                .setSubject(authentication.getName())
                .claim(AUTHORITIES_KEY, authorities)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(validity)
                .compact();
    }


    /**
     *
     * @param token
     * @return
     *
     * Token에 담겨있는 정보를 이용해 Authentication 객체를 리턴하는 메서드 생성
     */
    public Authentication getAuthentication(String token) {
        // JWT 토큰 파싱
        // 토큰을 파라미터로 받아서 클레임을 만들고
        Claims claims = Jwts
                .parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();


        // 사용자 권한 파싱 및 Principal 생성
        Collection<? extends GrantedAuthority> authorities = // 클레임에서 권한정보들을 빼내서
                Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                        .map(SimpleGrantedAuthority::new)
                        .collect(Collectors.toList());

        // 권한정보들을 이용해서 User 객체를 만들어 주고,
        User principal = new User(claims.getSubject(), "", authorities);

        // 유저객체와 토큰, 권한정보를 이용해서 최종적으로
        // Authentication 객체 반환
        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    /**
     *
     * @param token
     * @return
     * 토큰을 파라미터로 받아서 토큰의 유효성 검사를 수행하는 validateToken 메서드 추가
     */
    public boolean validateToken(String token) {
        try {
            // 토큰을 파싱해보고
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;

            // 나오는 Exception들을 캐치하고
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            logger.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            logger.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            logger.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            logger.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }
}

 

 


Repository

UserRepository(인터페이스)

public interface UserRepository extends JpaRepository<User, Long> {

    // 해당 쿼리 수행 될 때 Eager 조회로 authorities 정보를 같이 가져오게 된다.
    @EntityGraph(attributePaths = "authorities")
    Optional<User> findOneWithAuthoritiesByUsername(String username);
    //username을 기준으로 User 정보를 가져올 때 권한 정보도 같이 가져오게 된다.
}

 


Service

CustomUserDetailsService

package inflearn.freejwt.service;

import inflearn.freejwt.entity.User;
import inflearn.freejwt.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;

@Component("userDetailsService")
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // UserRepository를 사용하여 사용자 정보를 데이터베이스에서 검색
        return userRepository.findOneWithAuthoritiesByUsername(username)
                .map(user -> createUser(username, user))
                .orElseThrow(() -> new UsernameNotFoundException(username + " -> 데이터베이스에서 찾을 수 없습니다."));
    }

    // 사용자 정보를 기반으로 UserDetails 객체를 생성하는 메서드
    private org.springframework.security.core.userdetails.User createUser(String username, User user) {
        if (!user.isActivated()) { // 사용자가 활성화되지 않은 경우 예외 던짐
            throw new RuntimeException(username + " -> 활성화되어 있지 않습니다.");
        }

        // 사용자의 권한 정보를 가져와서 SimpleGrantedAuthority 객체로 변환
        List<GrantedAuthority> grantedAuthorities = user.getAuthorities()
                .stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getAuthorityName()))
                .collect(Collectors.toList());

        // UserDetails 인터페이스를 구현한 객체를 생성하여 반환
        return new org.springframework.security.core.userdetails.User(user.getUsername(),
                user.getPassword(), grantedAuthorities);
    }
}

 

 

UserService

package inflearn.freejwt.service;

import inflearn.freejwt.dto.UserDto;
import inflearn.freejwt.entity.Authority;
import inflearn.freejwt.entity.User;
import inflearn.freejwt.exception.DuplicateMemberException;
import inflearn.freejwt.repository.UserRepository;
import inflearn.freejwt.util.SecurityUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collections;

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;


    /**
     *
     * @param userDto
     * @return
     */
    @Transactional
    public UserDto signup(UserDto userDto) {
        if (userRepository.findOneWithAuthoritiesByUsername(userDto.getUsername()).orElse(null) != null) {
            throw new DuplicateMemberException("이미 가입되어 있는 유저입니다."); // 변경
        }
        // 권한 정보 생성
        Authority authority = Authority.builder()
                .authorityName("ROLE_USER")
                .build();

        // 유저 정보 생성
        User user = User.builder()
                .username(userDto.getUsername())
                .password(passwordEncoder.encode(userDto.getPassword()))
                .nickname(userDto.getNickname())
                .authorities(Collections.singleton(authority))
                .activated(true)
                .build();

        // 저장
        return UserDto.from(userRepository.save(user));
    }


    /**
     *
     * @param username
     * @return
     */
    @Transactional(readOnly = true)
    public UserDto getUserWithAuthorities(String username) {
        return UserDto.from(userRepository.findOneWithAuthoritiesByUsername(username).orElse(null));
    }

    /**
     *
     * @return
     */
    @Transactional(readOnly = true)
    public UserDto getMyUserWithAuthorities() {
        return UserDto.from(SecurityUtil.getCurrentUsername().flatMap(userRepository::findOneWithAuthoritiesByUsername).orElse(null));
    }

}

 


Util

SecurityUtil

package inflearn.freejwt.util;

import lombok.NoArgsConstructor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Optional;

@NoArgsConstructor
public class SecurityUtil {
    private static final Logger logger = LoggerFactory.getLogger(SecurityUtil.class);

    /**
     * 이 메서드의 역할은 Security Context의 Authentication 객체를 이용해 username을 리턴해주는 간단한 유틸성 메서드
     *
     * @return 현재 사용자의 username을 Optional<String> 형태로 반환. 인증 정보가 없으면 Optional.empty() 반환.
     */
    public static Optional<String> getCurrentUsername() {
        // 현재의 Authentication 객체를 가져온다.
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        // 인증 정보가 없는 경우
        if (authentication == null) {
            logger.debug("Security Context에 인증 정보가 없습니다.");
            return Optional.empty(); // 빈 Optional 반환
        }

        String username = null;

        // Authentication 객체의 주체(principal)를 확인
        if (authentication.getPrincipal() instanceof UserDetails) {
            UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
            username = springSecurityUser.getUsername(); // UserDetails에서 username을 가져옴
        }
        else if (authentication.getPrincipal() instanceof String) {
            username = (String) authentication.getPrincipal(); // 문자열로 된 주체의 경우, username으로 설정
        }

        return Optional.ofNullable(username); // Optional로 username 반환 (null인 경우에도 처리)
    }
}

 

728x90
Comments