intellij_spring(코딩애플)

intellij JWT랑 Spring Security를 이용한 로그인, logout 경로로 오면 jwt쿠키 삭제 방법, login한 사용자가 login 페이지를 가려하는걸 막는 방법

이이태태영영 2025. 7. 22. 14:05

 

 

 

 

 

 

JWT랑 Spring Security를 이용한 로그인

* 로그인

→ JWT 생성(db와 비교해 맞으면 JwtUtil의 createToken으로 생성)

클라이언트 저장(response.addCookie(cookie))

보호된 경로 설정 및 예외처리(SecurityConfig에 지정된 경로, 예외 발생시 /login 경로로 이동)

클라이언트가 보호된 경로로 요청(jwtUtil의 extractToken으로 검증 및 Authentication 등록)

get요청을 제외한 다른 비동기 요청은 status 프론트에 보냄(JwtFilter)

→ JwtFilter에서 검증이 완료되면, 스프링 시큐리티가 알아서 관리(SecurityConfig) *

 

로그인

script문에서 입력한 username, password를 fetch문을 통해 body에 담아서 보낸다.

에러 발생시 에러 내용 및 상태코드를 알려주고, 성공시 "/" 경로로 보낸다.

<form>
  <input name="username" id="username">
  <input name="password" type="password" id="password">
</form>
<button onclick="loginJWT()">전송</button>

<script>
  function loginJWT(){
    fetch('/login/jwt', {
      method : 'POST',
      headers : {'Content-Type': 'application/json'},
      body : JSON.stringify({
        username : document.querySelector('#username').value,
        password : document.querySelector('#password').value
      })
    })
    .then(response => {
    if (!response.ok) {
      throw new Error('로그인 실패: 상태 코드 ' + response.status);
    }
    return response.text();
    })
    .then(token => {
      console.log('JWT:', token);
      window.location.href = "/";
    })
    .catch(error => {
      alert(error.message);
    });
  }
</script>

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

JWT 생성(db와 비교해 맞으면 JwtUtil의 createToken으로 생성)

body의 내용을 Map형식으로 받아온다.

public String loginJWT(@RequestBody Map<String, String> data, HttpServletResponse response){

 

받아온 username, password로 Authentication 객체를 생성한다.

var authToken = new UsernamePasswordAuthenticationToken(data.get("username"), data.get("password"));

 

위에서 만든 Authentication 객체를 db와 비교하여 성공/실패 판단한다.

성공하면 auth에 인증 완료된 객체가 들어온다.

var auth = authenticationManagerBuilder.getObject().authenticate(authToken);

 

전역 보안 컨텍스트(SecurityContextHolder)에 인증 정보를 저장

SecurityContextHolder.getContext().setAuthentication(auth);

 

전역 보안 컨텍스트(SecurityContextHolder)에 저장된 인증 정보를 JwtUtil.createToken메서드에 전달해서 JWT를 생성

var jwt = JwtUtil.createToken(SecurityContextHolder.getContext().getAuthentication());

 

*JwtUtil의 createToken 메서드 내용 ↓*

public static String createToken(Authentication auth) {
    CustomUser user = (CustomUser) auth.getPrincipal();
    var authorities = auth.getAuthorities().stream().map(a -> a.getAuthority())
            .collect(Collectors.joining(","));

    String jwt = Jwts.builder()
            .claim("id", user.id)
            .claim("username", user.getUsername())
            .claim("displayName", user.displayName)
            .claim("authorities", authorities)
            .issuedAt(new Date(System.currentTimeMillis()))
            .expiration(new Date(System.currentTimeMillis() + 100000)) //유효기간 100초
            .signWith(key)
            .compact();
    return jwt;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

클라이언트 저장(response.addCookie(cookie))

위에서 생성된 JWT를 "jwt" 라는 이름의 cookie 객체를 생성

var cookie = new Cookie("jwt", jwt);

 

JWT만들었을때의 유지 기간이랑 동일하게 기간 설정 (100초)

cookie.setMaxAge(100);

 

쿠키를 자바스크립트로 조작 못하게 (XSS 공격 방지)

cookie.setHttpOnly(true);

 

쿠키가 사이트 전체 경로에 대해 유효하도록 지정 ( /로 시작하는 모든 경로에 지정)

cookie.setPath("/");

 

클라이언트(브라우저)에 저장

response.addCookie(cookie);

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

보호된 경로 설정 및 예외처리(SecurityConfig에 지정된 경로 , 예외 발생시 /login 경로로 이동)

authorize를 받아, 어떤 경로에 어떤 권한을 줄지 설정

http.authorizeHttpRequests((authorize) -> authorize

 

/login, /css/**, /js/** 경로는 누구나 접근 허용

.requestMatchers("/login", "/css/**", "/js/**").permitAll()

 

/detail/, /delete/, /edit/로 시작하는 모든 요청은 로그인(인증)이 된 사용자만 접근 허용

.requestMatchers("/detail/**", "/delete/**", "/edit/**").authenticated()

 

위에서 명시하지 않은 모든 나머지 요청은 접근 허용

.anyRequest().permitAll()

 

인증이나 권한이 없는 상태에서 발생하는 예외를 처리할 방법을 지정

http.exceptionHandling(exception ->

 

인증이 없는 사용자가 인증이 필요한 URL에 접근할 경우 자동으로 /login 페이지로 리다이렉트

exception.authenticationEntryPoint((request, response, authException) -> {
    response.sendRedirect("/login");
})

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

클라이언트가 보호된 경로로 요청(jwtUtil의 extractToken으로 검증 및 Authentication 등록)

SpringConfig에서 보호된 경로를 인식하고 JwtFilter로 이동

 

cookie정보를 받아와 쿠키값이 null인지, "jwt" 이름이 맞는지 확인

Cookie[] cookies = request.getCookies();
String jwtCookie = null;

if (cookies != null) {
    for (Cookie cookie : cookies) {
        if ("jwt".equals(cookie.getName())) {
            jwtCookie = cookie.getValue();
            break;
        }
    }
}

 

JWT 토큰을 JwtUtil의 extractToken메서드로 검증하고, 토큰에 담긴 클레임(정보들)을 꺼냄

Claims claim = JwtUtil.extractToken(jwtCookie);

 

사용자 권한(authorities) 정보를 SimpleGrantedAuthority 객체 리스트로 변환

var arr = claim.get("authorities").toString().split(",");
var authorities = Arrays.stream(arr)
    .map(SimpleGrantedAuthority::new)
    .toList();

 

"id" 값을 꺼내서 Long 타입으로 변환

Long id;
try {
    id = Long.valueOf(claim.get("id").toString());
} catch (NumberFormatException e) {
    id = (long) Double.parseDouble(claim.get("id").toString());
}

 

JWT에서 꺼낸 사용자 정보를 기반으로 CustomUser 객체를 생성(password는 임시로 저장)

var customUser = new CustomUser(
    id,
    claim.get("username").toString(),
    "none",
    authorities
);

 

displayName 값을 꺼내서 customUser 객체의 displayName 필드에 저장

customUser.displayName = claim.get("displayName").toString();

 

매개변수를 이용해 인증 토큰 객체 생성

var authToken = new UsernamePasswordAuthenticationToken(customUser, "", authorities);

 

인증 토큰 객체에 요청(request)의 상세 정보를 붙이는 단계

authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

 

최종 정보를 Authenticatioin에 저장(등록)

SecurityContextHolder.getContext().setAuthentication(authToken);

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

get요청을 제외한 다른 비동기 요청은 status 프론트에 보냄(JwtFilter)

비동기 요청은 HTTP 상태 코드를 반환하지만,

SecurityConfig에서 설정한 redirect는 브라우저가 자동으로 처리하지 않아서 클라이언트가 직접 대응

따라서 JwtFilter에서 조건문을 통해 /delete 경로(비동기)라면 프론트에게 상태코드를 보낸다.

프론트에서 script문으로 상태코드에 따라 /login페이지로 보낼지 설정

if (request.getRequestURI().startsWith("/delete")) {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    return;
}

 

.then(res => {
    if (res.status === 401) {
        window.location.href = '/login';
        throw new Error('Unauthorized');
    }
    return res.text();
})

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

JwtFilter에서 검증이 완료되면, 스프링 시큐리티가 알아서 관리(SecurityConfig) 

해당 경로를 토대로 스프링 시큐리티가 알아서 관리해준다.

//  지정된 경로 보호
http.authorizeHttpRequests((authorize) -> authorize
    .requestMatchers("/login", "/css/**", "/js/**").permitAll()
    .requestMatchers("/detail/**", "/delete/**", "/edit/**").authenticated()
    .anyRequest().permitAll()
);
//  auth 없이 지정된 경로 접속 시 /login으로 이동
http.exceptionHandling(exception ->
    exception.authenticationEntryPoint((request, response, authException) -> {
        response.sendRedirect("/login");
    })
);

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

logout 경로로 오면 jwt쿠키 삭제 방법

SecurityConfig - SecurityFilterChain - 

http.logout(logout -> logout
        .logoutUrl("/logout") // 기본 로그아웃 URL
        .logoutSuccessHandler((request, response, authentication) -> {
            // ✅ JWT 쿠키 삭제
            Cookie cookie = new Cookie("jwt", null);
            cookie.setPath("/");
            cookie.setMaxAge(0);
            response.addCookie(cookie);

            // ✅ 로그아웃 후 리디렉션
            response.sendRedirect("/login");
        })
);

작성

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

login한 사용자가 login 페이지를 가려하는걸 막는 방법

login controller에서 쿠키값을 비교해서 막기

@GetMapping("/login")
public String login(HttpServletRequest request){
    Cookie[] cookies = request.getCookies();
    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if ("jwt".equals(cookie.getName())) {
                // 토큰이 있으면 /로 리다이렉트
                return "redirect:/";
            }
        }
    }
    // 토큰 없으면 로그인 페이지 뷰 반환
    return "login.html";
}