폴더 서비스에서 addFolders에 String name이 아니라 User user을 파라미터값으로 넣었을때의 문제점 -> 지연로딩, 프록시객체랑 관련있음
json 직렬화, 역직렬화
- 사용자는 회원가입을 진행한다.
a. 해당 URI 요청은 permitAll 처리하고 사용자의 입력값으로 service에서 회원가입을 진행한다.- 사용자의 정보를 저장할 때 비밀번호를 암호화하여 저장한다.
a. PasswordEncoder를 사용하여 비밀번호를 암호화 한 후 저장한다.- 사용자는 로그인을 진행한다.
a. 해당 URI 요청은 permitAll 처리하고 사용자의 입력값으로 service에서 회원 인증을 진행한다. (비밀번호 일치여부 등)- 사용자 인증을 성공하면 사용자의 정보를 사용하여 JWT 토큰을 생성하고 Header에 추가하여 반환한다. Client 는 이를 쿠키저장소에 저장한다.
- 사용자는 게시글 작성과 같은 요청을 진행할 때 발급받은 JWT 토큰을 같이 보낸다.
- 서버는 JWT 토큰을 검증하고 토큰의 정보를 사용하여 사용자의 인증을 진행해주는 Spring Security 에 등록한 Custom Security Filter 를 사용하여 인증/인가를 처리한다.
- Custom Security Filter에서 SecurityContextHolder 에 인증을 완료한 사용자의 상세 정보를 저장하는데 이를 통해 Spring Security 에 인증이 완료 되었다는 것을 알려준다.
# 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();
}
}
# 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());
}
}
# 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);
}