~☆~ 우하하!!~ 개발블로그

[연재] SpringBoot diary - jwt 로그인으로 변경, 로그아웃까지 수정 본문

SpringBoot

[연재] SpringBoot diary - jwt 로그인으로 변경, 로그아웃까지 수정

iwoohaha 2024. 11. 15. 10:38
728x90
반응형

현재 diary 웹 프로그램은 세션 방식의 로그인을 사용하고 있어. 이번 포스트에서는 토큰 방식의 로그인으로 변경해보려고 해.

세션 방식의 로그인은 세션이 유지되는 동안 로그인이 유지되는 특징이 있어. 세션이 끊어지면 로그인을 다시 해주어야 하지. 그래서 remember-me 라는 쿠키를 사용해서 세션이 끊어지더라도 다시 로그인없이 웹 프로그램을 사용할 수 있게 하는 방법을 사용했었지. ☞ SpringBoot: study.diary – Spring Security remember-me, logout 처리 재정리

토큰 방식의 로그인은 로그인에 성공하면 토큰을 발급받는데, 이 토큰을 이용해서 사용자 인증을 확인하기 때문에 세션과는 무관해. 웹 프로그램에서는 이 토큰을 쿠키로 저장해두었다가 사용하는 방법으로 재사용하는데, 앱 프로그램에서는 별도의 저장소에 저장해둘 수가 있지.

사실 앱으로 개발하기 위한 과정 중의 하나로 토큰 방식의 로그인으로 변경하려고 하는 중인거야.

jwt 는 JSON Web Token 의 이니셜인데, jwt 에 대해서는 인터넷 검색을 통해서 별도로 학습해보길 바래.


세션 방식의 로그인 (현재상황 복습)

현재의 세션 방식 로그인에서는 SecurityConfig.java 에서 Form Login 방식의 설정에서 로그인 페이지를 /login 으로 설정했고, 로그인 프로세스를 처리하는 URL 을 /process_login 으로 설정했었지.

SecurityConfig.java
...
                .formLogin(form -> form
                        .loginPage("/login")
                        .loginProcessingUrl("/process_login")
                        .usernameParameter("email")
                        .failureHandler(customAuthFailureHandler)
                        .permitAll()
                )
...

위와 같은 설정으로 /login URL 핸들러는 login.html 템플릿으로 연결시켰고, login.html 에서는 form action 으로 /process_login 을 지정해 두었던걸 기억할거야.

LoginController.java
...
    @GetMapping("/login")
    public String login(@RequestParam(value="error", required = false) String error
            , @RequestParam(value="message", required = false) String message
            , @AuthenticationPrincipal User user
            , Model model) {
...
        return "login";
    }
}
login.html
...
<main class="form-signin w-100 m-auto">
    <th:block th:if="${error}">
        <div th:if="${message}" class="alert alert-danger" role="alert" th:text="${message}"></div>
    </th:block>
    <form th:action="@{/process_login}" method="POST">
        <h1 class="h3 mb-3 fw-normal">Please sign in</h1>
...

위와 같은 설정으로 실제 사용자 인증 후 로그인되는 처리는 LoginService (UserDetailsService 인터페이스의 구현) 의 loadUserByUsername 함수에서 이루어졌었지.

LoginService.java
...
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Member> member = memberMapper.GetMember(username);
        if (member.isEmpty())
            throw new UsernameNotFoundException("해당 이름의 사용자가 존재하지 않습니다: " + username);
        return new User(member.get());
    }
}

물론 사용자 인증 처리의 대부분은 Spring Security 프레임워크가 다 해주고 이 프로그램에 한정된 코딩만을 추가해준 것일 뿐이야.

이제 코드 이곳 저곳을 바꿔가면서 세션 방식의 로그인을 jwt 토큰 방식의 로그인으로 변경해볼께.

의존성 라이브러리 추가

jwt 토큰을 이용한 인증을 구현하려면 아래와 같은 3가지 의존성 라이브러리를 추가해야 해.

build.gradle
...
dependencies {
	//JWT
	implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
	implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
...

세션 방식의 폼 로그인을 처리하던 /process_login 대신에 /signin URL 을 새로 만들어서 이 URL 이 jwt 토큰방식의 로그인을 처리하도록 변경해보려고 해. 그러기 위해서 login.html 에서 form action URL 을 다음과 같이 변경했어.

login.html
...
<!--    <form th:action="@{/process_login}" method="POST">-->
    <form th:action="@{/signin}" method="POST">
...

현재는 /signin URL 핸들러가 없기 때문에 LoginController 클래스에 아래와 같이 만들어줄께.

LoginController.java
...
@Controller
public class LoginController {
...
    @GetMapping("/login")
    public String login(@RequestParam(value="error", required = false) String error
            , @RequestParam(value="message", required = false) String message
            , @AuthenticationPrincipal User user
            , Model model) {
...
    }

    @PostMapping("/signin")
    public String signIn() {

        return "redirect:/login";
    }
}

일단은 /signin URL 로 들어오면 /login URL 로 redirect 되도록 기본 코드를 작성했는데, 차근차근 수정해볼께.

아참, /signin URL 이 처음 추가되는 것이니까, SecurityConfig 에서 허용 URL 목록에 추가하는 것을 잊지마.

SecurityConfig.java
...
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/login", "/process_login", "/signin"
                                , "/signup"
                                , "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**"
                        ).permitAll()
                        .anyRequest().authenticated()
                )
...

/signin URL 핸들러 함수에 아직 아무것도 작성하지 않고 /login URL 로 redirect 되도록 해놨기 때문에 프로그램을 실행시켜서 로그인을 하더라도 계속 로그인 화면만 표시가 될거야.

이제 /signin URL 핸들러 함수에 구현해줄 내용을 확인해볼께.

LoginController.java
...
    @PostMapping("/signin")
    public String signIn() {
        // 입력받은 로그인 정보로 사용자 정보를 확인하여 인증한다.
        // 사용자 인증에 성공하면 쿠키로 jwt 토큰을 발급한다.
        return "redirect:/login";
    }
}

사용자정보 확인하기

login.html 에서 사용자가 입력한 정보(이메일 주소와 패스워드)를 전달받기 위해서 signin 함수에 파라미터를 추가해볼께.

LoginController.java
...
    @PostMapping("/signin")
    public String signIn(String email, String password) {
        System.out.println("email=" + email);
        System.out.println("password=" + password);
        // 입력받은 로그인 정보로 사용자 정보를 확인하여 인증한다.
        // 사용자 인증에 성공하면 쿠키로 jwt 토큰을 발급한다.
        return "redirect:/login";
    }
}

login.html 에서 form 안의 email, password 항목의 input 태그 name 에 따라 위와 같이 파라미터 이름을 정한거야.

login.html
...
    <form th:action="@{/signin}" method="POST">
        <h1 class="h3 mb-3 fw-normal">Please sign in</h1>
        <div class="form-floating">
            <input type="email" class="form-control" id="floatingInput" name="email" placeholder="name@example.com">
            <label for="floatingInput">Email address</label>
        </div>
        <div class="form-floating">
            <input type="password" class="form-control" id="floatingPassword" name="password" placeholder="Password">
            <label for="floatingPassword">Password</label>
        </div>
...

여기까지 코드를 작성한 상태에서 실행시켜보면 아래와 같이 email 과 password 값이 들어오는 것을 확인할 수 있어.


그러면 입력받은 값을 가지고 본격적으로 사용자인증을 수행하는 코드를 작성해볼께.

현재의 LoginService 클래스는 UserDetailsService 인터페이스를 구현한 클래스인데, loadUserByUsername 함수를 override 해 놓은 상태지. 이 함수에 사용자이름, 여기에서는 email 주소를 파라미터로 넘기면 DB에서 해당 주소에 대한 사용자를 조회해서 리턴해주고 있어.

LoginService.java
...
@Service
public class LoginService implements UserDetailsService {
...
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Member> member = memberMapper.GetMember(username);
        if (member.isEmpty())
            throw new UsernameNotFoundException("해당 이름의 사용자가 존재하지 않습니다: " + username);
        return new User(member.get());
    }
}

이 함수를 그대로 사용해볼께.

LoginController.java
...
@Controller
public class LoginController {

    private final LoginService loginService;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;

    public LoginController(LoginService loginService, AuthenticationManagerBuilder authenticationManagerBuilder) {
        this.loginService = loginService;
        this.authenticationManagerBuilder = authenticationManagerBuilder;
    }
...
    @PostMapping("/signin")
    public String signIn(String email, String password) {
        System.out.println("email=" + email);
        System.out.println("password=" + password);

        try {
            // 입력받은 로그인 정보로 사용자 정보를 확인하여 인증한다.
            // 1. 사용자정보 존재여부 확인
            UserDetails userDetails = loginService.loadUserByUsername(email);

            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                    new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
            // 2. 인증 가능 여부 확인(패스워드 일치여부 확인)
            Authentication authentication = authenticationManagerBuilder.getObject().authenticate(usernamePasswordAuthenticationToken);
            System.out.println(authentication);
        } catch (UsernameNotFoundException e) {
            String errorMessage = "아이디 또는 비밀번호가 맞지 않습니다. 다시 확인해 주세요.";
            errorMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8); /* 한글 인코딩 깨진 문제 방지 */
            return "redirect:/login?error=true&message="+errorMessage;
        } catch (BadCredentialsException e) {
            String errorMessage = "아이디 또는 비밀번호가 맞지 않습니다. 다시 확인해 주세요.";
            errorMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8); /* 한글 인코딩 깨진 문제 방지 */
            return "redirect:/login?error=true&message="+errorMessage;
        }

        // 사용자 인증에 성공하면 쿠키로 jwt 토큰을 발급한다.
        return "redirect:/login";
    }
}

LoginService 의 loadUserByUsername 은 email 이 일치하는 항목을 가져오는 작업까지 수행하고, authenticationManagerBuilder.getObject().authenticate(usernamePasswordAuthenticationToken); 를 이용하여 패스워드가 일치하는지를 확인하는거야.

loadUserByUsername 에서 일치하는 정보가 없을 경우 UsernameNotFoundException 예외가 발생하고, 패스워드가 일치하지 않을 경우에는 DaoAuthenticationProvider.class 에서 BadCredentialsException 이 발생하게 되지.

email 아이디와 패스워드가 일치하는 항목이 있을 경우에는 예외가 발생하지 않고 정상적으로 인증로직이 수행할거야.

jwt 토큰 발급하기

이제 사용자 인증이 성공적으로 완료가 되었으면 jwt 토큰을 발급해야 할 차례야.

토큰 발급과 검증 로직을 JwtUtil 이라는 클래스를 하나 만들어서 작성해볼께.

JwtUtil.java
package com.woohahaapps.study.diary.jwt;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.stream.Collectors;

@Component
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secretKey;
    private Key key;

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    public String createToken(Authentication authentication) {
        // 권한 가져오기
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = (new Date()).getTime();
        Date accessTokenExpiresIn = new Date(now + 86400000);

        // Access Token 생성
        return Jwts.builder()
                .setSubject(authentication.getName())
                .claim("auth", authorities)
                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }
}

코드에 대해서 간략하게 설명하자면, 토큰을 암호화하는데 사용할 키값(String secretKey)은 jwt.secret 속성값을 사용하도록 설정했어.

application.yml
jwt:
  secret: 98f21094cbf1a6df25a8d03e94e4ae348d9c007917a6ea30db89939fa0635444

이 값은 openssl rand -hex 32 명령어로 생성할 수 있는데, openssl 이 설치되어 있는 리눅스에서 실행하거나 아래 포스트를 참고해서 윈도우에 openssl 을 설치해서 실행하면 돼.

Windows 윈도우 10 에 OpenSSL 을 설치하는 방법

createToken 함수로 전달하는 인증정보를 jwt 토큰값 생성할 때 사용하는데, setSubject 에 email 주소값이 전달되고, “auth” 라는 이름의 claim 으로 권한 정보를 설정하고 있어. 이 토큰의 유효시간은 1일(=86,400,000밀리초)로 설정하고 있지.

LoginController 에서 JwtUtil 을 사용하는 코드를 작성해볼께.

LoginController.java
...
@Controller
public class LoginController {

    private final LoginService loginService;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final JwtUtil jwtUtil;

    public LoginController(LoginService loginService, AuthenticationManagerBuilder authenticationManagerBuilder, JwtUtil jwtUtil) {
        this.loginService = loginService;
        this.authenticationManagerBuilder = authenticationManagerBuilder;
        this.jwtUtil = jwtUtil;
    }
...
    @PostMapping("/signin")
    public String signIn(String email, String password, HttpServletResponse response) {
        System.out.println("email=" + email);
        System.out.println("password=" + password);

        try {
            // 입력받은 로그인 정보로 사용자 정보를 확인하여 인증한다.
            // 1. 사용자정보 존재여부 확인
            UserDetails userDetails = loginService.loadUserByUsername(email);

            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                    new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
            // 2. 인증 가능 여부 확인(패스워드 일치여부 확인)
            Authentication authentication = authenticationManagerBuilder.getObject().authenticate(usernamePasswordAuthenticationToken);
            System.out.println(authentication);

            // 사용자 인증에 성공하면 쿠키로 jwt 토큰을 발급한다.
            String jwtToken = jwtUtil.createToken(authentication);
            System.out.println(jwtToken);
            
            Cookie cookie = new Cookie("Authorization", "Bearer=" + jwtToken);
            cookie.setMaxAge(24 * 60 * 60);// 1일동안 유효
            cookie.setPath("/");
            cookie.setHttpOnly(true);
            
            response.addCookie(cookie);
        } catch (UsernameNotFoundException e) {
            String errorMessage = "아이디 또는 비밀번호가 맞지 않습니다. 다시 확인해 주세요.";
            errorMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8); /* 한글 인코딩 깨진 문제 방지 */
            return "redirect:/login?error=true&message="+errorMessage;
        } catch (BadCredentialsException e) {
            String errorMessage = "아이디 또는 비밀번호가 맞지 않습니다. 다시 확인해 주세요.";
            errorMessage = URLEncoder.encode(errorMessage, StandardCharsets.UTF_8); /* 한글 인코딩 깨진 문제 방지 */
            return "redirect:/login?error=true&message="+errorMessage;
        }

        return "redirect:/login";
    }
}

jwt 토큰을 발급받아서 1일동안만 유효한 쿠키를 발급하여 response 에 담아 내리는 로직을 작성했어.

프로그램을 빌드해서 실행시켜보면 정상적인 로그인 정보를 사용해서 로그인했을 때 아래 그림에서와 같이 Bearer= 으로 시작되는 쿠키가 발급된 것을 확인할 수가 있지.


그런데, 아직은 이 쿠키를 인증정보로 사용하는 로직을 작성하지 않았기 때문에 로그인 화면이 그대로 표시가 되고 있어.


jwt 인증 필터 추가

이번에는 Spring Security 의 프레임워크에서 동작하는 jwt 인증 필터를 추가해볼께.

OncePerRequestFilter 를 상속받아 JwtAuthenticationFilter 클래스를 아래와 같이 작성해보자.

JwtAuthenticationFilter.java
package com.woohahaapps.study.diary.jwt;

import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. Request Header 또는 쿠키에서 JWT 토큰 추출
        String token = jwtUtil.resolveToken(request);

        System.out.println(request.getRequestURI());

        // 2. validateToken으로 토큰 유효성 검사
        if (token != null && jwtUtil.validateToken(token)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장
            Claims info = jwtUtil.getUserInfoFromToken(token);
            setAuthentication(info.getSubject());
        }

        filterChain.doFilter(request, response);
    }
    
    public void setAuthentication(String username) {
        /*jwt 인증 성공 시*/
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = jwtUtil.createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }
}

OncePerRequestFilter 를 상속받도록 코딩하면 IntelliJ 에서 OncePerRequestFilter 의 doFilterInternal 을 오버라이드할 수 있도록 코드를 준비해주지.

Request Header 또는 쿠키에서 JWT 토큰을 추출하는 resolveToken 이나 토큰값의 유효성을 검사하는 validateToken, 토큰에서 사용자정보를 가져오는 getUserInfoFromToken 함수등이 JwtUtil 클래스에 아직 작성되어 있지 않으니 아래와 같이 추가해보자.

JwtUtil.java
...
@Component
@Slf4j
public class JwtUtil {

    @Value("${jwt.secret}")
    private String secretKey;
    private Key key;

    private final LoginService loginService;

    public JwtUtil(LoginService loginService) {
        this.loginService = loginService;
    }
...
    public String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");

        if (bearerToken == null) {// 헤더에 값이 없다면 쿠키 확인
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (Cookie c : cookies) {
                    String name = c.getName();
                    if (name.equals("Authorization")) {
                        bearerToken = c.getValue();
                    }
                }
            }
        }
        System.out.println(bearerToken);

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer=")) {
            return bearerToken.substring(7);
        }
        return null;
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.info("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }

    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }

    public Authentication createAuthentication(String username) {
        UserDetails userDetails = loginService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

Jwt 인증 필터를 작성했으니 이 필터가 동작할 수 있도록 설정을 해주어야 하는데 SecurityConfig 에서 아래와 같이 코딩해주면 되겠어.

SecurityConfig.java
...
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomAuthFailureHandler customAuthFailureHandler;
    private final LoginService loginService;
    private final JwtUtil jwtUtil;

    public SecurityConfig(CustomAuthFailureHandler customAuthFailureHandler, LoginService loginService, JwtUtil jwtUtil) {
        this.customAuthFailureHandler = customAuthFailureHandler;
        this.loginService = loginService;
        this.jwtUtil = jwtUtil;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
...
                .httpBasic(Customizer.withDefaults())// swagger 에서는 필수로 기록해야 한다.
                // JWT 인증을 위하여 직접 구현한 필터를 UsernamePasswordAuthenticationFilter 전에 실행
                .addFilterBefore(new JwtAuthenticationFilter(jwtUtil)
                        , UsernamePasswordAuthenticationFilter.class)
                .build();
    }
...

여기까지 작성한 후에 프로그램을 실행시켜보면 이미 생성되어 있는 쿠키정보를 읽어들여서 로그인이 될거야.

로그아웃

기존의 세션방식 폼 로그인을 사용했을 때 별도의 로그아웃 로직을 작성해주지 않았었지. jwt 토큰을 쿠키로 저장해놓으니까 로그아웃할 때 이 쿠키를 지워주어야만 해. 그렇게 하지 않으면 반복적으로 이 토큰을 사용해서 로그인된 것처럼 동작하게 되거든.

현재 로그아웃은 navigator.html 에 post method 로 /logout URL 을 호출하도록 구현되어 있지.

fragments/navigator.html
...
                    <form th:action="@{/logout}" method="post">
                        <button class="btn btn-primary w-100 py-2" type="submit">Logout</button>
                    </form>
...

/logout URL 에 대한 핸들러 함수를 별도로 작성해주지 않았는데도 이 기능이 동작했던 것은 Spring Security 에 기본적인 /logout URL 의 post method 에 대한 핸들러가 준비되어 있기 때문이었어.

/logout URL 을 post 가 아닌 get 방식으로 호출하도록 수정하더라도 Spring Security 에 의해서 로그아웃처리되는 로직이 수행되는건 마찬가지야. 그래서 /logout URL 핸들러를 작성해주더라도 해당 핸들러 함수가 호출되지는 않더라고.

그래서 아래와 같이 logout 이 성공한 후에 호출되는 URL 을 추가로 설정해서 그 URL 핸들러 함수에서 쿠키를 삭제하는 코드를 구현해봤어.

SecurityConfig.java
...
                .logout(logout -> logout
                        .logoutSuccessUrl("/logoutdone")
                )
...

LoginController.java 에 /logoutdone URL 에 대한 핸들러 함수를 작성해볼께.

LoginController.java
...
    @GetMapping("/logoutdone")
    public String logoutdone(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("/logoutdone url handler. 로그아웃 성공");
        new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
        Cookie[] cookies = request.getCookies();
        for (Cookie c : cookies) {
            Cookie cookie = new Cookie(c.getName(), null);
            cookie.setMaxAge(0);
            response.addCookie(cookie);
        }
        return "redirect:/";
    }
...

이제 로그아웃을 하면 Spring Security 프레임워크에 의해서 로그아웃처리가 된 후에 /logoutdone URL 이 호출되고, /logoutdone URL 핸들러 함수에서 쿠키를 삭제해주지

기존의 세션 인증 방식을 jwt 토큰 인증 방식으로 변경해봤는데, 코드가 그리 깔끔하지 않은 것은 해답을 제시하기 위한 것이 아니라, 이런 방법도 있다 라고 학습하는 과정이기 때문이라는 것을 이해해주길 바래.

반응형