김영한 강사님의 스프링 MVC 2편을 듣고 Cookie 로그인 부분을 이해,REST API 스타일로 바꾸어 보았다.또한 mysql과 연동또한 하였다.
먼저 로그인을 위해서는 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;
}
}
MemberRepository.class
public interface MemberRepository extends JpaRepository<Member,Long> {
public Member findByLoginId(String loginId);
}
위에서는 loginId로 Member를 찾을 수 있는 코드를 넣었다.
로그인을 시도할때 아이디 비밀번호가 맞는지 체크하는 로직이 필요하고 그 로직은 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;
}
}
}
@Data
@Getter
@AllArgsConstructor
public class MemberSignUpDto {
@NotEmpty
private String name;
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
@Data
@AllArgsConstructor
@Getter
public class MemberSignInDto {
@NotEmpty
private String loginId;
@NotEmpty
private String password;
}
@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를 저장할 수 있게 설계되었습니다.
@RestController
@Slf4j
public class ItemController {
@GetMapping("/items")
public String items(){
return "로그인 하지 못한 사용자는 볼수 없는 페이지";
}
}
우리가 익히 알고 있듯이 로그인한 사용자는 특정 페이지에 접근할 권한이 있다. 앞으로 우리는 위의 /items 를 로그인한 사용자는 접근 할 수 있고, 로그인하지 못한 사용자는 접근하지 못하도록 하는 것이 목표이다. 그러기 위해서는 Filter가 필요한데 Filter의 사용은 이따가 알아보도록 하자!
@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) 이제 요청을 보낼때마다 쿠키도 같이 보내준다.
그러면 서버는 사용자가 준 그 쿠키값으로 사용자를 구분하여 사용자에게 맞는 화면을 보여 줄 수 있다.
이제 사용자의 모든 요청에 쿠키가 딸려나간다.
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를 추가하였다
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를 썼다.
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를 수정한다.
@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를 삭제한다는 것을 알았다.
@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로는 로그인한 사용자는 자신의 이름+을 위한 기본 홈 화면이 나오지만
로그인이 안된 사용자는 그냥 기본 홈 화면만 나오게 설계 되었다.
예상했던 대로 로그인을 안한 상태라면 기본 홈 화면이 , items에는 접근을 못하는 모습을 볼 수 있다. 그러면 회원가입을 진행하고 로그인을 해보자.
이렇게 로그인이 성공하면 우리가 보낼 request의 헤더에
이렇게 cookie가 추가된다.
items를 입력하면
이렇게 나오고
homeByCookie를 하면 자신의 이름+을 위한 기본 홈화면이 나오는 것을 볼 수 있다.
로그아웃을 하면 request의 Headers에 Cookie가 쏙 빠진다.
이제는 items에 다시 접근할 수 없다.
1)보안문제가 있다. 쿠키값은 브라우저에서 임의로 변경이 가능한 것을 볼 수 있다. 우리가 설계한 대로 하였을 때 쿠키값을 변경하면 다른 사람의 정보를 볼 수 있을 것이다.
대안->사용자 별로 예측 불가능한 임의의 토큰을 노출하고 서버에서 토큰과 사용자 id를 매핑해서 인식하는 방법이 필요하다. 그리고 그 토큰은 서버에서 관리하는 식으로 해야 한다.
2)해커가 쿠키를 털어가면 평생 사용할 수 있다.
대안->해커가 토큰을 털어가도 시간이 지나면 사용할 수 없도록 서버에서 해당 토큰의 만료시간을 짧게 하자!