본문 바로가기

프로젝트/booktalk(책 중고 거래 서비스)

jwt 토큰 인증 인가

JwtUtil

@Slf4j
@Component
public class JwtUtil {

    // JWT 토큰에 사용될 헤더 이름 정의
    public static final String ACCESS_TOKEN_HEADER = "AccessToken";
    public static final String REFRESH_TOKEN_HEADER = "RefreshToken";
    public static final String AUTHORIZATION_KEY = "auth";
    public static final String BEARER_PREFIX = "Bearer ";

    // 액세스 토큰 및 리프레시 토큰의 유효 기간 설정
    public final long ACCESS_TOKEN_TIME = 60 * 1000 * 60;
    public final long REFRESH_TOKEN_TIME = 60 * 60 * 1000L * 24 * 3;

    // JWT 서명 알고리즘 설정
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    // 애플리케이션 프로퍼티에서 비밀 키 값 가져오기
    @Value("${jwt.secret.key}")
    private String secretKey;

    // 비밀 키 객체 초기화
    private Key key;

    // Bean 초기화 시 비밀 키를 Base64 디코딩하여 키 객체로 설정
    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // 토큰 유효성 검사 메서드
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.error("Invalid JWT signature");
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token");
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token");
        } catch (IllegalArgumentException e) {
            log.error("JWT claims is empty");
        }
        return false;
    }

    // 토큰에서 사용자 정보 추출 메서드
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

    // 액세스 토큰 생성 메서드
    public String createAccessToken(String email, UserRoleType role) {
        Date date = new Date();

        return BEARER_PREFIX +
            Jwts.builder()
                .setSubject(email)
                .claim(AUTHORIZATION_KEY, role)
                .setExpiration(new Date(date.getTime() + ACCESS_TOKEN_TIME))
                .setIssuedAt(date)
                .signWith(key, signatureAlgorithm)
                .compact();
    }
    
    public String createAccessToken(String email, UserRoleType role) {
    // 현재 시간을 가져옵니다.
    Date currentDate = new Date();

    // JWT 토큰의 헤더에 사용할 프리픽스를 설정합니다.
    String tokenPrefix = BEARER_PREFIX;

    // JWT 토큰의 페이로드를 구성합니다.
    JwtBuilder jwtBuilder = Jwts.builder()
            .setSubject(email) // 토큰의 주제로 사용자의 이메일을 설정합니다.
            .claim(AUTHORIZATION_KEY, role) // 사용자의 역할을 클레임으로 설정합니다.
            .setExpiration(new Date(currentDate.getTime() + ACCESS_TOKEN_TIME)) // 토큰의 만료 시간을 설정합니다.
            .setIssuedAt(currentDate); // 토큰의 발급 시간을 현재 시간으로 설정합니다.
			.setIssuedAt(date)// 서명 알고리즘과 서명에 사용할 키를 이용하여 토큰에 서명을 추가합니다.
			.compact();
	}

    // 리프레시 토큰 생성 메서드
    public String createRefreshToken(String email) {
        Date date = new Date();

        return BEARER_PREFIX +
            Jwts.builder()
                .setSubject(email)
                .setExpiration(new Date(date.getTime() + REFRESH_TOKEN_TIME))
                .setIssuedAt(date)
                .signWith(key, signatureAlgorithm)
                .compact();
    }

    // 액세스 토큰을 쿠키에 추가하는 메서드
    public void addAccessJwtToCookie(String token, HttpServletResponse res) {
        token = URLEncoder.encode(token, StandardCharsets.UTF_8)
            .replaceAll("\\+", "%20"); // 공백 처리를 위해 인코딩

        Cookie cookie = new Cookie(ACCESS_TOKEN_HEADER, token); // 쿠키 객체 생성
        cookie.setPath("/"); // 쿠키 경로 설정
        cookie.setMaxAge((int) ACCESS_TOKEN_TIME); // 쿠키 유효 기간 설정

        // 응답에 쿠키 추가
        res.addCookie(cookie);
    }

    // 리프레시 토큰을 쿠키에 추가하는 메서드
    public void addRefreshJwtToCookie(String token, HttpServletResponse res) {
        token = URLEncoder.encode(token, StandardCharsets.UTF_8)
            .replaceAll("\\+", "%20"); // 공백 처리를 위해 인코딩

        Cookie cookie = new Cookie(REFRESH_TOKEN_HEADER, token); // 쿠키 객체 생성
        cookie.setPath("/"); // 쿠키 경로 설정
        cookie.setMaxAge((int) REFRESH_TOKEN_TIME); // 쿠키 유효 기간 설정

        // 응답에 쿠키 추가
        res.addCookie(cookie);
    }

    // 요청에서 토큰을 추출하는 메서드
    public String getTokenFromRequest(HttpServletRequest req, String header) {
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals(header)) {
                    return URLDecoder.decode(cookie.getValue().replaceAll("Bearer%20", ""),
                        StandardCharsets.UTF_8); // 디코딩
                }
            }
        }
        return null;
    }

}

 

 

 

RefreshToken

@Service
@RequiredArgsConstructor
public class RefreshTokenService {

    private final RefreshTokenRepository refreshTokenRepository; // 새로 고침 토큰 저장소

    // 새로운 새로 고침 토큰을 저장하는 메서드
    public RefreshToken saveRefreshToken(String refreshToken, Long userId) {
        // 새로운 RefreshToken 엔티티를 생성합니다.
        RefreshToken refreshTokenEntity = RefreshToken.builder()
            .userId(userId) // 사용자 ID 설정
            .refreshToken(refreshToken) // 새로 고침 토큰 설정
            .build();

        // 새로 고침 토큰을 저장소에 저장합니다.
        refreshTokenRepository.save(refreshTokenEntity);

        // 저장된 RefreshToken 엔티티를 반환합니다.
        return refreshTokenEntity;
    }
}

 

 

webSecurityConfig

@Slf4j
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    // JwtUtil, UserDetailsService, ObjectMapper, RefreshTokenRepository 주입
    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;
    private final ObjectMapper objectMapper;
    private final RefreshTokenRepository refreshTokenRepository;

    // 비밀번호 암호화를 위한 Bean
    // Spring 애플리케이션에서 사용되는 비밀번호 암호화를 위해 BCrypt 알고리즘을 사용하는 PasswordEncoder
    //빈을 생성하는 메서드를 정의하고 있습니다.
    //이 빈은 애플리케이션 내에서 사용자의 비밀번호를 안전하게 저장하고 비교하는 데 사용
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // AuthenticationManager를 Bean으로 설정
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    // JwtAuthorizationFilter를 Bean으로 설정
    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService, objectMapper, refreshTokenRepository);
    }

    // HttpSecurity를 설정한 SecurityFilterChain을 Bean으로 설정
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 보안 설정 해제
        http.csrf((csrf) -> csrf.disable());

        // 세션 관리 정책 설정
        http.sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        // 요청에 대한 인가 정책 설정
        http.authorizeHttpRequests((authorizeHttpRequests) ->
            authorizeHttpRequests
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // 정적 자원에 대한 요청 허용
                .requestMatchers("/booktalk").permitAll() // "/booktalk" 경로에 대한 요청 허용
                .requestMatchers(HttpMethod.POST, "/api/v2/users/**").permitAll() // "/api/v2/users/**"에 대한 POST 요청 허용
                .requestMatchers("/api/v2/users/kakao/**").permitAll() // "/api/v2/users/kakao/**"에 대한 요청 허용
                .requestMatchers("/api/v2/images/**").permitAll() // "/api/v2/images/**"에 대한 요청 허용
                .requestMatchers(HttpMethod.GET, "/booktalk").permitAll() // 메인페이지에 대한 GET 요청 허용
                .requestMatchers(HttpMethod.GET, "/api/v2/products/main").permitAll() // 메인페이지에서 인가상품Top3 반환에 대한 GET 요청 허용
                .requestMatchers(HttpMethod.GET, "/booktalk/products/list").permitAll() // 상품목록페이지에 대한 GET 요청 허용
                .requestMatchers(HttpMethod.GET, "/api/v2/products/**").permitAll() // 상품조회에 대한 GET 요청 허용
                .requestMatchers(HttpMethod.GET, "/booktalk/products/detail/**").permitAll() // 상품단건조회페이지에 대한 GET 요청 허용
                .requestMatchers("/booktalk/admin/**").hasRole("ADMIN") // "/booktalk/admin/**"에 대한 요청은 ADMIN 역할을 가진 사용자에게만 허용
                .requestMatchers("/api/v2/admin/**").hasRole("ADMIN") // "/api/v2/admin/**"에 대한 요청은 ADMIN 역할을 가진 사용자에게만 허용
                .requestMatchers("/booktalk/users/signup").permitAll() // 회원가입에 대한 요청 허용
                .requestMatchers("/booktalk/users/login").permitAll() // 로그인에 대한 요청 허용
                .requestMatchers("/getKakaoLoginUrl").permitAll() // Kakao 로그인 URL 요청 허용

                .anyRequest().authenticated() // 그 외 요청은 인증이 필요함
        );

        // 로그아웃 설정
        http.logout(logout -> logout
            .logoutUrl("/api/v2/users/logout") // 로그아웃 URL 설정
            .logoutSuccessUrl("/api/v2") // 로그아웃 성공 후 이동할 URL 설정
            .logoutSuccessHandler((request, response, authentication) -> {
                // 로그아웃 성공 시 권한 정보를 clear
                SecurityContextHolder.clearContext();

                // 추가적인 로그아웃 처리 로직을 여기에 추가할 수 있음
                log.info("로그아웃 성공. /api/v1으로 리다이렉트합니다.");
                
                // 응답 전송
                response.setStatus(HttpStatus.OK.value());
                response.setContentType("application/json; charset=UTF-8");
                response.getWriter().write(objectMapper.writeValueAsString(new UserLogoutRes("로그아웃 완료")));
            })
            .deleteCookies("AccessToken", "RefreshToken") // AccessToken과 RefreshToken 쿠키 삭제
        );

        // 필터 추가
        http.addFilterBefore(jwtAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class); // UsernamePasswordAuthenticationFilter 앞에 jwtAuthorizationFilter 추가

        return http.build();
    }
}