[Spring] 스프링 심화 - 시큐리티 적용

호호빵·2023년 1월 4일
0

Spring

목록 보기
20/24
  • 폴더 서비스에서 addFolders에 String name이 아니라 User user을 파라미터값으로 넣었을때의 문제점 -> 지연로딩, 프록시객체랑 관련있음

  • json 직렬화, 역직렬화


Security에서 JWT를 사용한 인증/ 인가

  1. 사용자는 회원가입을 진행한다.
    a. 해당 URI 요청은 permitAll 처리하고 사용자의 입력값으로 service에서 회원가입을 진행한다.
  2. 사용자의 정보를 저장할 때 비밀번호를 암호화하여 저장한다.
    a. PasswordEncoder를 사용하여 비밀번호를 암호화 한 후 저장한다.
  3. 사용자는 로그인을 진행한다.
    a. 해당 URI 요청은 permitAll 처리하고 사용자의 입력값으로 service에서 회원 인증을 진행한다. (비밀번호 일치여부 등)
  4. 사용자 인증을 성공하면 사용자의 정보를 사용하여 JWT 토큰을 생성하고 Header에 추가하여 반환한다. Client 는 이를 쿠키저장소에 저장한다.
  5. 사용자는 게시글 작성과 같은 요청을 진행할 때 발급받은 JWT 토큰을 같이 보낸다.
  6. 서버는 JWT 토큰을 검증하고 토큰의 정보를 사용하여 사용자의 인증을 진행해주는 Spring Security 에 등록한 Custom Security Filter 를 사용하여 인증/인가를 처리한다.
  7. Custom Security Filter에서 SecurityContextHolder 에 인증을 완료한 사용자의 상세 정보를 저장하는데 이를 통해 Spring Security 에 인증이 완료 되었다는 것을 알려준다.
  • 즉, JWT 토큰과 Custom Security Filter를 통해 사용자를 로그인이 된 상태로 유지시켜준다' 라고 이해하면 된다.

1. 사전 작업

# build.gradle
// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'
# WebSecurityConfig

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;

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


    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // h2-console 사용 및 resources 접근 허용 설정
        return (web) -> web.ignoring()
                .requestMatchers(PathRequest.toH2Console())
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable();

        // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.authorizeRequests().antMatchers("/api/user/**").permitAll()
                .antMatchers("/api/search").permitAll()
                .antMatchers("/api/shop").permitAll()
                .anyRequest().authenticated()
                // JWT 인증/인가를 사용하기 위한 설정
                .and().addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);


        http.formLogin().loginPage("/api/user/login-page").permitAll();

        http.exceptionHandling().accessDeniedPage("/api/user/forbidden");

        return http.build();
    }

}

  • DTO?

2. Security에 JWT 인증방식 적용하기

  • JwtUtil
  • JwtAuthFilter
  • WebSecurityConfig -> 추가 메소드는 위 코드에서 추가해놓음
  • UserDetailsImpl
  • UserDetailsServiceImpl
# JwtUtil

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
    private final UserDetailsServiceImpl userDetailsService;

    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String AUTHORIZATION_KEY = "auth";
    private static final String BEARER_PREFIX = "Bearer ";
    private static final long TOKEN_TIME = 60 * 60 * 1000L;

    @Value("${jwt.secret.key}")
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    // header 토큰을 가져오기
    public String resolveToken(HttpServletRequest request) {...return null;}

    // 토큰 생성
    public String createToken(String username, UserRoleEnum role) { ... }

    // 토큰 검증
    public boolean validateToken(String token) {...return false;}

    // 토큰에서 사용자 정보 가져오기
    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

    // 인증 객체 생성  <-  Security에 JWT 인증방식을 적용하기 위해 
    public Authentication createAuthentication(String username) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}
# JwtAuthFilter

@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

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

        String token = jwtUtil.resolveToken(request);

		// 토큰이 request header에 있냐 없냐로 분기처리
        // 모든 uri 가 permitAll 되어있는 것이 아님
        // 회원가입과 로그인은 인증이 필요없음 -> 토큰이 헤더에 들어있지 않음, 분기처리 해주지 않으면 토큰 검증과정에서 exception 발생
		// 토큰이 없고 인증이 필요없는 로직은 바로 다음 필터처리 됨
        if(token != null) {
            if(!jwtUtil.validateToken(token)){
                jwtExceptionHandler(response, "Token Error", HttpStatus.UNAUTHORIZED.value());
                return;
            }
            Claims info = jwtUtil.getUserInfoFromToken(token);
            setAuthentication(info.getSubject());
        }
        
        // 이 필터로 이동할때 이 요청은 인증이 되었다고 시큐리티가 인지하고 컨트롤러 전까지 요청이 전달됨
        filterChain.doFilter(request,response);
    }

	// 인증객체를 만들어서 context 안에 넣어줌 -> SecurityContextHolder에 넣어줌
    public void setAuthentication(String username) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = jwtUtil.createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

    public void jwtExceptionHandler(HttpServletResponse response, String msg, int statusCode) {
        response.setStatus(statusCode);
        response.setContentType("application/json");
        try {
            String json = new ObjectMapper().writeValueAsString(new SecurityExceptionDto(statusCode, msg));
            response.getWriter().write(json);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
# UserDetailsImpl

public class UserDetailsImpl implements UserDetails {

    private final User user;
    private final String username;

    public UserDetailsImpl(User user, String username) {
        this.user = user;
        this.username = username;
    }

    public User getUser() {
        return user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }
 }
# UserDetailsServiceImpl

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));

        return new UserDetailsImpl(user, user.getUsername());
    }
}

3. Product 도메인에 security 적용하기

# ProductController

	// 변경 전, 관심 상품 등록하기
    @PostMapping("/products")
    public ProductResponse createProduct(@RequestBody ProductRequest requestDto, HttpServletRequest request) {
        // 응답 보내기
        return productService.createProduct(requestDto, request);
    }  

	// 변경 후, 관심 상품 등록하기
    @Secured(UserRoleEnum.Authority.ADMIN)
    @PostMapping("/products")
    public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        // 응답 보내기
        return productService.createProduct(requestDto, userDetails.getUser());
    }
# ProductService

// 변경 전
@Transactional
    public ProductResponse createProduct(ProductRequest requestDto, HttpServletRequest request) {
        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;   // JWT 내 정보를 담을 수 있는 객체라고 생각하기

        // 토큰이 있는 경우에만 관심상품 추가 가능
        if (token != null) {
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            // 요청받은 DTO 로 DB에 저장할 객체 만들기
            Product product = productRepository.saveAndFlush(new Product(requestDto, user.getId()));

            return new ProductResponse(product);
        } else {
            return null;
        }
    }
    
    
// 변경 후    
@Transactional
    public ProductResponseDto createProduct(ProductRequestDto requestDto, User user) {
        System.out.println("ProductService.createProduct");
        System.out.println("user.getUsername() = " + user.getUsername());

        // 요청받은 DTO 로 DB에 저장할 객체 만들기
        Product product = productRepository.saveAndFlush(new Product(requestDto, user.getId()));

        return new ProductResponseDto(product);
    }


profile
하루에 한 개념씩

0개의 댓글