반응형

Spring Boot에서 예외 처리 및 전역 예외 핸들러를 구현하는 코드를 통해, 다양한 예외 상황에서 일관된 응답을 제공하는 방법을 학습할 수 있습니다. 

@ControllerAdvice를 사용하여 전역 예외 처리기를 구현하고, 다양한 예외에 대해 적절한 응답을 반환하는 방법을 살펴보겠습니다.

 

1. 기본 예외 처리 설정

 

Spring Boot에서는 기본적으로 예외가 발생하면 HTTP 상태 코드와 함께 오류 메시지를 반환합니다. 하지만 이 기본 방식은 사용자 경험(UX)을 향상시키기 위해 커스터마이징이 필요할 수 있습니다.

 

2. 전역 예외 처리 클래스 생성

 

@ControllerAdvice를 사용하여 전역 예외 처리기를 생성합니다. 이 클래스는 애플리케이션 전반에서 발생하는 예외를 한 곳에서 처리하고, 일관된 방식으로 응답을 반환할 수 있게 합니다.

 

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
@RestController
public class GlobalExceptionHandler {

    // 1. 기본적인 예외 처리
    @ExceptionHandler(Exception.class)
    public final ResponseEntity<Object> handleAllExceptions(Exception ex, WebRequest request) {
        Map<String, Object> response = new HashMap<>();
        response.put("message", ex.getMessage());
        response.put("details", request.getDescription(false));
        return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    // 2. 특정 예외 처리 (예: NullPointerException)
    @ExceptionHandler(NullPointerException.class)
    public final ResponseEntity<Object> handleNullPointerException(NullPointerException ex, WebRequest request) {
        Map<String, Object> response = new HashMap<>();
        response.put("message", "Null value encountered!");
        response.put("details", request.getDescription(false));
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }

    // 3. 유효성 검사 실패 예외 처리 (예: MethodArgumentNotValidException)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public final Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> 
            errors.put(error.getField(), error.getDefaultMessage()));
        return errors;
    }

    // 4. 사용자 정의 예외 처리
    @ExceptionHandler(ResourceNotFoundException.class)
    public final ResponseEntity<Object> handleResourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        Map<String, Object> response = new HashMap<>();
        response.put("message", ex.getMessage());
        response.put("details", request.getDescription(false));
        return new ResponseEntity<>(response, HttpStatus.NOT_FOUND);
    }
}

 

3. 사용자 정의 예외 클래스 생성

 

필요에 따라 사용자 정의 예외를 만들어 사용할 수 있습니다. 예를 들어, 리소스를 찾을 수 없을 때 발생시키는 예외를 만들 수 있습니다.

src/main/java/com/example/demo/exception/ResourceNotFoundException.java

package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }
}

어노테이션을 사용하는 경우:

예외 클래스에 @ResponseStatus 어노테이션을 추가하면, 해당 예외가 발생할 때 Spring이 자동으로 지정된 HTTP 상태 코드를 반환합니다.

예를 들어, @ResponseStatus(HttpStatus.NOT_FOUND)를 사용하면, 이 예외가 발생할 때 자동으로 404 Not Found 상태 코드가 반환됩니다.

어노테이션을 사용하지 않는 경우:

@ResponseStatus 어노테이션을 사용하지 않으면, 예외 처리 클래스에서 ResponseEntity를 사용해 상태 코드를 명시적으로 설정할 수 있습니다. 이렇게 하면 예외 상황에 대해 더 유연한 처리가 가능합니다.

 

 

4. 예외 발생 시나리오

 

다음은 리소스를 찾지 못했을 때 ResourceNotFoundException을 발생시키는 서비스 코드의 예입니다.

 

src/main/java/com/example/demo/service/PostService.java

package com.example.demo.service;

import com.example.demo.entity.Post;
import com.example.demo.exception.ResourceNotFoundException;
import com.example.demo.repository.PostRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class PostService {

    @Autowired
    private PostRepository postRepository;

    public Post getPostById(Long id) {
        Optional<Post> post = postRepository.findById(id);
        if (!post.isPresent()) {
            throw new ResourceNotFoundException("Post not found with id: " + id);
        }
        return post.get();
    }

    // 기타 CRUD 메서드들...
}

5. 컨트롤러에서의 사용 예시

컨트롤러에서 서비스 메서드를 호출할 때, 예외가 발생하면 GlobalExceptionHandler가 이를 처리하여 일관된 응답을 반환합니다.

 

src/main/java/com/example/demo/controller/PostController.java

package com.example.demo.controller;

import com.example.demo.entity.Post;
import com.example.demo.service.PostService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/posts")
public class PostController {

    @Autowired
    private PostService postService;

    // GET /api/posts/{id} - Get a post by id
    @GetMapping("/{id}")
    public ResponseEntity<Post> getPostById(@PathVariable Long id) {
        Post post = postService.getPostById(id);
        return ResponseEntity.ok(post);
    }

    // 기타 CRUD 엔드포인트들...
}

 

반응형
반응형

스프링 시큐리티 및 JWT 를 활용하기 위해 결국 유저정보를 저장하는 DB 가 있어야 합니다.


아래 글과 겹치는 부분 있을 수 있으니 먼저 확인해보세요. 아래 포스트는 디비 없이 하드코딩으로 user 와 password 를 설정했습니다.
https://juntcom.tistory.com/249 

 

spring security 와 jwt 를 이용한 인증 인가(spring boot3)

• Spring Security는 Spring 기반 애플리케이션의 보안을 제공하는 강력하고, 유연한 프레임워크입니다.특징  • 인증(Authentication)과 인가(Authorization) 기능 제공 • 다양한 보안 공격 방어 (CSRF, XSS 등)

juntcom.tistory.com

 

회원 테이블

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password VARCHAR(100) NOT NULL,
    enabled BOOLEAN NOT NULL,
    authority VARCHAR(50) NOT NULL
);

user 테이블과

authority 권한 테이블을 정규화 해서 나눌 수도 있지만 한 유저당 여러권한이 없다고 보는게 낫고 예시를 위해 한 테이블로 진행하려고 합니다. 

 

Entity Class

@Setter
@Getter
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private boolean enabled;

    @Column(nullable = false)
    private String authority;

}

 

 

CustomUserDetailsService 

UserDetailsService 를 implements 해야 합니다. 

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }

        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
                user.isEnabled(), true, true, true,
                Collections.singletonList(new SimpleGrantedAuthority(user.getAuthority())));
    }
}

UserRepository

package com.junt.studybasic.repository;

import com.junt.studybasic.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username);
}

 

Security 

package com.junt.studybasic.config;

import com.junt.studybasic.filter.JwtRequestFilter;
import com.junt.studybasic.service.CustomUserDetailsService;
import com.junt.studybasic.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
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.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
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;

@Configuration
public class SecurityConfig {

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(authorizeRequests ->
                authorizeRequests
                    .requestMatchers("/api/auth/**").permitAll()
                    .anyRequest().authenticated()
            )
            .sessionManagement(sessionManagement ->
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

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

 

Login Controller 

package com.junt.studybasic.controller;

import com.junt.studybasic.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.bind.annotation.*;

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

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/login")
    public String createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
        );

        final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
        final String jwt = jwtUtil.generateToken(userDetails);

        return jwt;
    }
}

class AuthRequest {
    private String username;
    private String password;

    // Getters and Setters
}

 

 

회원가입 API

UserService

package com.junt.studybasic.service;

import com.junt.studybasic.entity.User;
import com.junt.studybasic.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public User registerNewUser(String username, String password, String authority) {
        User user = new User();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(password));
        user.setEnabled(true);
        user.setAuthority(authority);
        return userRepository.save(user);
    }
}

 

AuthController

 

package com.junt.studybasic.controller;

import com.junt.studybasic.entity.User;
import com.junt.studybasic.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

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

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public User registerUser(@RequestBody RegisterRequest registerRequest) {
        return userService.registerNewUser(
                registerRequest.getUsername(),
                registerRequest.getPassword(),
                registerRequest.getAuthority()
        );
    }
}

@Getter
@Setter
class RegisterRequest {
    private String username;
    private String password;
    private String authority;

}

 

 

JwtUtil

@Component
public class JwtUtil {

    private final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(SECRET_KEY)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    public String generateToken(UserDetails userDetails) {
        return createToken(userDetails.getUsername());
    }

    private String createToken(String subject) {
        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10시간 유효
                .signWith(SECRET_KEY)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

 

JwtRequestFilter

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails)) {

                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

 

참고

유저 권한에 따라 코드를 다르게 실행하는 방법

1. 메서드 수준의 보안 애노테이션 사용

 

Spring Security는 메서드 수준에서 접근 제어를 지원하는 몇 가지 애노테이션을 제공합니다. 가장 일반적인 애노테이션은 @PreAuthorize@Secured입니다.

1.1 @PreAuthorize 애노테이션 사용

 

@PreAuthorize 애노테이션을 사용하면 메서드 호출 전에 표현식을 평가하여 접근을 제어할 수 있습니다. 이를 위해 @EnableGlobalMethodSecurity(prePostEnabled = true)를 설정해야 합니다.

package com.junt.studybasic.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
    // 추가 설정이 필요 없다면 이 클래스는 비어 있어도 됩니다.
}

서비스 클래스 예시

package com.junt.studybasic.service;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class MyService {

    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public void adminOnlyMethod() {
        // 관리자 전용 메서드 로직
    }

    @PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')")
    public void userOrAdminMethod() {
        // 사용자 또는 관리자 접근 가능 메서드 로직
    }
}

 

2. Spring Security 권한 검사 API 사용

 

Spring Security의 권한 검사 API를 사용하여 프로그램 내에서 동적으로 권한을 검사할 수도 있습니다.

 

2.1 SecurityContextHolder를 사용한 권한 검사

@Service
public class MyService {

    public void performActionBasedOnRole() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
            // 관리자 전용 로직
        } else if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_USER"))) {
            // 사용자 전용 로직
        } else {
            // 기타 권한 로직
        }
    }
}

 

3. 컨트롤러에서 권한에 따라 다른 로직 수행

 

Spring MVC 컨트롤러에서도 @PreAuthorize를 사용할 수 있습니다.

 

3.1 컨트롤러에서의 예시

package com.junt.studybasic.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

    @GetMapping("/admin")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String adminEndpoint() {
        return "관리자 전용 페이지";
    }

    @GetMapping("/user")
    @PreAuthorize("hasRole('ROLE_USER')")
    public String userEndpoint() {
        return "사용자 전용 페이지";
    }

    @GetMapping("/common")
    @PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')")
    public String commonEndpoint() {
        return "모든 사용자 접근 가능 페이지";
    }
}

 

요약

 

메서드 수준 보안 애노테이션: @PreAuthorize@Secured를 사용하여 메서드 접근 제어.

권한 검사 API: SecurityContextHolder를 사용하여 동적으로 권한 검사.

컨트롤러에서 권한 검사: @PreAuthorize를 사용하여 요청 핸들러 접근 제어.

템플릿 엔진에서 권한 검사: Thymeleaf를 사용하여 동적으로 UI 요소 표시.

 
반응형
반응형

Spring Security는 Spring 기반 애플리케이션의 보안을 제공하는 강력하고, 유연한 프레임워크입니다.

특징

 

인증(Authentication)과 인가(Authorization) 기능 제공

다양한 보안 공격 방어 (CSRF, XSS 등)

확장 가능하고 커스터마이징이 용이

 

 

프로젝트 라이브러리

build.gradle

    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'

spring security 뿐 아니라 API 에서 jwt 토큰을 인증 및 인가 하는 기능을 사용하려면 jsonwebtoken 라이브러리도 추가하여야 한다. 

 

인증 개념

(Authentication)

인증은 사용자의 신원을 확인하는 과정입니다.

 

인가 개념

(Authorization)

인가는 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정입니다.

 

JwtRequestFilter

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    private UserDetailsService userDetailsService;
    private JwtUtil jwtUtil;
    @Autowired
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Autowired
    public void setJwtUtil(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String authorizationHeader = request.getHeader("Authorization");

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtUtil.validateToken(jwt, userDetails)) {

                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}

 

JwtUtil

@Component
public class JwtUtil {

//    private String SECRET_KEY = Base64.getEncoder().encodeToString("secret".getBytes());

    private final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);


    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(SECRET_KEY)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    public String generateToken(UserDetails userDetails) {
        return createToken(userDetails.getUsername());
    }

    private String createToken(String subject) {
        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10시간 유효
                .signWith(SECRET_KEY)
                .compact();
    }


    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

 

SecurityConfig

@Configuration
public class SecurityConfig {

    private final JwtRequestFilter jwtRequestFilter;

    //@Lazy 이렇게 하면 JwtRequestFilter 빈의 초기화를 지연시켜 순환 참조 문제를 피할 수 있습니다.
    @Autowired
    public SecurityConfig(@Lazy JwtRequestFilter jwtRequestFilter) {
        this.jwtRequestFilter = jwtRequestFilter;
    }



    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//        http
//                .authorizeHttpRequests(authorizeRequests ->
//                        authorizeRequests
//                                .requestMatchers("/admin/**").hasRole("ADMIN")
//                                .requestMatchers("/user/**").hasRole("USER")
//                                .anyRequest().authenticated()
//                )
//          •  authorizeHttpRequests 메서드를 사용하여 HTTP 요청에 대한 인가(Authorization) 규칙을 설정합니다.
//          •  requestMatchers("/admin/**").hasRole("ADMIN"): URL 패턴이 /admin/**인 요청은 ADMIN 역할을 가진 사용자만 접근할 수 있도록 설정합니다.
//          •   requestMatchers("/user/**").hasRole("USER"): URL 패턴이 /user/**인 요청은 USER 역할을 가진 사용자만 접근할 수 있도록 설정합니다.
//          •   anyRequest().authenticated(): 나머지 모든 요청은 인증된 사용자만 접근할 수 있도록 설정합니다.

//        .formLogin(formLogin ->
//                formLogin
//                        .loginPage("/login")
//                        .permitAll()
//        )
//          •  formLogin 메서드를 사용하여 폼 기반 로그인을 설정합니다.
//          •   loginPage("/login"): 사용자 정의 로그인 페이지를 설정합니다. 사용자가 /login URL로 접근하면 로그인 페이지가 표시됩니다.
//          •   permitAll(): 로그인 페이지는 인증되지 않은 사용자도 접근할 수 있도록 허용합니다.
//
//                .csrf(csrf -> csrf.disable()); // CSRF 보호 비활성화. 필요한 경우 활성화 가능
//          •  csrf 메서드를 사용하여 CSRF(Cross-Site Request Forgery) 보호 설정을 합니다.
//          •  csrf.disable(): CSRF 보호를 비활성화합니다. CSRF 보호가 필요할 경우 이 부분을 제거하거나 다른 방법으로 설정할 수 있습니다.
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers("/api/auth/**").permitAll()
                                .anyRequest().authenticated()
                )
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        var userDetailsManager = new InMemoryUserDetailsManager();

        var user = User.withUsername("user")
                .password(passwordEncoder().encode("password"))
                .roles("USER")
                .build();

        var admin = User.withUsername("admin")
                .password(passwordEncoder().encode("admin"))
                .roles("ADMIN")
                .build();

        userDetailsManager.createUser(user);
        userDetailsManager.createUser(admin);

        return userDetailsManager;
    }

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


    /**
     * Spring Security 설정 파일에서 AuthenticationManager를 빈으로 정의해야 합니다.
     * Spring Boot 2.x에서는 AuthenticationManager를 직접 정의하는 것이 필요했지만,
     * Spring Boot 3.x에서는 이를 자동으로 구성하지 않으므로 명시적으로 설정해야 합니다.
     * @param authenticationConfiguration
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

 

user 와 password 라는 아이디 및 비밀번호를 이용하면 로그인 가능하다.

 

AuthController

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

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @PostMapping("/login")
    public String createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
        );

        final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
        final String jwt = jwtUtil.generateToken(userDetails);

        return jwt;
    }
}

 

AuthRequest

@Getter
@Setter
public class AuthRequest {
    private String username;
    private String password;
}

 

 

로그인시 

curl --location 'http://localhost:8080/api/auth/login' \
--header 'Content-Type: application/json' \
--data '{
"username": "user",
"password": "password"
}'

이렇게 요청할 경우 JWT 토큰을 응답 받고 응답받은 토큰을 API Request 헤더에 
Authtication : Bearar {토큰} 
으로 넣어주면 인가를 해주면 됩니다. 

인가를 원하지 않으면 SecurityConfig 클래스의 securityFilterChain 에 permit 하는 경로를 추가해 주면 됩니다.

 

Spring Security 5.4 이후부터는 WebSecurityConfigurerAdapter 대신 SecurityFilterChain을 사용하여 보안 구성을 합니다.

 

 

Spring Security에서 인증 및 인가 구현 시 사용되는 주요 개념들

 

1. Filter 체인

설명: Spring Security는 다양한 필터들의 체인으로 구성됩니다. 각 필터는 Request를 가로챈 후 일련의 절차를 처리합니다.

주요 필터: UsernamePasswordAuthenticationFilter는 사용자가 제출한 인증 정보를 처리합니다.

2. UsernamePasswordAuthenticationToken 생성

설명: UsernamePasswordAuthenticationFilter는 UsernamePasswordAuthenticationToken을 생성하여 AuthenticationManager에게 전달합니다. 이 토큰에는 사용자가 제출한 인증 정보가 포함되어 있습니다.

3. AuthenticationManager

설명: AuthenticationManager는 실제로 인증을 수행하는데, 여러 AuthenticationProvider들을 이용합니다.

4. AuthenticationProvider

설명: 각각의 Provider들은 특정 유형의 인증을 처리합니다. 예시로 DaoAuthenticationProvider는 사용자 정보를 데이터베이스에서 가져와 인증을 수행합니다.

5. PasswordEncoder

설명: 인증과 인가에서 사용될 패스워드의 인코딩 방식을 지정합니다.

6. UserDetailsService

설명: AuthenticationProvider는 UserDetailsService를 사용하여 사용자 정보를 가져옵니다. UserDetailsService는 사용자의 아이디를 받아 loadUserByUsername을 호출하여 해당 사용자의 UserDetails를 반환합니다.

7. UserDetails

설명: UserDetails에는 사용자의 아이디, 비밀번호, 권한 등이 포함되어 있습니다.

8. Authentication 객체 생성

설명: 인증이 성공하면, AuthenticationProvider는 Authentication 객체를 생성하여 AuthenticationManager에게 반환합니다. 이 Authentication 객체에는 사용자의 세부 정보와 권한이 포함되어 있습니다.

9. SecurityContextHolder

설명: 현재 실행 중인 스레드에 대한 SecurityContext를 제공합니다.

10. SecurityContext

설명: 현재 사용자의 Authentication이 저장되어 있습니다. 애플리케이션은 SecurityContextHolder를 통해 현재 사용자의 권한을 확인하고, 인가 결정을 합니다.

 

인증 및 인가 설정에 대한 주요 사항

 

1. CSRF 보호 비활성화

설명: CSRF 토큰을 사용하지 않으므로 확인하지 않도록 설정합니다.

2. CORS 설정 적용

설명: 다른 도메인의 웹 페이지에서 리소스에 접근할 수 있도록 허용합니다.

3. 폼 로그인과 HTTP 기본 인증 비활성화

설명: Spring 웹 페이지에서 제공되는 로그인 폼을 통해 사용자를 인증하는 메커니즘과 HTTP 기본 인증을 비활성화합니다.

4. JwtAuthFilter 추가

설명: UsernamePasswordAuthenticationFilter 앞에 JwtAuthFilter를 추가하여, JWT 필터를 거치도록 설정합니다. JwtAuthFilter를 통해서 Authentication을 획득하였다면 인증된 자원에 접근할 수 있습니다.

5. 권한에 대한 규칙 작성

설명: anyRequest().permitAll() 설정은 기본적으로 모두 허용해 주지만, EnableGlobalMethodSecurity를 설정하는 이유는 Annotation으로 접근 제한을 설정하기 위함입니다.

추가 설명: 만약 Annotation을 통해 접근 제한을 하지 않을 것이라면, anyRequest() 부분에서 접근 승인할 엔드포인트들을 작성해야 합니다.

6. 인증과 인가 실패 시 Exception Handler 추가

설명: Security 단계에서 권한 관련 401이나 403 에러 등을 처리해 줄 핸들러를 함께 등록해줍니다.

authenticationEntryPoint: 인증되지 않은 사용자에 대해 처리하는 핸들러 정의

accessDeniedHandler: 인증되었지만, 특정 리소스에 대한 권한이 없는 경우(인가) 처리하는 핸들러 정의

 

요약

CSRF 보호 비활성화: CSRF 토큰 사용하지 않음.

CORS 설정: 다른 도메인의 웹 페이지에서 리소스 접근 허용.

폼 로그인과 HTTP 기본 인증 비활성화: Spring 웹 페이지의 로그인 폼과 HTTP 기본 인증 비활성화.

JwtAuthFilter 추가: UsernamePasswordAuthenticationFilter 앞에 JWT 필터 추가.

권한 규칙 작성: anyRequest().permitAll() 설정과 EnableGlobalMethodSecurity 설정.

Exception Handler 추가: 인증과 인가 실패 시 처리할 핸들러 정의.

반응형
반응형

excel poi 라이브러리

 <dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.3</version>
</dependency>

spring webflux 에서는 HttpServletResponse 를 사용하지 않아 header 에 xlsx 정보를 넣어주는부분에서 spring 일반 샘플대로 하면 오류가 생길 것이다. 
HttpHeaders 에 set 해주고 Mono.just 에 headers 를 세팅해주면 된다. 

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import reactor.core.publisher.Mono;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

@RestController
public class ExcelController {

    @GetMapping("/download-excel")
    public Mono<ResponseEntity<ByteArrayResource>> downloadExcel() {
        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet("Sheet1");

        // Create some sample data
        Row headerRow = sheet.createRow(0);
        headerRow.createCell(0).setCellValue("Name");
        headerRow.createCell(1).setCellValue("Email");

        Row dataRow = sheet.createRow(1);
        dataRow.createCell(0).setCellValue("John Doe");
        dataRow.createCell(1).setCellValue("john.doe@example.com");

        // Generate Excel file bytes
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            workbook.write(outputStream);
            workbook.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Set up the HTTP response headers
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        headers.setContentDispositionFormData("attachment", "data.xlsx");

        // Create a ByteArrayResource from the file bytes
        ByteArrayResource resource = new ByteArrayResource(outputStream.toByteArray());

        // Return the response entity with the file bytes and headers
        return Mono.just(ResponseEntity.ok()
                .headers(headers)
                .contentLength(outputStream.size())
                .body(resource));
    }
}

 

반응형
반응형

필요한 라이브러리

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
    <groupId>io.asyncer</groupId>
    <artifactId>r2dbc-mysql</artifactId>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

 

 

설정 코드 

@Configuration
@EnableR2dbcRepositories
public class R2DBCConfiguration extends AbstractR2dbcConfiguration {
    @Override
    @Bean
    public ConnectionFactory connectionFactory() {
        ConnectionFactory connectionFactory = ConnectionFactories.get(ConnectionFactoryOptions.builder()
                        .option(DRIVER, "mysql")
                        .option(HOST, "localhost")
                        .option(USER, "root")
                        .option(PASSWORD, "password")
                        .option(PORT, 3307)
                        .option(DATABASE, "db")
                                                    .build());
        return connectionFactory;
    }
}

와 같이 설정하면 db 연결된다.

세팅 및 설정 관련 문서

https://github.com/asyncer-io/r2dbc-mysql

반응형
반응형

webflux 를 사용하면 spring boot 내장서버가 tomcat 에서 netty 로 사용해야 하는데,

netty 가 비동기에 유리한 서버이다 보니, sleep 을 주고 싶은 로직이 있을 떄 sleep 을 주기가 비효율적이다

 

 

Spring Netty를 사용할 때 Thread.sleep()를 사용하여 지정된 시간 동안 스레드 실행을 일시 중지 할 수 있다 .

하지만 이벤트 루프를 차단하고 성능 문제를 일으킬 수 있으므로 Netty 애플리케이션에서 와 같은 차단 작업을 사용하는 것은 일반적으로 권장되지 않는다 .

 

대신 Netty는 패키지 에서 사용할 수 있는 ScheduledExecutorService를 사용하여 작업 지연을 위한 비동기 대안을 제공한다 io.netty.util. schedule()의 방법을 사용하여 ScheduledExecutorService지정된 지연 후 작업 실행을 예약 할 수 있습니다 .

ScheduledExecutorService 다음은 Spring Netty 애플리케이션에서 어떻게 사용할 수 있는지에 대한 예입니다

 

 

spring netty 에서 ScheduledExecutorService를 사용한 예시다.

EventLoopGroup클래스에 빈을 주입합니다 .

@Autowired
private EventLoopGroup eventLoopGroup;
 

ScheduledExecutorService지연이 있는 작업을 예약하려면 다음을 사용

 
eventLoopGroup.schedule(() -> {
    // Code to be executed after the delay
}, 500, TimeUnit.MILLISECONDS);

 

 

이 예에서 작업은 500밀리초 지연 후에 실행됩니다. 필요에 따라 지연 및 시간 단위를 조정할 수 있다.

Netty에서 제공하 ScheduledExecutorService를 사용하면 이벤트 루프를 지연시키지 않으므로 애플리케이션이 응답성과 성능을 유지할 수 있습니다.

참고: Spring Netty 애플리케이션에서 Thread.sleep()를 사용해야 하는 경우 이벤트 루프 차단을 피하기 위해 별도의 스레드에서 사용할 수 있습니다. 그러나 일반적으로 가능할 때마다 Netty에서 제공하는 비동기 메커니즘을 활용하는 것이 좋다.

 

ScheduledExecutorService는 특정 시간 또는 간격으로 작업을 예약하고 실행하는 편리한 방법을 제공하므로 주기적 또는 지연된 작업을 수행해야 하는 시나리오에 유용.
비동기 이벤트 기반 네트워크 애플리케이션 프레임워크인 Netty는 ScheduledExecutorService를 활용하여 비차단 방식으로 지연된 작업을 처리한다. 애플리케이션의 실행 및 성능을 차단하지 않고 이벤트 루프에서 작업을 예약할 수 있다.

 

 

 

반응형
반응형

Spring Boot 2.x -> 3.0 차이

  • 최소 요구사항 변경 (M4 기준)
    • Gradle 7.5
    • Groovy 4.0
    • Jakarta EE 9
    • Java 17
    • Kotlin 1.6
    • Hibernate 6.1
    • Spring Framework 6
  • AOT maven, gradle 플러그인 제공
  • native 지원 기능 확대

spring 3.0 지원 라이브러리

Spring의 AOT란? (Ahead Of Time)

Spring AOT 엔진은 빌드 시 스프링 애플리케이션을 분석하고 최적화하는 도구입니다. 또한 AOT 엔진은 GraalVM Native Configuration이 필요로 하는 reflection configuration을 생성해줍니다. 이것은 Spring native 실행 파일로 컴파일 하는데 사용되고 이후에 애플리케이션의 시작 시간과 메모리 사용량을 줄일 수 있게 됩니다.

 

Spring Boot 3.0 AOT

Spring Boot 3.0 AOT 부분 확대

위 그림에서 보면 AOT가 Spring Boot 환경에서 하는 일들과 순서를 알 수 있습니다. 간단하게 얘기하자면 Bytecode를 분석하고 최적화해서 좀 더 실행하기에 빠르고 메모리적으로 효율적인 코드를 만듭니다.

(+ spring의 native-image는 JVM에서 실행되는 파일에 비해 빌드 시간은 길고 시작시간이 짧고 메모리는 적게 사용하게 된다.)

AOT 적용 효과

  • 런타임시 Spring 인프라를 적게 사용
  • 런타임 시 검증할 조건 수 감소
  • 리플렉션을 줄이고 프로그래밍적 Bean 등록 방식 사용

 

제일 메이저한 변화는 java17 이다. 또한 R2DBC 지원도 눈에 띈다.

 

 

참고문헌

https://www.baeldung.com/spring-boot-3-spring-6-new

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Release-Notes  - github release note (자세한 버젼 정보)

반응형
반응형

1. 자바 11 지원

2. 스프링 데이터 JPA, lazy 모드 지원

3. 의존성이 많이 변경됨.

  • 스프링 프레임워크 5.0 -> 스프링 프레임워크 버전 5.1

 

  • JUnit 4.12 -> JUnit 5.2
  •  

 

  • 톰캣 8.5.39 -> 톰캣 9
  • BIO 커넥터 사라지고 NIO 커넥터 기본으로 사용
  • HTTP/2 지원
  • 웹소켓 2.0 지원
  • 서블릿 4.0 / JSP 2.4 지원
  • 참고

 

  • 하이버네이트 5.2 -> 하이버네이트 5.3
  • JPA 2.2 지원
  • Java 8의 Date와 Time API 지원
  • 참고

 

4. 빈 오버라이딩을 기본으로 허용하지 않도록 변경

만약 허용해야 하면 

spring.main.allow-bean-definition-overriding to true.

과 같이 설정 변경.

 

5. Acutator에 “/info”와 “/health” 공개하도록 바뀜

 

6.프로퍼티 변경

7. 로깅 그룹

 

 

어떤버젼을 사용해야 하는가

부트 버젼은 GA 라고 명시된 버젼을 사용하는것 권장. 스냅샷 버젼은 권장 X

https://spring.io/projects/spring-boot#learn - > 링크에서 확인 가능.

 

 

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.1-Release-Notes [공식문서]

 

반응형

+ Recent posts