AWS Back Day 80. "Spring Boot를 활용한 도서관리 시스템 : JWT 필터 구현 및 검색 , 카테고리 기능 구현"

이강용·2023년 4월 26일
1

Spring Boot

목록 보기
15/20

jwt 필터 구현 (토큰 인증)

security
JwtAuthenticationFilter 생성

package com.toyproject.bookmanagement.security;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
	
	private final JwtTokenProvider jwtTokenProvider; //filter 는 IOC등록된 녀석이 아님 

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		
		HttpServletRequest httpRequest = (HttpServletRequest) request; // 다운캐스팅
		String accessToken  = httpRequest.getHeader("Authorization"); //Authorization에 토큰 들어있음
		accessToken = jwtTokenProvider.getToken(accessToken);
		boolean validationFlag = jwtTokenProvider.validateToken(accessToken); // 유효성 검사 
		
		if(validationFlag) {   //
			Authentication authentication = jwtTokenProvider.getAuthentication(accessToken);
			SecurityContextHolder.getContext().setAuthentication(authentication); // 여기에 등록이 되어야 로그인이 된것이다.
		}
		
		
		chain.doFilter(request, response);
	}

}

JwtTokenProvider (추가)

public Authentication getAuthentication(String accessToken) {
		
		Authentication authentication = null;
		Claims claims = getClaims(accessToken);
		
		if(claims.get("auth") == null) {
			throw new CustomException("AccessToken에 권한 정보가 없습니다.");
		}
		
		List<SimpleGrantedAuthority> authorities = new ArrayList<>();
		
		String auth = claims.get("auth").toString();
		for(String role :auth.split(",")) {
			authorities.add(new SimpleGrantedAuthority(role));
		}
		
		
		UserDetails userDetails = new User(claims.getSubject(),"",authorities);
		
		authentication = new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
		
		return authentication;
	}

SecurityConfig (수정)

package com.toyproject.bookmanagement.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.toyproject.bookmanagement.security.JwtAuthenticationFilter;
import com.toyproject.bookmanagement.security.JwtTokenProvider;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	
	private final JwtTokenProvider jwtTokenProvider; //여기서는 DI 가능
	
	
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		 http.csrf().disable();
		 http.httpBasic().disable();
		 http.formLogin().disable();
		 
		 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		 
		 http.authorizeRequests()
		 	 .antMatchers("/auth/**")
		 	 .permitAll()
		 	 .anyRequest()
		 	 .authenticated()
		 	 .and()
		 	 .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);
		 		
	}
}

security

JwtAuthenticationEntryPoint(생성)

package com.toyproject.bookmanagement.security;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.toyproject.bookmanagement.dto.common.ErrorResponseDto;

@Component 
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

	@Override
	public void commence(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException authException) throws IOException, ServletException {
		
		response.setContentType(MediaType.APPLICATION_JSON_VALUE); 
		response.setStatus(HttpStatus.UNAUTHORIZED.value()); // 401 error
		ErrorResponseDto<?> errorResponseDto = 
				new ErrorResponseDto<AuthenticationException>("토큰 인증 실패",authException);
		ObjectMapper objectMapper = new ObjectMapper(); 
		
		String responseJson = objectMapper.writeValueAsString(errorResponseDto); //객체를 넣으면 알아서 Json으로 변환
		
		PrintWriter out = response.getWriter();
		out.println(responseJson);
		
	}

}

SecurityConfig (수정)

package com.toyproject.bookmanagement.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.toyproject.bookmanagement.security.JwtAuthenticationEntryPoint;
import com.toyproject.bookmanagement.security.JwtAuthenticationFilter;
import com.toyproject.bookmanagement.security.JwtTokenProvider;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	
	private final JwtTokenProvider jwtTokenProvider; //@component 여기서는 DI 가능
	private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; //@component 여기서는 DI 가능
	
	
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		 http.cors();
	     http.csrf().disable();
		 http.httpBasic().disable();
		 http.formLogin().disable();
		 
		 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		 
		 http.authorizeRequests()
		 	 .antMatchers("/auth/**")
		 	 .permitAll()
		 	 .anyRequest()
		 	 .authenticated()
		 	 .and()
		 	 .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
		 	 .exceptionHandling()
		 	 .authenticationEntryPoint(jwtAuthenticationEntryPoint);
		 		
	}
}

Front

Main.js (수정 & 추가)

const Main = () => {
    const [ refresh, setRefresh ] = useState(true);
    const [ books, setBooks ] = useState([]);
    useEffect(() => {

    },[]);

    const searchParam ={
        page: 1
    }

    const option = {
        params: searchParam,
        headers: {
            Authorization: localStorage.getItem("accessToken")
        }
    }

    const searchBooks = useQuery(["searchBooks"], async () => {
        const response = await axios.get("http://localhost:8080/books", option);
        return response;
    },{
        onSuccess: (response) => {
            setBooks([...books, ...response.data]);
        },
        enabled: refresh        
    });

    return (
        <div css ={mainContainer}>
            <Sidebar></Sidebar>
            <header css={header}>
                <div>도서검색</div>
                <div>
                    <input type="search" />
                </div>
            </header>
            <main css ={main}>
                {books.length > 0 ? books.map(book => (<BookCard key={book.bookId} book={book}></BookCard>)) : ""}
            </main>
        </div>
    );
};

export default Main;

결과

BookCard.js(수정)

const BookCard = ({ book }) => {
    return (
        <div css={cardContainer}>
            <header css={header}>
                <h1 css={titleText}>{book.bookName} </h1>
            </header>
            <main css={main}>
                <div css ={imgBox}>
                    <img css={img} src={book.coverImgUrl} alt={book.bookName} />
                </div>
            </main>
            <footer css={footer}>
                <div css={like}><div css={likeIcon}><AiOutlineLike /></div>추천: 10 </div>
                <h2>저자명: {book.authorName}</h2>
                <h2>출판사: {book.publisherName}</h2>
            </footer>
        </div>
    );
};

Intersection Observer

  • 웹 API의 한 종류로, HTML 요소와 뷰포트(ViewPort) 또는 다른 요소간의 교차점을 비동기적으로 관찰할 수 잇게 해주는 기능
  • 일반적으로 스크롤 이벤트를 사용하여 요소의 가시성을 추적하는 것보다 효율적이고 성능이 뛰어남

특징

  • 지연로딩(Lazy loading) : 이미지나 다른 컨텐츠가 사용자의 뷰포트에 진입할 때 까지 로딩을 지연시키는 것으로, 성능 최적화의 데이터 사용량 절약에 도움이 됨
  • 무한 스크롤(Infinity scroll) : 페이지 끝에 도달하면 추가 컨텐츠를 자동으로 로드하는 기능으로, 사용자 경험을 개선하는데 도움이 됨
  • 광고 추적 : 광고 노출과 관련된 이벤트를 추적하여 광고 성과를 측정하는데 사용
  • 스크롤 기반 애니메이션 : 특정 요소가 뷰포트에 진입하거나 나갈 때 애니메이션을 실행할 수 있어, 동적인 웹페이지를 구현하는데 도움이 됨

검색 기능 구현

BookRepository(추가)

package com.toyproject.bookmanagement.repository;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Mapper;

import com.toyproject.bookmanagement.entity.Book;

@Mapper
public interface BookRepository {
	
	public List<Book> searchBooks(Map<String, Object> map);
	public int getTotalCount(Map<String, Object> map);

}

BookMapper.xml (추가)

<select id="getTotalCount" parameterType="hashMap" resultType="Integer">
		select 
			count(*)
		from 
			book_tb bt
			left outer join author_tb at on (at.author_id = bt.author_id)
			left outer join publisher_tb pt on (pt.publisher_id = bt.publisher_id)
			left outer join category_tb ct on (ct.category_id = bt.category_id)
		
	</select>

BookService(수정 & 추가 )

package com.toyproject.bookmanagement.service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;

import com.toyproject.bookmanagement.dto.book.SearchBookReqDto;
import com.toyproject.bookmanagement.dto.book.SearchBookRespDto;
import com.toyproject.bookmanagement.repository.BookRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class BookService {
	
	private final BookRepository bookRepository;
	
	public Map<String, Object> searchBooks(SearchBookReqDto searchBookReqDto){
		List<SearchBookRespDto> list = new ArrayList<>();
		
		int index = (searchBookReqDto.getPage() - 1) * 20; // 20 부분 수정 가능 
		Map<String, Object> map = new HashMap<>();
		
		map.put("index" , index);
		
		bookRepository.searchBooks(map).forEach(book -> {
			
			list.add(book.toDto());
		});
		
		int totalCount = bookRepository.getTotalCount(map);
		
		
		Map<String, Object> responseMap = new HashMap<>();
		responseMap.put("totalCount", totalCount);
		responseMap.put("bookList", list);
		
		return responseMap;
	}
}

Front

Main.js (추가 +& 수정)

const Main = () => {

    const [ searchParam, setSearchParam] = useState({page: 1, searchValue:"", categoryId: 0});
    const [ refresh, setRefresh ] = useState(false);
    const [ books, setBooks ] = useState([]);
    const [ lastPage, setLastPage ] = useState(1);
    const lastBookRef = useRef();

    useEffect(() => {

        const observerService = (entries, observer) => {
            entries.forEach(entry => {
                if(entry.isIntersecting){
                    setRefresh(true);
                }
            });
        }

        const observer = new IntersectionObserver(observerService, {threshold: 1});
        observer.observe(lastBookRef.current); //대상이 화면에 보이면 실행해라 
    },[]);


    const option = {
        params: searchParam,
        headers: {
            Authorization: localStorage.getItem("accessToken")
        }
    }

    const searchBooks = useQuery(["searchBooks"], async () => {
        const response = await axios.get("http://localhost:8080/books", option);
        return response;
    },{
        onSuccess: (response) => {

            if(refresh){
                setRefresh(false);
            }
            console.log(response);
            const totalCount = response.data.totalCount;
            setLastPage(totalCount % 20 === 0 ? totalCount / 20 : Math.ceil(totalCount / 20) );
            setBooks([...books, ...response.data.bookList]);
            setSearchParam({...searchParam, page: searchParam.page + 1});
        },
        enabled: refresh && searchParam.page < lastPage + 1        
    });




    return (
        <div css ={mainContainer}>
            <Sidebar></Sidebar>
            <header css={header}>
                <div css ={title}>도서검색</div>
                <div css ={searchItems}>
                    <select css={categorySelect}>

                    </select>
                    <input css={searchInput} type="search" />
                </div>
            </header>
            <main css ={main}>
                {books.length > 0 ? books.map(book => (<BookCard key={book.bookId} book={book}></BookCard>)) : ""}
                <div ref={lastBookRef}></div>
            </main>
        </div>
    );
};

카테고리 구현

dto > book
CategoryRespDto (생성)

package com.toyproject.bookmanagement.dto.book;

import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class CategoryRespDto {
	private int categoryId;
	private String categoryName;
}

BookRepository (추가)

public List<Category> getCategories();

BookMapper.xml (추가)

<select id="getCategories" resultMap="CategoryMap">
  select
  category_id,
  category_name
  from
  category_tb
</select>

BookService (추가)

public List<CategoryRespDto> getCategories() {
		
		List<CategoryRespDto> list = new ArrayList<>();
		
		bookRepository.getCategories().forEach(category ->{
			list.add(category.toDto());
		});
		
		return list;
	}

Category (추가)

public CategoryRespDto toDto() {
		return CategoryRespDto.builder()
				.categoryId(categoryId)
				.categoryName(categoryName)
				.build();
				
	}

BookController (추가)

@GetMapping("/categories")
	public ResponseEntity<?> categories(){
		return ResponseEntity.ok().body(bookService.getCategories());
	}

Main.js (추가)

const categories = useQuery(["categories"], async () => {

        const option = {
            headers: {
                Authorization: localStorage.getItem("accessToken")
            }
        }

        const response = await axios.get("http://localhost:8080/categories", option);
        return response;
    })

Server단 변경

SearchBookReqDto (수정)

package com.toyproject.bookmanagement.dto.book;

import java.util.List;

import lombok.Data;

@Data
public class SearchBookReqDto {
	private int page;
	private String searchValue;
	private List<Integer> categoryIds;
}

BookService (수정)

public Map<String, Object> searchBooks(SearchBookReqDto searchBookReqDto){
		List<SearchBookRespDto> list = new ArrayList<>();
		
		int index = (searchBookReqDto.getPage() - 1) * 20; // 20 부분 수정 가능 
		Map<String, Object> map = new HashMap<>();
		
		map.put("index" , index);
		map.put("categoryIds", searchBookReqDto.getCategoryIds());
		
		bookRepository.searchBooks(map).forEach(book -> {
			
			list.add(book.toDto());
		});
		
		int totalCount = bookRepository.getTotalCount(map);
		
		
		Map<String, Object> responseMap = new HashMap<>();
		responseMap.put("totalCount", totalCount);
		responseMap.put("bookList", list);
		
		return responseMap;
	}

profile
HW + SW = 1

0개의 댓글