스프링- 로그인 API 구현-Cookie-(1)

이진우·2023년 7월 26일
0

스프링 학습

목록 보기
5/41

김영한 강사님의 스프링 MVC 2편을 듣고 Cookie 로그인 부분을 이해,REST API 스타일로 바꾸어 보았다.또한 mysql과 연동또한 하였다.

domain-Member

먼저 로그인을 위해서는 Member가 필요하다. domain이란 패키지내에 Member클래스를 넣어보자.

Member.class

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long id;

    @Column(name = "loginId",nullable = false)
    private String loginId;

    @Column(name = "name",nullable = false)
    private String name;

    @Column(name = "password",nullable = false)
    private String password;

    public Member(String name,String loginId,String password){
        this.name=name;
        this.loginId=loginId;
        this.password=password;
    }
}

Repository-MemberRepository

또한 이 Member를 저장하기 위한 MemberRepository가 필요해서 repository 패지키에 추가해준다.

MemberRepository.class

public interface MemberRepository extends JpaRepository<Member,Long> {
    public Member findByLoginId(String loginId);
}

위에서는 loginId로 Member를 찾을 수 있는 코드를 넣었다.

Service-LoginService

로그인을 시도할때 아이디 비밀번호가 맞는지 체크하는 로직이 필요하고 그 로직은 LoginService에 넣는다.

LoginService.class

@Service
@RequiredArgsConstructor

public class LoginService {
    private final MemberRepository memberRepository;

    public Member login(String loginId,String password){
        Member member= memberRepository.findByLoginId(loginId);
        if(member.getPassword().equals(password)){
            return member;
        }
        else{
            return null;
        }
    }
}

Dto

MemberSignUpDto
회원가입할때 사용자가 보내는 Dto입니다.
@Data
@Getter
@AllArgsConstructor
public class MemberSignUpDto {
    @NotEmpty
    private String name;

    @NotEmpty
    private String loginId;

    @NotEmpty
    private String password;
}
MemberSignInDto
사용자가 로그인을 시도할때 보내는 Dto입니다
@Data
@AllArgsConstructor
@Getter
public class MemberSignInDto {
    @NotEmpty
    private String loginId;

    @NotEmpty
    private String password;
}

Controller

MemberController
@RestController
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
    private final MemberRepository memberRepository;
    @PostMapping ("/add")
    public String addForm(@RequestBody @Valid MemberSignUpDto memberSignUpDto){
        Member member=new Member(memberSignUpDto.getName(),memberSignUpDto.getLoginId(),memberSignUpDto.getPassword());
        memberRepository.save(member);
        return "저장되었습니다";
    }

MemberController에서 리포지토리에 직접 member를 저장할 수 있게 설계되었습니다.

ItemController
@RestController
@Slf4j
public class ItemController {
    @GetMapping("/items")
    public String items(){
        return "로그인 하지 못한 사용자는 볼수 없는 페이지";
    }
}

우리가 익히 알고 있듯이 로그인한 사용자는 특정 페이지에 접근할 권한이 있다. 앞으로 우리는 위의 /items 를 로그인한 사용자는 접근 할 수 있고, 로그인하지 못한 사용자는 접근하지 못하도록 하는 것이 목표이다. 그러기 위해서는 Filter가 필요한데 Filter의 사용은 이따가 알아보도록 하자!

LoginController
@RestController
@RequiredArgsConstructor
@Slf4j
public class LoginController {
    private final LoginService loginService;
    @PostMapping("/login")
    public String loginByCookie(@Valid @RequestBody MemberSignInDto memberSignInDto,HttpServletResponse response){
        System.out.println(memberSignInDto.getLoginId()+ " "+memberSignInDto.getPassword());
        Member loginMember=loginService.login(memberSignInDto.getLoginId(), memberSignInDto.getPassword());
        log.info("login? {}",loginMember);
        if(loginMember==null){
            return "로그인 실패";
        }
        return "로그인 성공";
    }
}

위 코드로 저장되어 있는 Member를 가져오고 아이디 패스워드가 일치한다면 로그인 성공이라는 글자를 출력할 수는 있다.

문제점

그렇지만 여러가지 문제점이 있는데 이 서버가 나라는 사람을 어떻게 특정하고 내 정보를 가져와 주는가?? 쿼리 파라미터에 계속 내 아이디랑 비밀번호를 주면서 나를 특정하게 하는가?? 비효율적이고 보안상으로도 분명 안좋을 것이다.

쿠키 사용

위 그림을 텍스트로 해석하면
1)사용자는 아이디와 비밀번호를 입력해서 로그인을 시도한다.
2)올바르게 입력하면 서버에서 response(응답)에 cookie라는 것을 설정한다.
3) 그렇다면 사용자의 브라우저는 그 쿠키값을 자신의 쿠키 저장소에 저장해둔다.
4) 이제 요청을 보낼때마다 쿠키도 같이 보내준다.


그러면 서버는 사용자가 준 그 쿠키값으로 사용자를 구분하여 사용자에게 맞는 화면을 보여 줄 수 있다.

이제 사용자의 모든 요청에 쿠키가 딸려나간다.

Filter 사용-cookie 연계

어떤 페이지에 접근하고 말고를 결정할 수 있는 것은 Filter이다. 코드를 보면
LoginCheckFilter
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;

import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@Slf4j
public class LoginCheckFilter implements Filter {

    private static final String[] whitelist={"/members/add","/members/homeBySession","/members/homeByCookie","/loginByCookie","/logoutByCookie","/loginBySession","/logoutBySession"};

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest=(HttpServletRequest) request;
        String requestURI=httpRequest.getRequestURI();
        HttpServletResponse httpResponse=(HttpServletResponse) response;
        try{
            log.info("인증 체크 필터 시작 {}",requestURI);
            if(isLoginCheckPath(requestURI)){
                log.info("인증 체크 로직 실행 {}",requestURI);
                Cookie []cookies=httpRequest.getCookies();
                if(cookies==null){
                    return;
                }
                boolean used=false;
                for (Cookie cookie : cookies) {
                    if(cookie.getName().equals("memberId")){
                        used=true;
                        String cookieValue=cookie.getValue();
                        log.info("cookieValue={}",cookieValue);
                    }
                }
                if(!used){
                    return;
                }
                
            }
            chain.doFilter(request,response);
        }catch (Exception e){
            throw e;
        }finally {
            log.info("인증 체크 필터 종료 {}",requestURI);
        }
    }
    public boolean isLoginCheckPath(String requestURI){
        return !PatternMatchUtils.simpleMatch(whitelist,requestURI);
    }
}

위에서 doFilter 내부에 try를 보면 memberId라는 쿠키이름이 있다면 요청 URL에 접근 할 수 있게 memberId라는 쿠키 이름이 없거나 cookies가 null이라면 더이상 진행하지 못하게 하였다.
단 isLoginCheckPath를 통해서 whitelist에 등록된 URL은 로그인 여부와 상관없이 모두 통과되게 하였다.

또한 모든 요청에 로그를 남기기 위해서
LogFilter를 추가하였다

LogFilter
import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Slf4j
public class LogFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
       log.info("log filter init");
    }

    @Override
    public void destroy() {
        log.info("log filter destroy");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        //ServletRequest 는 Http요청이 아닌 경우까지 고려한 인터페이스
        HttpServletRequest httpRequest=(HttpServletRequest) request;
        String requestURI= httpRequest.getRequestURI();
        String uuid= UUID.randomUUID().toString();
        try{
            log.info("REQUEST [{}][{}]",uuid,requestURI);
            chain.doFilter(request,response);
        }catch (Exception e){
            throw e;
        }finally {
            log.info("RESPONSE [{}][{}]",uuid,requestURI);
        }
    }
}

이제 이 Filter를 Bean에다가 등록을 해서 써야 한다.
그러기 위해서는 WebConfig를 썼다.

WebConfig
import com.login.CookieSessionTest.configuration.filter.LogFilter;
import com.login.CookieSessionTest.configuration.filter.LoginCheckFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;

@Configuration
public class WebConfig {
    @Bean
    public FilterRegistrationBean logFilter(){
        FilterRegistrationBean<Filter> filterRegistrationBean=new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LogFilter());
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean loginCheckFilter(){
        FilterRegistrationBean<Filter> filterRegistrationBean=new FilterRegistrationBean<>();
        filterRegistrationBean.setFilter(new LoginCheckFilter());
        filterRegistrationBean.setOrder(2);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }
}

코드 수정

이제 코드만 약간 수정하면 된다.

위에서 LoginController를 수정한다.

LoginController-수정
@RestController
@RequiredArgsConstructor
@Slf4j
public class LoginController {
    private final LoginService loginService;
    @PostMapping("/loginByCookie")
    public String loginByCookie(@Valid @RequestBody MemberSignInDto memberSignInDto,HttpServletResponse response){
        System.out.println(memberSignInDto.getLoginId()+ " "+memberSignInDto.getPassword());
        Member loginMember=loginService.login(memberSignInDto.getLoginId(), memberSignInDto.getPassword());
        log.info("login? {}",loginMember);
        if(loginMember==null){
            return "로그인 실패";
        }
        Cookie idCookie=new Cookie("memberId",String.valueOf(loginMember.getId()));
        //헤더가 Cookie이고 value는 memberId=1인 상황이다.
        response.addCookie(idCookie);
        return "로그인 성공";
    }


    @PostMapping("/logoutByCookie")
    public String logoutByCookie(HttpServletResponse response){
        expireCookie(response,"memberId");
        return "로그아웃 완료";
    }
    public void expireCookie(HttpServletResponse response,String cookieName){
        Cookie cookie=new Cookie(cookieName,null);
        cookie.setMaxAge(0);
        response.addCookie(cookie);
    }
}

위에서 loginByCookie를 볼 때 로그인 성공 시 Cookie를 새로 생성하고 response 에다가 cookie를 담아 주는 것을 볼 수 있다.

또한 로그아웃 할 때는 쿠키의 수명을 0으로 만듦으로써 cookie를 삭제한다는 것을 알았다.

MemberController-추가
@RestController
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
    private final MemberRepository memberRepository;
    @PostMapping ("/add")
    public String addForm(@RequestBody @Valid MemberSignUpDto memberSignUpDto){
        Member member=new Member(memberSignUpDto.getName(),memberSignUpDto.getLoginId(),memberSignUpDto.getPassword());
        memberRepository.save(member);
        return "저장되었습니다";
    }

    @GetMapping("/homeByCookie")
    public String homeLoginByCookie(@CookieValue(name = "memberId",required = false)Long memberId){
        if(memberId==null){
            return "기본 홈 화면";
        }
        Member loginMember=memberRepository.findById(memberId).get();
        if(loginMember==null){
            return "기본 홈 화면";
        }
        String memberName=loginMember.getName();
        return memberName+"을 위한 기본 홈 화면";

    }
}

homeByCookie로는 로그인한 사용자는 자신의 이름+을 위한 기본 홈 화면이 나오지만
로그인이 안된 사용자는 그냥 기본 홈 화면만 나오게 설계 되었다.

POSTMAN을 통해서 테스트

예상했던 대로 로그인을 안한 상태라면 기본 홈 화면이 , items에는 접근을 못하는 모습을 볼 수 있다. 그러면 회원가입을 진행하고 로그인을 해보자.


이렇게 로그인이 성공하면 우리가 보낼 request의 헤더에

이렇게 cookie가 추가된다.

items를 입력하면

이렇게 나오고

homeByCookie를 하면 자신의 이름+을 위한 기본 홈화면이 나오는 것을 볼 수 있다.



로그아웃을 하면 request의 Headers에 Cookie가 쏙 빠진다.
이제는 items에 다시 접근할 수 없다.

쿠키의 단점

1)보안문제가 있다. 쿠키값은 브라우저에서 임의로 변경이 가능한 것을 볼 수 있다. 우리가 설계한 대로 하였을 때 쿠키값을 변경하면 다른 사람의 정보를 볼 수 있을 것이다.

대안->사용자 별로 예측 불가능한 임의의 토큰을 노출하고 서버에서 토큰과 사용자 id를 매핑해서 인식하는 방법이 필요하다. 그리고 그 토큰은 서버에서 관리하는 식으로 해야 한다.

2)해커가 쿠키를 털어가면 평생 사용할 수 있다.

대안->해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 하자!

참고

  • 영속 쿠키:만료 날짜를 입력하면 해당 날짜까지 유지
  • 세션 쿠키:만료 날짜를 생략하면 브라우저 종료시 까지만 유지
  • profile
    기록을 통해 실력을 쌓아가자

    0개의 댓글