일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
- 빈생성안됨
- 스프링 부트 기능
- jpa에러
- 최종 프로젝트
- @IdClass
- JPA
- github
- Spring Spring boot 차이
- json gson 차이
- REST란
- Q 클래스
- jpa회원가입
- json
- Unsatisfied dependency
- 복합키
- JPA주의사항
- REST API 규칙
- 1차캐시
- 스프링 부트 공식 문서
- uncheck Exception
- Error creating bean with name
- JoinColumn
- 인텔리제이
- spring서버
- git
- Filter
- 스프링부트오류
- jwt메서드
- ERD 작성
- queryDSL
- Today
- Total
Everyday Dev System
JWT HTTP Method 확인 방법 본문
# 문제점 :
로그인 url에 POST를 제외한 다른 Method로 요청이 들어와서 로그인 처리가 된다는 문제점이 도출되었습니다.
GET, DELETE, PUT 등의 요청이 들어오면 로그인 처리가 되지 않도록 거부해야 합니다.
현재 로그인 처리는 아래 클래스에서 수행합니다. (JwtAuthenticationFilter.java)
UsernamePasswordAuthenticationFilter를 상속받았습니다.
<< JwtAuthenticationFilter >>
- JwtUtil 타입의 객체인 jwtUtil을 멤버 변수로 갖습니다.
- JwtAuthenticationFilter 클래스는 아래와 같은 생성자 메서드를 갖습니다.
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/api/user/login");
}
- UsernamePasswordAuthenticationFilter 클래스의 메서드들을 오버라이딩 했습니다.
- attemptAuthentication() ,
successfulAuthentication() ,
unsuccessfulAuthentication()
- attemptAuthentication() ,
- Client에 상태코드와 메시지를 반환하기 위해 HTTPServletResponse 객체에 Setting하는 메서드 생성
private void responseResult(HttpServletResponse response, int statusCode, String message) throws IOException {
String jsonResponse = "{\"status\": " + statusCode + ", \"message\": \"" + message + "\"}";
// Content-Type 및 문자 인코딩 설정
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// PrintWriter 를 사용하여 응답 데이터 전송
PrintWriter writer = response.getWriter();
writer.write(jsonResponse);
writer.flush();
}
#시도 :
WebSecurityConfig.java 내에 @Bean이 달려있는 SecurityFilterChain() 메서드 내부 수정.
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
.requestMatchers("/").permitAll() // 메인 페이지 요청 허가
.requestMatchers("/api/user/signup").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
.requestMatchers("/api/posts/**").permitAll() // '/api/posts/'로 시작하는 요청 모두 접근 허가
.requestMatchers("/api/post/**").permitAll() // '/api/post/'로 시작하는 요청 모두 접근 허가
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
.requestMatchers()에서 Method를 확인해보려고 시도해보았습니다.
그러나, Method 확인이 한개씩 밖에 되지 않아서, Method 별로 모두 접근 거부를 해줘야 했습니다.
아래 코드와 같이 하게되면 코드가 길어진다는 단점이 있습니다.
개발자는 코드를 최대한 효율적으로 쓰는 것이 좋다 생각하여 다른 방법을 생각해보았습니다.
.requestMatchers(HttpMethod.GET,"/api/user/login").denyAll()
.requestMatchers(HttpMethod.DELETE,"/api/user/login").denyAll()
.requestMatchers(HttpMethod.PATCH,"/api/user/login").denyAll()
.requestMatchers(HttpMethod.PUT,"/api/user/login").denyAll()
그래서 HTTP Method를 배열로 넘겨줄 수는 없을까 고민해보았습니다.
먼저, requestMatchers 구현체를 살펴보다가, HttpMethod 구현체도 같이 살펴보았습니다.


HttpMethod 구현체 내부에서 아래의 static 메서드를 발견하였습니다.
public static HttpMethod[] values() {
HttpMethod[] copy = new HttpMethod[values.length];
System.arraycopy(values, 0, copy, 0, values.length);
return copy;
}
WebSecurityConfig.class 내부에 배열을 선언해서 사용해보자 생각했습니다.
아래와 같이 POST 메서드는 제외하고 배열을 생성했습니다.
HttpMethod[] values = new HttpMethod[] { HttpMethod.GET, HttpMethod.HEAD,
HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE };
아래와 같이 접근 제한을 걸어봤지만, 의도한 대로 실행이 되지 않았습니다.
.requestMatchers(Arrays.toString(values),"/api/user/login").denyAll()
전체코드
HttpMethod[] values = new HttpMethod[] { HttpMethod.GET, HttpMethod.HEAD,
HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS, HttpMethod.TRACE };
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
.requestMatchers("/").permitAll() // 메인 페이지 요청 허가
.requestMatchers("/api/user/signup").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
.requestMatchers("/api/posts/**").permitAll() // '/api/posts/'로 시작하는 요청 모두 접근 허가
.requestMatchers("/api/post/**").permitAll() // '/api/post/'로 시작하는 요청 모두 접근 허가
.requestMatchers(Arrays.toString(values),"/api/user/login").denyAll()
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
그러나, 의도한 대로 실행이 되지 않았습니다.
HttpMethod 구현체 내부에 있는 values 메서드는 이때 활용하는 것이 아닌것 같았다..
# 해결 방법
JwtAuthenticationFilter 내부에 attemptAuthentication() 메소드에서 메서드를 판별하면 됩니다.
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals(HttpMethod.POST.name())) {
// 해당 url 로 들어온 요청의 Method 가 POST 가 아니라면
try {
responseResult(response,400,"HTTP Method Error");
return null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
-- 중략 --
}
클라이언트로 부터 들어온 요청을 HttpServletRequest request에 받습니다.
해당 request 객체의 메서드가 POST인지 아닌지 판별합니다.
POST일 경우, response에 상태코드와 응답 메시지를 setting 한 후에 return null을 하여
해당 메서드를 종료 시킬 수 있습니다.
그렇게 되면 로그인 요청은 POST 메서드일 경우에만 실행됩니다.
결과 :


전체 코드는 다음과 같습니다.
JwtAuthenticationFilter.java
package com.sparta.myblog.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.myblog.dto.UserRequestDto;
import com.sparta.myblog.entity.UserRoleEnum;
import com.sparta.myblog.jwt.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.IOException;
import java.io.PrintWriter;
@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/api/user/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals(HttpMethod.POST.name())) {
// 해당 url 로 들어온 요청의 Method 가 POST 가 아니라면
try {
responseResult(response,400,"HTTP Method Error");
return null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
try {
UserRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), UserRequestDto.class);
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
requestDto.getUsername(),
requestDto.getPassword(),
null
)
);
} catch (IOException e) {
log.error(e.getMessage());
throw new RuntimeException(e.getMessage());
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();
String token = jwtUtil.createToken(username, role);
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
// 응답 상태 코드와 메시지 설정
responseResult(response, 200, "로그인 성공");
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
responseResult(response, 400, "회원을 찾을 수 없습니다.");
}
// Client 에 반환할 msg, status 세팅 메서드
private void responseResult(HttpServletResponse response, int statusCode, String message) throws IOException {
String jsonResponse = "{\"status\": " + statusCode + ", \"message\": \"" + message + "\"}";
// Content-Type 및 문자 인코딩 설정
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// PrintWriter 를 사용하여 응답 데이터 전송
PrintWriter writer = response.getWriter();
writer.write(jsonResponse);
writer.flush();
}
}
JwtUtil.java
package com.sparta.myblog.jwt;
import com.sparta.myblog.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
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);
}
// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID)
.claim(AUTHORIZATION_KEY, role) // 사용자 권한
.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
// header 에서 JWT 가져오기
public String getJwtFromHeader(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}
'내배캠 주요 학습 > TIL : Today I Learned' 카테고리의 다른 글
Spring Security (0) | 2023.07.07 |
---|---|
협업을 할 때에 궁금한 사항들 및 유의사항 (0) | 2023.07.04 |
JPA 외래키 설정, 복합키일 경우 (0) | 2023.06.27 |
스프링 오류 : Error creating bean with name 해결 (0) | 2023.06.15 |
TIL 로컬에서 git 버전관리 파일이 이상할 때 (0) | 2023.06.12 |