diff --git a/build.gradle b/build.gradle index 2973eacd4..643f83bbb 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-devtools' implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-mail' + // spring security 라이브러리 + implementation 'org.springframework.boot:spring-boot-starter-security' // websocket implementation 'org.springframework.boot:spring-boot-starter-websocket' diff --git a/src/main/java/codesquad/fineants/spring/config/CacheConfig.java b/src/main/java/codesquad/fineants/spring/config/CacheConfig.java deleted file mode 100644 index 7d8255337..000000000 --- a/src/main/java/codesquad/fineants/spring/config/CacheConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package codesquad.fineants.spring.config; - -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableCaching -public class CacheConfig { -} diff --git a/src/main/java/codesquad/fineants/spring/config/FilterConfig.java b/src/main/java/codesquad/fineants/spring/config/FilterConfig.java deleted file mode 100644 index 625e588e4..000000000 --- a/src/main/java/codesquad/fineants/spring/config/FilterConfig.java +++ /dev/null @@ -1,41 +0,0 @@ -package codesquad.fineants.spring.config; - -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import codesquad.fineants.domain.jwt.JwtProvider; -import codesquad.fineants.domain.oauth.support.AuthenticationContext; -import codesquad.fineants.spring.api.member.service.OauthMemberRedisService; -import codesquad.fineants.spring.filter.JwtAuthorizationFilter; -import codesquad.fineants.spring.filter.LogoutFilter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@Configuration -public class FilterConfig { - - private final JwtProvider jwtProvider; - private final AuthenticationContext authenticationContext; - private final ObjectMapper objectMapper; - private final OauthMemberRedisService redisService; - - @Bean - public FilterRegistrationBean jwtAuthorizationFilter() { - FilterRegistrationBean filterFilterRegistrationBean = new FilterRegistrationBean<>(); - filterFilterRegistrationBean.setFilter( - new JwtAuthorizationFilter(jwtProvider, authenticationContext, objectMapper, redisService)); - filterFilterRegistrationBean.addUrlPatterns("/api/*"); - return filterFilterRegistrationBean; - } - - @Bean - public FilterRegistrationBean logoutFiler() { - FilterRegistrationBean logoutFilerBean = new FilterRegistrationBean<>(); - logoutFilerBean.setFilter(new LogoutFilter(redisService, objectMapper)); - logoutFilerBean.addUrlPatterns("/api/auth/logout"); - return logoutFilerBean; - } -} diff --git a/src/main/java/codesquad/fineants/spring/config/SecurityConfig.java b/src/main/java/codesquad/fineants/spring/config/SecurityConfig.java new file mode 100644 index 000000000..14ad69225 --- /dev/null +++ b/src/main/java/codesquad/fineants/spring/config/SecurityConfig.java @@ -0,0 +1,71 @@ +package codesquad.fineants.spring.config; + +import codesquad.fineants.domain.jwt.JwtProvider; +import codesquad.fineants.domain.member.MemberRepository; +import codesquad.fineants.domain.oauth.support.AuthenticationContext; +import codesquad.fineants.spring.api.member.service.OauthMemberRedisService; +import codesquad.fineants.spring.filter.CorsFilter; +import codesquad.fineants.spring.filter.JwtAuthorizationFilter; +import codesquad.fineants.spring.filter.LogoutFilter; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +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.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@RequiredArgsConstructor +@EnableWebSecurity +public class SecurityConfig { + private final JwtProvider jwtProvider; + private final AuthenticationContext authenticationContext; + private final ObjectMapper objectMapper; + private final OauthMemberRedisService redisService; + private final MemberRepository memberRepository; + + // 인증이 필요하지 않은 주소 + private final String[] accessUrl = {"/api/auth/login", + "/api/auth/signup", + "/api/auth/**/authUrl", + "/api/auth/**/login", + "/api/auth/refresh/token", + "/api/auth/logout", + "/api/stocks/**"}; + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + /** + * 기본 설정 + */ + http.csrf().disable(); + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션을 사용하지 않는다. + http.formLogin().disable(); // 로그인 UI Form 미사용 + http.httpBasic().disable(); // http basic 방식 미사용 (ID+PW로 인증), bearer token 방식 사용 (JWT 토큰) + + /** + * 필터 추가 + */ + http.addFilterBefore(new CorsFilter(), UsernamePasswordAuthenticationFilter.class); // cors 필터 + http.addFilterBefore(new JwtAuthorizationFilter(jwtProvider, authenticationContext, objectMapper, redisService,memberRepository), UsernamePasswordAuthenticationFilter.class); // JWT 토큰으로 모든 접근에 대해 인증한다. + http.addFilterBefore(new LogoutFilter(redisService, objectMapper), UsernamePasswordAuthenticationFilter.class); + + /** + * 요청 허용 / 미허용 + */ + http.authorizeRequests() + .antMatchers(accessUrl).permitAll() + .anyRequest().authenticated(); + + return http.build(); + } +} diff --git a/src/main/java/codesquad/fineants/spring/config/WebConfig.java b/src/main/java/codesquad/fineants/spring/config/WebConfig.java deleted file mode 100644 index 42fe25687..000000000 --- a/src/main/java/codesquad/fineants/spring/config/WebConfig.java +++ /dev/null @@ -1,32 +0,0 @@ -package codesquad.fineants.spring.config; - -import java.util.List; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import codesquad.fineants.domain.oauth.support.AuthPrincipalArgumentResolver; -import codesquad.fineants.spring.intercetpor.LogoutInterceptor; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@Configuration -public class WebConfig implements WebMvcConfigurer { - - private final AuthPrincipalArgumentResolver authPrincipalArgumentResolver; - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new LogoutInterceptor()) - .excludePathPatterns("/api/*") - .addPathPatterns("/api/auth/logout"); - } - - @Override - public void addArgumentResolvers(List resolvers) { - resolvers.add(authPrincipalArgumentResolver); - WebMvcConfigurer.super.addArgumentResolvers(resolvers); - } -} diff --git a/src/main/java/codesquad/fineants/spring/filter/CorsFilter.java b/src/main/java/codesquad/fineants/spring/filter/CorsFilter.java index e06bbb64e..14b2d3669 100644 --- a/src/main/java/codesquad/fineants/spring/filter/CorsFilter.java +++ b/src/main/java/codesquad/fineants/spring/filter/CorsFilter.java @@ -17,14 +17,12 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -@Component -@Order(Ordered.HIGHEST_PRECEDENCE) +//@Component +//@Order(Ordered.HIGHEST_PRECEDENCE) public class CorsFilter implements Filter { @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws - IOException, - ServletException, ServletException { + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; diff --git a/src/main/java/codesquad/fineants/spring/filter/JwtAuthorizationFilter.java b/src/main/java/codesquad/fineants/spring/filter/JwtAuthorizationFilter.java index 42b99adde..b051b7f18 100644 --- a/src/main/java/codesquad/fineants/spring/filter/JwtAuthorizationFilter.java +++ b/src/main/java/codesquad/fineants/spring/filter/JwtAuthorizationFilter.java @@ -9,6 +9,13 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import codesquad.fineants.spring.filter.auth.CustomUserDetails; +import codesquad.fineants.domain.member.Member; +import codesquad.fineants.domain.member.MemberRepository; +import io.jsonwebtoken.Claims; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.AntPathMatcher; import org.springframework.util.StringUtils; import org.springframework.web.cors.CorsUtils; @@ -31,62 +38,78 @@ @RequiredArgsConstructor public class JwtAuthorizationFilter extends OncePerRequestFilter { - public static final String AUTHORIZATION = "Authorization"; - public static final String BEARER = "Bearer"; - private static final AntPathMatcher pathMatcher = new AntPathMatcher(); - private static final List excludeUrlPatterns = List.of( - "/api/auth/**/authUrl", - "/api/auth/**/login", - "/api/auth/refresh/token", - "/api/auth/logout", - "/api/stocks/**"); - private final JwtProvider jwtProvider; - private final AuthenticationContext authenticationContext; - private final ObjectMapper objectMapper; - private final OauthMemberRedisService redisService; - - @Override - protected boolean shouldNotFilter(HttpServletRequest request) { - return excludeUrlPatterns.stream() - .anyMatch(pattern -> pathMatcher.match(pattern, request.getRequestURI())); - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - if (CorsUtils.isPreFlightRequest(request)) { - filterChain.doFilter(request, response); - return; - } - try { - String token = extractJwt(request).orElseThrow( - () -> new UnAuthorizationException(JwtErrorCode.EMPTY_TOKEN)); - jwtProvider.validateToken(token); - redisService.validateAlreadyLogout(token); - authenticationContext.setAuthMember(jwtProvider.extractAuthMember(token)); - } catch (FineAntsException e) { - setErrorResponse(response, e.getErrorCode()); - return; - } - - filterChain.doFilter(request, response); - } - - private Optional extractJwt(HttpServletRequest request) { - String header = request.getHeader(AUTHORIZATION); - - if (!StringUtils.hasText(header) || !header.startsWith(BEARER)) { - return Optional.empty(); - } - - return Optional.of(header.split(" ")[1]); - } - - private void setErrorResponse(HttpServletResponse httpServletResponse, ErrorCode errorCode) throws IOException { - httpServletResponse.setStatus(errorCode.getHttpStatus().value()); - httpServletResponse.setContentType("application/json"); - httpServletResponse.setCharacterEncoding("UTF-8"); - ApiResponse body = ApiResponse.error(errorCode); - httpServletResponse.getWriter().write(objectMapper.writeValueAsString(body)); - } + public static final String AUTHORIZATION = "Authorization"; + public static final String BEARER = "Bearer"; + private static final AntPathMatcher pathMatcher = new AntPathMatcher(); + private static final List excludeUrlPatterns = List.of( + "/api/auth/**/authUrl", + "/api/auth/**/login", + "/api/auth/signup", + "/api/auth/login", + "/api/auth/refresh/token", + "/api/auth/logout", + "/api/stocks/**"); + private final JwtProvider jwtProvider; + private final AuthenticationContext authenticationContext; + private final ObjectMapper objectMapper; + private final OauthMemberRedisService redisService; + private final MemberRepository memberRepository; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + return excludeUrlPatterns.stream() + .anyMatch(pattern -> pathMatcher.match(pattern, request.getRequestURI())); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + if (CorsUtils.isPreFlightRequest(request)) { + filterChain.doFilter(request, response); + return; + } + try { + String token = extractJwt(request).orElseThrow( + () -> new UnAuthorizationException(JwtErrorCode.EMPTY_TOKEN)); + jwtProvider.validateToken(token); + redisService.validateAlreadyLogout(token); + authenticationContext.setAuthMember(jwtProvider.extractAuthMember(token)); + + // Authentication 객체 생성 + Claims claims = jwtProvider.getClaims(token); + long memberId = Long.parseLong(String.valueOf(claims.get("memberId"))); + + Member member = memberRepository.findById(memberId).orElseThrow(()-> new RuntimeException("there is no member")); + + CustomUserDetails userDetail = new CustomUserDetails(member); + + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities()); + + // security 세션에 authentication 객체 넣기 (spring security가 인증 / 인가를 처리 하기 위함) + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (FineAntsException e) { + setErrorResponse(response, e.getErrorCode()); + return; + } + + filterChain.doFilter(request, response); + } + + private Optional extractJwt(HttpServletRequest request) { + String header = request.getHeader(AUTHORIZATION); + + if (!StringUtils.hasText(header) || !header.startsWith(BEARER)) { + return Optional.empty(); + } + + return Optional.of(header.split(" ")[1]); + } + + private void setErrorResponse(HttpServletResponse httpServletResponse, ErrorCode errorCode) throws IOException { + httpServletResponse.setStatus(errorCode.getHttpStatus().value()); + httpServletResponse.setContentType("application/json"); + httpServletResponse.setCharacterEncoding("UTF-8"); + ApiResponse body = ApiResponse.error(errorCode); + httpServletResponse.getWriter().write(objectMapper.writeValueAsString(body)); + } } diff --git a/src/main/java/codesquad/fineants/spring/filter/auth/CustomUserDetailService.java b/src/main/java/codesquad/fineants/spring/filter/auth/CustomUserDetailService.java new file mode 100644 index 000000000..bd66f4735 --- /dev/null +++ b/src/main/java/codesquad/fineants/spring/filter/auth/CustomUserDetailService.java @@ -0,0 +1,23 @@ +package codesquad.fineants.spring.filter.auth; + +import codesquad.fineants.domain.member.Member; +import codesquad.fineants.domain.member.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailService implements UserDetailsService { + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + System.out.println("PrincipalDetailService.loadUserByUsername"); + Member member = memberRepository.findMemberByEmail(email) + .orElseThrow(()-> new RuntimeException("사용자를 찾을 수 없습니다.")); + return new CustomUserDetails(member); + } +} diff --git a/src/main/java/codesquad/fineants/spring/filter/auth/CustomUserDetails.java b/src/main/java/codesquad/fineants/spring/filter/auth/CustomUserDetails.java new file mode 100644 index 000000000..f64e71c82 --- /dev/null +++ b/src/main/java/codesquad/fineants/spring/filter/auth/CustomUserDetails.java @@ -0,0 +1,52 @@ +package codesquad.fineants.spring.filter.auth; + +import codesquad.fineants.domain.member.Member; +import lombok.Data; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +@Data +public class CustomUserDetails implements UserDetails { + private Member member; + + public CustomUserDetails(Member member) { + this.member = member; + } + + @Override + public Collection getAuthorities() { + return null; + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +}