작성
·
697
·
수정됨
0
안녕하세요.
이제 개발 공부를 시작한 학생입니다.
이번에 Security 와 Jwt 에 대해서 배웠는데, 실습 간 문제가 발생하여 부끄럽지만,,, 질문드립니다.
문제의 핵심은 "토큰을 헤더에 담아서 요청했는데, 왜 토큰 값이 전달되지 않는가 ?" 입니다...
먼저 코드 보여드리겠습니다.
WebSecurityConfig
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf((csrf) -> csrf.disable());
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
.requestMatchers("/").permitAll() // 메인 페이지 요청 허가
.requestMatchers("/user/**").permitAll() // '/user/'로 시작하는 요청 모두 접근 허가
.anyRequest().authenticated() // 그 외 모든 요청 인증처리
);
http.formLogin((formLogin) ->
formLogin
.loginPage("/user/login-page").permitAll()
.defaultSuccessUrl("/test", true) // 로그인 성공 시 /test 경로로 리다이렉트
);
// 필터 관리
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
우선 config 쪽에서는 리소스 및 기본페이지, user 로 시작하는 페이지는 인증 없이 접근 가능하도록 했습니다.
.anyRequest().authenticated()
<- 이걸 설정하면 나머지 요청들은 인증이 필요하다는 것으로 알고 있습니다.
JwtAuthenticationFilter
인증 필터는 다음과 같습니다.
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
log.info("로그인 성공 및 JWT 생성");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(new ApiResponseDto("로그인 성공", HttpStatus.OK.value())));
String loginId = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();
String accessToken = jwtUtil.createAccessToken(loginId, role);
String refreshToken = jwtUtil.createRefreshToken(loginId);
redisService.saveRefreshToken(loginId, refreshToken);
// Access Token 헤더에 저장
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, accessToken);
// Refresh Token 쿠키에 저장
Cookie refreshCookie = new Cookie("refreshToken", refreshToken);
refreshCookie.setHttpOnly(true);
refreshCookie.setSecure(true);
refreshCookie.setPath("/");
response.addCookie(refreshCookie);
}
Access 토큰과 Refresh 토큰을 구현해보고 싶어서 위와 같이 구현하였습니다.
login.html
<h1>로그인</h1>
<form id="loginForm">
<label for="loginId">아이디:</label>
<input type="text" id="loginId" name="loginId" required>
<br>
<label for="password">비밀번호:</label>
<input type="password" id="password" name="password" required>
<br>
<button type="button" id="loginButton">로그인</button>
</form>
<script>
$(document).ready(function() {
$('#loginButton').on('click', function() {
const loginId = $('#loginId').val();
const password = $('#password').val();
$.ajax({
type: "POST",
url: "/user/login",
contentType: "application/json",
data: JSON.stringify({ loginId: loginId, password: password }),
success: function(data, textStatus, xhr) {
// 응답 헤더에서 Access Token 추출
const accessToken = xhr.getResponseHeader('Authorization');
if (accessToken) {
// 로컬 스토리지에 Access Token 저장
localStorage.setItem('Authorization', accessToken);
// 모든 AJAX 요청에 대해 Authorization 헤더를 설정
setupAjaxRequests(accessToken);
// 로그인 성공 후 리다이렉션
window.location.href = '/test';
} else {
alert('Authorization token not found');
}
},
error: function(jqXHR, textStatus, errorThrown) {
alert('로그인 실패: ' + textStatus);
}
});
});
});
function setupAjaxRequests(token) {
$.ajaxSetup({
headers: { 'Authorization': token }
});
}
</script>
</body>
</html>
그래서 실제로 위와 같은 페이지를 만들어서 로그인을 시도해보면 로컬스토리지와 쿠키에 각각 토큰이 담기는 것을 확인할 수 있었습니다.
그런데 여기서 문제가 있는 게, window.location.href = '/test';
아것이 동작이 안된다는 것입니다...
분명 '/test'; 라고 명시해두면
TestController
@Controller
public class TestController {
@GetMapping("/test")
public String testPage() {
return "test";
}
}
해당 api 가 호출되어 test 라는 페이지가 떠야하는 게 아닌가요 ? ㅠㅠㅠㅠ
test page 는 우선 body 에 "테스트 페이지입니다" 정도로만 제작해두었습니다.
왜 안될까,,, 고민하다가 우선 인가 필터 쪽 로그를 찍어서 확인했습니다.
JwtAuthorizationFilter
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
// 헤더에서 토큰 추출
log.info("헤더에서 토큰 추출");
String tokenValue = jwtUtil.getJwtFromHeader(req);
log.info("토큰 : " + tokenValue);
if (StringUtils.hasText(tokenValue)) {
// 토큰 유효성 검사
if (!jwtUtil.validateToken(tokenValue)) {
log.info("Token Error");
return;
}
Claims info = jwtUtil.getUserInfoFromToken(tokenValue);
try {
setAuthentication(info.getSubject());
} catch (Exception e) {
log.error(e.getMessage());
return;
}
}
else {
log.info("토큰이 없습니다.");
}
filterChain.doFilter(req, res);
}
이와 같으며, 로그인 성공 후 window.location.href = '/test';
동작할때와 그냥 주소창에 localhost8080/test 로 접근해 본 결과
로그가 이렇게 찍히는 겁니다...
2024-02-25T22:49:52.276+09:00 INFO 54640 --- [nio-8081-exec-2] JWT 검증 및 인가 : 헤더에서 토큰 추출
2024-02-25T22:49:52.276+09:00 INFO 54640 --- [nio-8081-exec-2] JWT 검증 및 인가 : 토큰 : null
2024-02-25T22:49:52.276+09:00 INFO 54640 --- [nio-8081-exec-2] JWT 검증 및 인가 : 토큰이 없습니다.
대체 왜 토큰이 없는 걸까....
분명 로컬 스토리지에 Access Token 저장된 거 개발자 도구로 확인했고, 이를 다시 Header 담아서 전송하는 것까지 login.html 쪽에 구현했다고 생각했는데,,, 이게 의도한 방향대로 흘러가질 않습니다.
대체 왜 토큰이 비어있으며, test 라는 페이지에 접속하지 못하는 걸까요...
제가 놓친 부분이 있을까요 ?
답변 1
0
안녕하세요? 도움이 될 지 모르겠지만 답변 남겨봅니다.
먼저 WebSecurityConfig 클래스에서 anyRequests().authenticated()
가 어디까지 제한을 거는 지 알고 가면 좋을 듯 합니다.
저는 저 제한이 어디까지 적용되는지 몰라서 저라면 requestMatcher(new AntPathRequestMatcher("/test/**")).authenticated() 처럼 인증이 필요한 부분에 인증이 되도록 걸어둘 것 같고 그러면 코드 의미가 더 명료해질 것 같습니다.
그리고 토큰이 콘솔 부분으로 잘 찍힌다면 발급이 잘 된 것으로 생각됩니다. ㅎㅎ..