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);
}
}
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>
);
};
비동기적
으로 관찰할 수 잇게 해주는 기능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;
}
}
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;
})
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;
}