본문 바로가기
Study/Spring

Spring Security + JWT #5

by 네빛나래 2024. 1. 11.
버전 및 의존성

IntelliJ IDEA 2023.3.2 (Ultimate Edition)
JDK 17
SpringBoot 3.2.1
Spring Security 6.2.1
JWT 0.9.1
Spring Data JPA - MariaDB
LomBok

권한에 따른 접속페이지를 구별하기 위해서 USER와 ADMIN을 나눴다.

 

그리고 테이블 내에 USER Role을 가진 사용자를 생성해서 구별할 수 있도록 했다.

 

그리고 JWT를 로컬스토리지에 저장해서 사용하려고 했었는데 이어서 계속 서치 해본 결과 여러 가지 취약점이 있다고 한다.

그리고 쿠키에 저장하는 것도 마찬가지로 많은 취약점이 있지만 해당사항 같은 경우엔 csrf취약점이나 https로만 보낼 수있도록해서 방어가 가능하다고 한다.

 

또한 쿠키에 저장하게 되면 매 요청마다 알아서 헤더에 해당 토큰을 저장해서 보내기 때문에 별도로 보내주는 방식을 쓰지 않아도 괜찮은 것 같다.

 

그리고 매번 해당 메서드가 실행되었는지, 혹은 해당 단계가 몇 번 실행되었는지 확인하기 위해서 System.out.pirnt를 써서 확인했었는데 Log 라이브러리들을 이용해서 간단하게 콘솔단에서 확인할 수 있었다.

 

Log를 사용할 때엔 로그에 레벨이 있는데

- 로그 레벨 : TRACE > DEBUG > INFO > WARN > ERROR

 

이런 식으로 나뉘어있다곤 한다. 

 

@Override
	protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		log.info ("doFilterInternal");
		String token = null;



		Cookie[] cookies = request.getCookies ();
		if (cookies != null) {
			for (Cookie cookie : cookies) {
				if ("jwtToken".equals (cookie.getName ())) {
					token = cookie.getValue ();
					break;
				}
			}
		}
		log.info ("doFilterInternal // token: " + token);

		if (token == null || jwtUtil.isExpired (token)) {
			filterChain.doFilter (request, response);
			return;
		}

		String username = jwtUtil.getUsername (token);
		String role = jwtUtil.getRole (token);

		log.info ("doFilterInternal // username: " + username + " role: " + role);

		UserEntity userEntity = new UserEntity ();
		userEntity.setUsername (username);
		userEntity.setPassword ("temp_password");  // 임시 비밀번호
		userEntity.setRole (role);

		CustomUserDetails customUserDetails = new CustomUserDetails (userEntity);
		Authentication authToken = new UsernamePasswordAuthenticationToken (customUserDetails, null, customUserDetails.getAuthorities ());

		SecurityContextHolder.getContext ().setAuthentication (authToken);
		filterChain.doFilter (request, response);
	}

 

또한 로그인 성공했을 때 쿠키에 토큰을 저장하기 위해서

 

<form id="loginForm">
    <label for="username"></label><input type="text" id="username" placeholder="Username">
    <label for="password"></label><input type="password" id="password" placeholder="Password">
    <button type="submit" onclick="login()">로그인</button>
</form>
<script>
    document.getElementById('loginForm').addEventListener('submit', function (event) {
        event.preventDefault();
        login();
    });

    function login() {
        const username = document.getElementById('username').value;
        const password = document.getElementById('password').value;

        fetch('/login', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: 'username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password)
        })
            .then(response => {
                if (!response.ok) {
                    throw new Error('로그인 실패');
                }
                return response.json();
            })
            .then(data => {
                console.log(data);
                const cookieToken = data.token;
                setCookie('jwtToken', cookieToken, 1);
                console.log('로그인 성공, 토큰:', cookieToken);
            })
            .then(() =>
                window.location.href = '/main'
            )
            .catch(error => {
                console.error('Error:', error);
            });
    }

    function setCookie(name, value, days) {
        let expires = "";
        if (days) {
            const date = new Date();
            date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
            expires = "; expires=" + date.toUTCString();
        }
        document.cookie = name + "=" + (value || "") + expires + "; path=/";
    }
</script>

다음과 같이 수정했고 또한 권한확인 같은 버튼을 빼고 로그인 시 실행되도록 했다.

 

사실 요청을 보낼 때 무조건적으로 POST방식인지 GET방식인지 지정해 주고 보내줘야 한다고 생각했었는데

그냥 window.location.href로 보내게 되면 GET방식으로 날아가는 것 같다.

 

위와 같이 쿠키에 저장하는 것을 완료했다.

 

 

 

그다음 목표는 이 상태에서 관리자 권한을 가진 사용자만 관리자 페이지로 이동가능하고 마찬가지로 사용자페이지는 USER만 진입가능하도록 하는 것이다.

그리고 권한 없는 사용자라면 이전 페이지로 리다이렉트 되도록 하는 것.

 

그런데 관리자 같은 경우엔 관리자페이지 진입이 정상적으로 가능하고, 사용자 페이지에 진입 시 로그인페이지로 넘어가게 되는데

사용자는 어떤 페이지에 진입하더라도 진입에 실패하는 게 문제다.

 

테이블에 다음과 같이 유저의 role이 설정되어 있고

 

http
       .authorizeHttpRequests ((auth) -> auth
             .requestMatchers ("/LoginPage/*", "/", "/join", "/login","/favicon.ico").permitAll ()
             .requestMatchers ("/main").hasAnyRole ("USER", "ADMIN")
             .requestMatchers ("/user").hasRole ("USER")
             .requestMatchers ("/admin").hasRole ("ADMIN")
             .anyRequest ().authenticated ());

SecurityConfig 내에서도 /user에 접근가능하도록 되어있는데 왜 안 되는지 잘 모르겠었는데

 

UserPagecontroller에서 USER_PAGE로 보내주는 부분에서 redirect를 넣어놔서 그랬던 것이었다.

 

해당 부분을 지워주니 정상적으로 작동하게 되었다,

 

그리고 사용자 권한에따른 접근 실패시에는 AccessDeniedHandler를 만들어서 SeucrityConfig에 추가 해주어 /accessDeniedPage으로 리다이렉트 되도록 만들었다.

http
       .exceptionHandling (auth -> auth
             .accessDeniedHandler (customAccessDeniedHandler));
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle (HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
       response.sendRedirect ("/accessDenied");
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
접근 거부
</body>
</html>

 

다시 실행해서 예상대로 나오는지 확인해보면

 

메인페이지
사용자 페이지 버튼 진입시
관리자 페이지 진입시

다행이 아무런 문제없이 정상적으로 실행된다.

 

이제 해야할것은 가입페이지, 관리자 게시판, 사용자게시판을 다따로만들어서 진입하도록 만드는것이다.

'Study > Spring' 카테고리의 다른 글

Spring Security + JWT # 6  (0) 2024.01.11
Spring Security + JWT #4  (0) 2024.01.08
Spring Security + JWT #3  (0) 2024.01.07
Spring Security + JWT #2  (0) 2024.01.07
Spring Security + JWT Token #1  (0) 2024.01.07