Skip to content

Commit

Permalink
[bug] spring security 403 error 해결 (#28)
Browse files Browse the repository at this point in the history
* #15 fix: 403 error 해결 (인증 필터 수정)

* #15 fix : merge 중 spring security 라이브러리 누락된 부분 추가
  • Loading branch information
Jnghne authored Nov 24, 2023
1 parent 0e5f355 commit 99a7f7e
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 145 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

This file was deleted.

41 changes: 0 additions & 41 deletions src/main/java/codesquad/fineants/spring/config/FilterConfig.java

This file was deleted.

71 changes: 71 additions & 0 deletions src/main/java/codesquad/fineants/spring/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
32 changes: 0 additions & 32 deletions src/main/java/codesquad/fineants/spring/config/WebConfig.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String> 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<String> 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<Object> 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<String> 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<String> 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<Object> body = ApiResponse.error(errorCode);
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(body));
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 99a7f7e

Please sign in to comment.