Spring

SpringSecurity 프로젝트 적용

쭈녁 2024. 3. 3. 01:03

SecurityConfig

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtUtil jwtUtil;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtFilter jwtFilter) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(request ->
                        request
                                .requestMatchers(jwtUtil.allowedUrls).permitAll()
                                .anyRequest().authenticated())
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // session을 사용하지 않음
                )
                .addFilterBefore(jwtFilter, RequestCacheAwareFilter.class)
                .build();
    }
}

 

csrf : 

Cross-Site Request Forgery의 약어

"사이트 간 요청 위조"를 의미

공격자가 사용자가 로그인한 웹사이트를 통해 사용자가 모르는 상황에서 원치 않는 동작을 수행하게 하는 보안 취약점에 대한 보안 설정

 

authorizeRequests : 요청에 의한 보안검사 설정

  • .requestMatchers(jwtUtil.allowedUrls).permitAll() // 특정 url이 매칭되면  검사하지 않고 넘긴다.
  • .anyRequest().authenticated() / /어떤 요청에도 보안검사를 한다.

.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) : 

// session을 사용하지 않음

 

.addFilterBefore(jwtFilter, RequestCacheAwareFilter.class) : RequestCacheAwareFilter 전에 jwtFilter 적용

 

@RequiredArgsConstructor
@Component
public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    private final JwtProvider jwtProvider;

    private AuthenticationManager authenticationManager;

    @PostConstruct
    public void init() {
    	// 인증 객체를 생성하는 provider 를 주입하는 메서드
        this.authenticationManager = new ProviderManager(jwtProvider);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        //해당 URL에 속하면 필터를 수행하지 않음
        return Arrays.stream(jwtUtil.allowedUrls).anyMatch(item -> item.equalsIgnoreCase(request.getServletPath())); // true면 fileter 안 탐
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            // Jwt 토큰을 가져오는 로직
            String token = jwtUtil.extractJwtFromRequest(request);
			// 토큰이 존재하고 토큰의 유효성이 통과되면 수행하는 로직
            if (token != null && jwtUtil.validateToken(token)) {
            	//인증 객체를 생성
                MyTokenAuthentication authenticateRequest = new MyTokenAuthentication(token);
                Authentication authenticationResult = authenticationManager.authenticate(authenticateRequest);

                if (!authenticateRequest.isAuthenticated()) {
                    throw new BadCredentialsException("인증이 실패하였습니다.");
                }
                //context에 인증 객체 넣는 로직
                SecurityContextHolder.getContext().setAuthentication(authenticationResult);
            }
		//예외가 발생했을때의 코드
        } catch (ExpiredJwtException e) {
            SecurityContextHolder.clearContext();

            response.sendError(401, e.getMessage());
            // void 메서드 임으로 예외가 발생하면 return
            return;
        }catch (Exception e) {
            SecurityContextHolder.clearContext();

            response.setStatus(400);
            // void 메서드 임으로 예외가 발생하면 return
            return;
        }

		//로직을 예외 없이 통과했으면 다음 필터로 넘어가는 메서드 호출
        filterChain.doFilter(request, response);
    }
}

 

 

@Slf4j
@RequiredArgsConstructor
@Component
public class JwtProvider implements AuthenticationProvider {
    private final JwtUtil tokenService;

	//인증 객체를 생겅하는 로직
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        log.debug("토큰 인증을 수행합니다.");
        if (supports(authentication.getClass())) {
            MyTokenPayload verified = tokenService.verify(((MyTokenAuthentication) authentication).getToken());
            authentication.setAuthenticated(true);
            ((MyTokenAuthentication) authentication).setPayload(verified);
            return authentication;
        } else {
            throw new BadCredentialsException("지원하지 않는 인증입니다.");
        }
    }

	//특정 Authentication 객체를 지원하는지 확인하는 로직
    @Override
    public boolean supports(Class<?> authentication) {
        return MyTokenAuthentication.class.equals(authentication);
    }
}

 

@Service
@RequiredArgsConstructor
public class JwtUtil {
	// 비밀 키 값
    @Value("${jwt.secret}")
    private String secret;
	
    // 토큰의 만료 기간
    @Value("${jwt.expiration}")
    private long expiration;
    
    private SecretKey secretKey;
    
    // 필터 및 security 를 태우지 않을 URL 의 배열
    public final String[] allowedUrls = {
            "/v2/api-docs",
            "/swagger-resources",
            "/swagger-resources/**",
            "/configuration/ui",
            "/configuration/security",
            "/swagger-ui.html",
            "/webjars/**",
            /* swagger v3 */
            "/v3/api-docs/**",
            "/swagger-ui/**",
            "/",
            "/user/**",
            "/api/first_categories/**",
            "/error"
    };


	//비밀 키 값을 SecretKey 객체로 만드는 메서드
    @PostConstruct
    public void init() {
        this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
    }

	//토큰으르 발급하는 로직
    public String generate(MyTokenPayload payload) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);

        return Jwts.builder()
                .claim("id", payload.getUserId())
                .claim("username", payload.getUsername())
                .claim("roles", payload.getRoles())
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(secretKey)
                .compact();
    }

	// 토큰을 열어보고 유저의 정보를 담는 로직
    public MyTokenPayload verify(String token) throws BadCredentialsException {
        try {
            Claims payload = Jwts.parserBuilder()
                    .setSigningKey(secret.getBytes(StandardCharsets.UTF_8))
                    .build()
                    .parseClaimsJws(token)
                    .getBody();

            Long id = payload.get("id", Long.class);
            String username = payload.get("username", String.class);
            List<String> roles = (List<String>) payload.get("roles", List.class);

            MyTokenPayload myTokenPayload = new MyTokenPayload(id, username, roles);
            return myTokenPayload;
        } catch (Exception e) {
            e.printStackTrace();
            throw new BadCredentialsException("올바르지 않은 토큰입니다.");
        }
    }

	// 토큰을 열어보고 username을 가져오는 로직
    public String extractUsername(String token) {
        return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().getSubject();
    }

	// 토큰의 유효성을 검증하는 로직
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
            return true;
        } catch (ExpiredJwtException e) {
            throw e;
        } catch (Exception e) {
            return false;
        }
    }
	
    //헤더에서 토큰을 빼오는 로직
    public String extractJwtFromRequest(HttpServletRequest request) {
        // Extract JWT from the Authorization header
        String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Bearer ")) {
            return header.substring(7);
        }

        return null;
    }
}

 

@Getter
@RequiredArgsConstructor
//인증 객체
public class MyTokenAuthentication implements Authentication {
	//토큰
    private final String token;
    
    //유저 정보를 담아놓은 payload
    @Setter
    private MyTokenPayload payload = new MyTokenPayload();
    
    //인증을 받았는지 여부
    @Setter
    private boolean authenticated = false;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return payload.getRoles().stream()
                .map(SimpleGrantedAuthority::new)
                .toList();
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public MyTokenPayload getDetails() {
        return payload;
    }

    @Override
    public Long getPrincipal() {
        return payload.getUserId();
    }

    @Override
    public String getName() {
        return payload.getUsername();
    }

}

 

 

'Spring' 카테고리의 다른 글

Security 인가(Authorization) 적용  (1) 2024.03.16
Mappstruct 사용  (0) 2024.03.11
SpringSecurity 인증 방식  (0) 2024.03.02
SpringSecurity 인증  (0) 2024.03.01
SpringSecurity 아키텍쳐이해하기  (0) 2024.02.24