로그인 로직 설계

기존에는 스프링 시큐리티가 따로 로그인 기능까지 지원해주었기 때문에 본인이 따로 코드를 작성할 필요가 없었지만,

지금은 상황이 바뀌었기 때문에 로그인 폼도 만들어줘야 하고 서비스 빈에 로그인 로직과 컨트롤러빈에 로그인 메소드를 선언해줘야한다.

로그인 구현하기

    public BoardPrincipal loginUser(String username, String password) {
          UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(username, password);
            Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            BoardPrincipal principal1 = BoardPrincipal.from(UserAccountDto.from(userAccountRepository.findById(username).orElseThrow()));
            return principal1;
    }

로그인 dto 에서 전달받은 아이디와 비밀번호로 인증을 시도하고 정보가 일치한다면 인증 정보를 ContextHolder 에 저장하고, UserDetails 를 구현한 PrincipalDto 타입으로 반환해준다.

[SpringSecurity] Authentication(인증) 관련 클래스와 처리

로그인 컨트롤러 메소드 작성

이제 로그인 컨트롤러에서는 로그인 화면으로 이동하기 위한 Get 메소드와, 로그인 처리를 하기위한 Post메소드를 선언할 것이다.

   @GetMapping("/login")
   public String login() {
       return "/user/login_form";
   }//로그인 화면으로 이동
   
       @PostMapping("/login")
   public String login(LoginDto user,
                       HttpServletRequest req,
                       HttpServletResponse res) {
       try {
           final BoardPrincipal principal = userService.loginUser(user.getUsername(), user.getPassword());
           Cookie accessToken = cookieUtil.createCookie(TokenProvider.ACCESS_TOKEN_NAME, tokenProvider.generateToken(principal));
           accessToken.setMaxAge((int) TimeUnit.MILLISECONDS.toSeconds(TokenProvider.TOKEN_VALIDATION_SECOND));
           Cookie refreshToken = cookieUtil.createCookie(TokenProvider.REFRESH_TOKEN_NAME, tokenProvider.generateRefreshToken(principal));
           refreshToken.setMaxAge((int) TimeUnit.MILLISECONDS.toSeconds(TokenProvider.REFRESH_TOKEN_VALIDATION_SECOND));
           redisUtil.setDataExpire(refreshToken.getValue(), principal.getUsername(), TokenProvider.REFRESH_TOKEN_VALIDATION_SECOND);
           res.addCookie(accessToken);
           res.addCookie(refreshToken);
           return "redirect:/";
       } catch (Exception e) {
           log.error("login error", e);
           return "redirect:/login";
       }
   }

어떻게 보면 가장 핵심이라 할 수 있다.
로그인 DTO로 유저 아이디와 비밀번호를 받아오면, 해당 값을 기반으로 로그인을 진행하고 액세스토큰과 리프레쉬 토큰을 발급해서 쿠키에 저장한다.

그리고 레디스에 리프레쉬 토큰을 기존에 설정한 만료기한을 추가하여 저장한다.

로그아웃 구현하기

로그아웃은 단순히 비즈니스 로직을 만들 필요 없이 컨트롤러로 구현할 수 있다.

JWTRequestFilter 가 매번 계정 정보를 토큰을 통해서 가져오기 때문에, 해당 토큰을 가져오는 길만 차단해두면 된다.

쿠키를 통해서 액세스 토큰으로 UserDetails 를 가져오기 때문에 해당 쿠키를 만료시켜 준 후에, 탈취를 예방하기 위한 블랙리스트를 생성해주면 된다.

    @GetMapping("/logout")
    public String logout(
             HttpServletRequest req,
            HttpServletResponse res) {
        SecurityContextHolder.clearContext();
        Cookie accessToken = cookieUtil.getCookie(req, TokenProvider.ACCESS_TOKEN_NAME);
        Cookie refreshToken = cookieUtil.getCookie(req, TokenProvider.REFRESH_TOKEN_NAME);
        if (accessToken != null) {
            Long expiration = tokenProvider.getExpireTime(accessToken.getValue());
            redisUtil.setBlackList(accessToken.getValue(), "accessToken", expiration-System.currentTimeMillis());
            accessToken.setMaxAge(0);
            res.addCookie(accessToken);
        }
        if (refreshToken != null) {
            refreshToken.setMaxAge(0);
            res.addCookie(refreshToken);
            redisUtil.deleteData(refreshToken.getValue());
        }


        return "redirect:/";
    }

만료시기간을 당장 코앞으로 설정하고, 해당 액세스 토큰의 남은 시간에서 현재시간을 뺀 만큼의 기한을 레디스에 저장한다. (로그아웃시에 해당 액세스 토큰 앞에 LOGOUT_ 을 붙여 구별가능하게 해준다.)

그리고 리프레쉬 토큰은 로그아웃시에 레디스에서 삭제해준다.

로그인 폼 타임리프로 작성

<form th:action="@{/login}"  method="post" style="width:700px">
  <p class="register"> 로그인 </p>
<label for="username" class="form-label"> 아이디 </label>
<input id="username"  name="username" type="text" class="input" >
  <label for="password" class="form-label"> 비밀번호 </label>
  <input id="password"  name="password" type="password" class="input" >
  <br>
<button type="submit">로그인</button>
&nbsp;
&nbsp;
<a th:href="@{/signup}" ><button type="button">회원가입</button></a>
</form>

회원가입 폼과 마찬가지로 타임리프는 따로 값을 지정하지 않아도 태그에 들어간 id로 해당값을 전달해주기 때문에 어떠한 동작을 할것인지만 명시해주면 된다.

로그인 로그아웃 해보기

레디스에 저장된 데이터를 터미널로 확인할 순 있지만, 좀더 쉽게 확인하기 위해 Medis 라는 redis gui 를 다운받아 사용했다.

Modern GUI for Redis

해당 프로그램을 설치 후에 호스트와 포트를 설정하고 접속하면 어떠한 데이터가 저장되었는지 확인하고 삭제도 가능하다.

물론 여러 자료구조를 가진 데이터또한 저장이 가능하다.

내가 따로 만든 로그인 화면이다.

로그인 후에 크롬의 확장 프로그램인 EditThisCookie 를 이용해 리프레쉬 토큰과 액세스 토큰을 확인할 수 있다.

마찬가지로 로그에서도 해당 필터가 모든 페이지에 접근할때마다 실행되기 때문에, 토큰의 유무를 확인할 수 있다.
refreshToken 은 accessToken 이 만료되었을때 토큰을 쿠키에서 가져와 저장한다.
그리고 토큰이 남아있다면, 레디스에서 토큰을 키로 가진 데이터의 벨류(유저네임)을 가져와 인증을 처리하는 방식이다.

이렇게 마찬가지로 redis 에서도 리프레쉬토큰이 저장된걸 확인할 수 있다.

로그아웃시에 모든 쿠키들은 만료되며 없어진다.

마찬가지로 레디스에 로그아웃시에 기존 토큰의 남은 시간을 가진 블랙리스트를 가진다.

해당 블랙리스트 키를 바탕으로 JwtRequestFilter 에서 매 인증시에 해당 액세스 토큰이 유효한지를 블랙리스트 키의 존재유무로 판단해 인증을 처리한다.

정리

기존의 JSESSIONID 를 활용하지 않고, Jwt토큰을 활용한 인증을 구현해보았다.

기존의 토큰의 단점은 짧은 만료기한을 가져 매번 로그인을 진행해야 했고

원래대로라면 토큰의 만료시간이 10분이라면 10분마다 로그인을 해줘야 했지만 리프레쉬 토큰을 따로 서버에 저장함으로써 로그인을 유지 할 수 있게 바꿔주었다.

따로 토큰 재발급을 하는 부분이 이해가 안됐지만 동근님의 블로그를 보고 많은 참고를 하게 되었고,

전체적으로 기존에 진행하던 프로젝트에 적용을 시키기에 한 글로는 부족해서 여러 글을 참고하면서 글을 작성해 보았다.

레디스를 활용해보면서 아 이걸로 생각보다 여러 기능을 또 구현할 수 있겠구나. 라는 생각이 들었고 레디스를 활용해서 글 추천/조회수 등을 중복으로 카운팅되는걸 방지할 수 있게 구현도 가능했다.

작성을 하면서 지식이 부족해 막히는 부분도 있었으나, 정리할부분이 생겨 오히려 기쁜 마음이다.🥰

profile
자스코드훔쳐보는변태

0개의 댓글