spring boot

JWT를 이용한 인증-III(feat. react, spring boot, spring security)

hoazzinews 2024. 12. 28. 00:18

[특별 30% 쿠폰할인!!] 스프링부트를 이용한 웹 프로그래밍: 웹사이트 이렇게 만드는 거예요!

 

블스님이 선물하는 할인쿠폰

스프링부트를 이용한 웹 프로그래밍: 웹사이트 이렇게 만드는 거예요!

www.inflearn.com

 

이번 시간에는 지난 시간에 이어서 spring security를 적용하겠습니다.

 

1. filter 패키지 생성

 

2. LoginRequest 클래스 생성

package com.office.jwtex.jwt.filter;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class LoginRequest {
	
	private String id;
    private String pw;
    
}

 

3. UsernamePasswordAuthenticationFilter를 상속한 JwtUsernamePasswordAuthenticationFilter 클래스 생성

package com.office.jwtex.jwt.filter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.office.jwtex.jwt.JwtService;
import com.office.jwtex.jwt.token.TokenGenerateAndSaveResult;
import com.office.jwtex.jwt.token.TokenResponse;
import com.office.jwtex.member.MemberConstant;
import com.office.jwtex.member.MemberDto;
import com.office.jwtex.member.MemberMapper;
import com.office.jwtex.member.response.SignInResponse;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class JwtUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

	private final AuthenticationManager authenticationManager;
	private final JwtService jwtService;
	private final MemberMapper memberMapper;
	
	public JwtUsernamePasswordAuthenticationFilter(
			AuthenticationManager authenticationManager, 
			JwtService jwtService, 
			MemberMapper memberMapper) {
		this.authenticationManager = authenticationManager;
		this.jwtService = jwtService;
		this.memberMapper = memberMapper;
		
	}
	
	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		log.info("attemptAuthentication()");
		
		try {
			// JSON 요청에서 username과 password를 추출
            ObjectMapper objectMapper = new ObjectMapper();
            LoginRequest loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequest.class);
            
            UsernamePasswordAuthenticationToken authenticationToken = 
    				new UsernamePasswordAuthenticationToken(loginRequest.getId(), loginRequest.getPw());
                
            return authenticationManager.authenticate(authenticationToken);
                
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException("Failed to parse authentication request", e);
			
		}
		
	}
	
	@Override
	protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
			Authentication authResult) throws IOException, ServletException {
		log.info("successfulAuthentication()");
		
		// 인증 성공 시 사용자 정보를 가져옴
        UserDetails userDetails = (UserDetails) authResult.getPrincipal();
        String username = userDetails.getUsername();
        MemberDto memberDto = memberMapper.selectMemberById(username);
        
        // JWT 생성 및 저장
        if (memberDto != null) {
        	
        	TokenGenerateAndSaveResult tokenGenerateAndSaveResult = jwtService.generateTokenAndSave(memberDto);
        	
        	// 쿠키 설정
            response.addHeader(HttpHeaders.SET_COOKIE, tokenGenerateAndSaveResult.getResponseCookie().toString());
        	
            // 응답 데이터 작성
            Map<String, Object> responseData = new HashMap<>();
            responseData.put("signInResponse", SignInResponse.builder()
                    .isSuccess(true)
                    .message(MemberConstant.SIGNIN_SUCCESS)
                    .userId(username)
                    .build());
            
            responseData.put("tokenResponse", TokenResponse.builder()
                    .accessToken(tokenGenerateAndSaveResult.getAccessToken())
                    .refreshToken(tokenGenerateAndSaveResult.getRefreshToken())
                    .build());
            
            // 응답 헤더 설정
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_OK);
            
            // JSON 변환 및 출력
            ObjectMapper objectMapper = new ObjectMapper();
            response.getWriter().write(objectMapper.writeValueAsString(responseData));
        	
        }
		
	}
	
	@Override
	protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException failed) throws IOException, ServletException {
		log.info("unsuccessfulAuthentication()");
		
		response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
		response.getWriter().write(new ObjectMapper().writeValueAsString("Authentication failed: " + failed.getMessage()));
	}
	
}

 

 

4. UserDetailsService 인터페이스를 구현한 CustomUserDetailsService 클래스 생성

package com.office.jwtex.jwt.filter;

import org.springframework.security.core.userdetails.User;
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.Service;

import com.office.jwtex.member.MemberDto;
import com.office.jwtex.member.MemberMapper;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class CustomUserDetailsService implements UserDetailsService {

	private final MemberMapper memberMapper;
	
	public CustomUserDetailsService(MemberMapper memberMapper) {
		this.memberMapper = memberMapper;
		
	}

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		log.info("loadUserByUsername()");
		
		MemberDto memberDto = memberMapper.selectMemberById(username);
		if (memberDto != null)
			return User.builder()
					.username(memberDto.getId())
					.password(memberDto.getPw())
					.roles("USER")
					.build();
		
		return null;
		
	}

}

 

5. OncePerRequestFilter 추상클래스를 상속한 JwtAuthenticationFilter 클래스 생성

package com.office.jwtex.jwt.filter;

import java.io.IOException;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;

import com.office.jwtex.jwt.JwtUtil;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

	private final JwtUtil jwtUtil;
    private final CustomUserDetailsService customUserDetailsService;
	
    public JwtAuthenticationFilter(
    		JwtUtil jwtUtil, 
    		CustomUserDetailsService customUserDetailsService) {
        this.jwtUtil = jwtUtil;
        this.customUserDetailsService = customUserDetailsService;
        
    }
    
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		log.info("doFilterInternal()");
		
		String authorizationHeader = request.getHeader("Authorization");
        String jwtToken = null;
        
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer "))
            jwtToken = authorizationHeader.substring(7); // Remove "Bearer "
        
        if (jwtToken != null && SecurityContextHolder.getContext().getAuthentication() == null) {
        	if (jwtUtil.validateToken(jwtToken)) {
        		String username = jwtUtil.getUsername(jwtToken);
        		
        		UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
        		
        		if (username.equals(userDetails.getUsername())) {
        			UsernamePasswordAuthenticationToken authenticationToken = 
        					new UsernamePasswordAuthenticationToken(
        							customUserDetailsService, 
        							null, 
        							userDetails.getAuthorities());
        			
        			SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        			
        		}
        		
        	} else {
        		log.info("Invalid JWT token");
        		
        	}
        	
        }
        filterChain.doFilter(request, response);
		
	}

}

 

6. JwtService 클래스에 JwtUtil getter 추가

public JwtUtil getJwtUtil() {
	return jwtUtil;
	
}

 

7. SecurityConfig에 JwtUsernamePasswordAuthenticationFilter와 JwtAuthenticationFilter 필터 추가

package com.office.jwtex.config;

import java.util.Arrays;
import java.util.Collections;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import com.office.jwtex.jwt.JwtService;
import com.office.jwtex.jwt.filter.CustomUserDetailsService;
import com.office.jwtex.jwt.filter.JwtAuthenticationFilter;
import com.office.jwtex.jwt.filter.JwtUsernamePasswordAuthenticationFilter;
import com.office.jwtex.member.MemberMapper;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	private final AuthenticationConfiguration authenticationConfiguration;
	private final JwtService jwtService;
	private final CustomUserDetailsService customUserDetailsService;
	private final MemberMapper memberMapper;
	
	public SecurityConfig(
			AuthenticationConfiguration authenticationConfiguration, 
			JwtService jwtService, 
			CustomUserDetailsService customUserDetailsService, 
			MemberMapper memberMapper) {
		this.authenticationConfiguration = authenticationConfiguration;
		this.jwtService = jwtService;
		this.customUserDetailsService = customUserDetailsService;
		this.memberMapper = memberMapper;
		
	}
	
	@Bean AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
		return configuration.getAuthenticationManager();
		
	}
	
	@Bean PasswordEncoder passwordEncoder() {
		log.info("passwordEncoder()");
		
		return new BCryptPasswordEncoder();
		
	}
	
@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		
		http
		.cors(cors -> cors
				.configurationSource(new CorsConfigurationSource() {
					
					@Override
					public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
						
						CorsConfiguration corsConfiguration = new CorsConfiguration();
						
						corsConfiguration.setAllowedOrigins(Arrays.asList(
								"http://localhost:3000", 
								"http://localhost:3001", 
								"http://localhost:3002"));
						corsConfiguration.setAllowedMethods(Arrays.asList(
								"GET", 
								"POST", 
								"PUT", 
								"DELETE"));
						corsConfiguration.setAllowCredentials(true);
						corsConfiguration.setAllowedHeaders(Collections.singletonList("*"));
						corsConfiguration.setMaxAge(3600L);
						corsConfiguration.setExposedHeaders(Collections.singletonList("Authorization"));
						
						return corsConfiguration;
						
					}
				}))
		.csrf(csrf -> csrf
				.disable());
		
		http
		.formLogin(auth -> auth
				.disable())
		.httpBasic(auth -> auth
				.disable());
		
		http
		.authorizeHttpRequests(auth -> auth
				.requestMatchers(
						"/", 
						"/member/signup", 
						"/member/signout").permitAll()
				.anyRequest().authenticated());
		
		
		http
		.addFilterBefore(new JwtAuthenticationFilter(jwtService.getJwtUtil(), customUserDetailsService), JwtUsernamePasswordAuthenticationFilter.class);
		
		JwtUsernamePasswordAuthenticationFilter jwtUsernamePasswordAuthenticationFilter = 
				new JwtUsernamePasswordAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtService, memberMapper);
		jwtUsernamePasswordAuthenticationFilter.setFilterProcessesUrl("/member/signin");
		
		http
		.addFilterAt(jwtUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
		
		http
		.sessionManagement(auth -> auth
				.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
		
		return http.build();
		
	}
	
}

 

이번 시간에는 인증에 사용되는 JWT 필터를 만들고 Spring Security에 적용했습니다. 

jwtex.zip
0.14MB