WEB 인증 - session

강서진·2024년 2월 13일

웹 인증

Web 인증 (Web Authentification)
: 웹 애플리케이션에서 사용자의 정체성을 확인하고 적절한 권한을 부여하는 과정
사용자가 누구인지 확인하고, 해당 사용자가 액세스하려는 웹 리소스나 서비스에 대한 권한이 있는지 확인하는 데 사용.
웹 서비스의 보안성을 높이고, 사용자 데이터의 무단 접근을 방지하기 위한 중요한 보안 요소
EX) 관리자만 접근할 수 있는 메뉴에 아무나 접근할 수 없음, A유저의 데이터를 B유저가 접근할 수 없음

웹 인증은 사용자 등록, 사용자 인증, 세션 관리에 사용된다.

  • 사용자 등록: 사용자는 서버로 아이디, 비밀번호 등의 인증 정보를 전송하고, 서버는 정보를 안전하게 저장해야 한다.
  • 사용자 인증: 아이디, 비밀번호를 통해 사용자는 웹에 인증하고, 서버는 사용자를 확인하여 적절한 인증 및 인가를 실행한다.
    (최근에는 아이디, 비밀번호 뿐만 아니라 OAuth 연동, 지문 인식, 전화번호 인증 등 다양한 인증 방법을 사용하고 있다.)
  • 세션 관리: 인증된 사용자는 웹 서버의 리소스에 접근할 수 있는 권한을 받는다. 서버는 쿠키, 세션, 토큰 등 적절한 기술을 통홰 사용자의 로그인 상태와 권한을 관리한다.

로그인 인증

  1. ID/PW 기반 로그인
    : 가장 보편적인 방식으로, 아이디와 패스워드를 사용
  2. 소셜 로그인(OAuth)
    : 해당 사이트에 가입하는 형태가 아니라, 다른 소셜 기반의 아이디, 패스워드를 대신 가져와서 로그인하는 방식
    표준화된 기술로, 구글, 카카오, 네이버 등에서 제공하고 있다
  3. 이메일 인증
    : 이메일을 넣고 로그인하면, 이메일로 인증번호를 보내 사용자가 이를 입력하는 형태
  4. 휴대폰 인증
    : 통신사, 생년월일, 이름을 입력해 문자나 앱으로 본인 인증
  5. 다중 인증요소(MFA)
    : ID/PW + OTP 등

이 방법들 중에서 3~5번은 실제 도메인 등록이 필요하기 때문에 실습은 힘들고, 2번의 소셜 로그인도 로컬 호스트를 지원하는 곳이 있는가 하면 승인이 필요한 곳도 있어서 연습할 때는 1번을 구현할 예정이다.


HTTP Session

사용자가 로그인을 시도하면, 서버는 사용자의 인증 정보를 검증하여 session ID를 생성한다. 이 세션은 서버측에서 관리하고, 서버에서 갱신 및 변경할 수 있다. 이 session ID는 쿠키로 사용자에게 전달되며, 웹 어플리케이션에서 사용한다(따라서 안드로이드, IOS 등 네이티브 어플리케이션에서는 이 방법을 사용할 수 없다).

웹 어플리케이션에서 사용자 정보를 저장하는 기술
1. Http session은 쿠키를 사용하여 구현한다.
2. Http의 stateless 특성때문에 사용자가 요청을 보낼 때마다 사용자 정보를 매번 다시 전송하는데, 각 session마다 고유한 ID를 부여하여 해결한다.
3. Http session은 사용자 로그인 정보를 관리할 때 사용하며, 만료기간 내에 사용자가 다시 접속하면 유지된다.
4. Http session은 서버에서 관리되기 때문에 사용자가 임의로 세션 정보를 조작할 수 없다. 랜덤한 값으로 생성되며, Https를 통해 암호화된다.

실습

먼저, 새 프로젝트를 하나 만든 다음, controller, db, model 패키지를 만든다. model에 이름만을 가진 UserDto 클래스를 만들어 넣어주고,

// UserDto
@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
  private String name;
  private String password;
}

메모리 DB로 사용할 레포지토리를 만든다.

@Service
public class UserRepository {

  private List<UserDto> userList = new ArrayList<>();

  @PostConstruct
  public void init(){
    userList.add(new UserDto("aaa","1111"));
    userList.add(new UserDto("bbb","2222"));
    userList.add(new UserDto("ccc","3333"));
  }

  public Optional<UserDto> findByName(String name){
    return userList.stream().filter(it -> {
      return it.getName().equals(name);
    }).findFirst();
  }
}

-init 메서드로 실습에 사용할 데이터를 미리 만들어두고, @PostConstruct 애너테이션을 붙여 빈이 생성되면 init을 호출하도록 한다.

  • 이름을 가지고 검색해서 UserDto를 반환하는 메서드도 하나 만들어준다. stream을 사용하며, 반환값은 Optional로 받아준다.

이 다음에는 로그인 요청 클래스를 만들고, 로그인을 로직을 작성한 서비스를 만든다.

@Builder
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class LoginRequest {
  private String id;
  private String password;
}
@Service
public class UserService {

  @Autowired
  private UserRepository repository;

  public void login(LoginRequest loginRequest, HttpSession session){
    var id = loginRequest.getId();
    var pw = loginRequest.getPassword();

    var optionalUser = repository.findByName(id);
    if (optionalUser.isPresent()){
      var userDto = optionalUser.get();

      if (userDto.getPassword().equals(pw)){
        // 세션에 정보 저장
        session.setAttribute("USER",userDto);
      } else {
        throw new RuntimeException("Password does not match");
      }

    } else {
      // 없는 유저
      throw new RuntimeException("User not found");
    }
  }
}
  • session을 파라미터로 받게 설정하면, 스프링이 알아서 session을 생성해준다.
  • id로 userDto를 반환하는 메서드를 사용해 userDto를 불러오고, 입력받은 pw가 저장된 pw와 일치하는지 확인한다.
  • 일치하는 경우에만 session에 USER를 설정해준다.
  • 비밀번호가 일치하지 않거나, 없는 id일 경우에는 예외가 발생하고, 생성된 세션은 없어진다.
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/account")
public class AccountApiController {

  private final UserService service;

  @PostMapping("/login")
  public void login(@RequestBody LoginRequest loginRequest,
                    HttpSession session) {
    service.login(loginRequest, session);
  }
}

강의 자료에 있는 index.html을 static에 추가하여 비동기통신으로 /api/account/login으로 POST 요청이 날아오는지 확인한다.
개발자 도구의 network에서 성공적으로 로그인이 실행되면 헤더에 쿠키로 세션 ID가 함께 날아오는 것을 확인할 수 있었다. 이 쿠키는 애플리케이션에서 쿠키 저장소에 들어있다.

이 세션은 로그인에 실패해도 상관없이 항상 생긴다. 이는 웹서버와 연결할 때 임의로 생성된 세션으로, 로그인이 성공하면 서블릿에서 발행한 세션값을 받게 된다. 이 때 도메인은 로컬 호스트에 할당이 됐고, 패스는 루트에 할당됐다. 만료 기간은 원래 숫자로 나오지만, session이라고 적혀있다. 이는 서버와 연결된 브라우저가 열려있거나, 세션이 살아있는 동안 유지하겠다는 뜻이다. 이 뒤로 주고받는 통신에는 항상 세션 ID가 담겨있어 세션이 유지된다.

로그인을 하면 세션이 유지되는 것을 확인하기 위해 새로운 컨트롤러를 하나 만들고, session에 USER 키값으로 저장된 userDto를 제대로 받아오는지 확인해본다. 만약 저장된 USER가 없으면(=로그인되어 있지 않으면) null을 반환한다.

@RestController
@RequestMapping("/api/user")
public class UserApiController {

  @GetMapping("/me")
  public UserDto me(HttpSession session){
    var userObject = session.getAttribute("USER");
    if (userObject != null){
      var userDto = (UserDto)userObject;
      return userDto;
    } else {
      return null;
    }
  }
}

만약 로그인에 실패하면 /api/user/me에는 아무 내용도 찍히지 않지만, 로그인에 성공하면 userDto가 출력된다.
이 값은 서버에서 클라이언트로 보내는 값으로, 사용자가 조작할 수 없다. 서버에 저장되어 있기 때문에 클라이언트가 별도로 인증값을 넣고 요청하지 않아도 연결된 세션 값을 통해 현재 로그인한 사용자의 정보를 가져올 수 있다.

따라서, session으로 인증을 진행할 경우에는 setAttribute로 어떤 값을 저장하겠다는 코드가 반드시 존재하게 된다. 그러나 getAttribute로 세션에 저장된 값을 불러올 때는 세션이 만료될 수 있기 때문에 null이 나올 수 있다는 사실을 기억해야 한다.
추후 이러한 로그인 인증 코드는 인터셉터 등에서 처리할 수도 있다.

0개의 댓글