[Spring] 세션 기반 인증을 구현해보자.

dohyun-dev·2023년 6월 18일
4

Session과 JWT

목록 보기
2/2

세션 기반 인증을 구현해봅시다.

Dependencies

  • springboot:2.7.12
  • org.springframework.boot:spring-boot-starter-web
  • org.springframework.boot:spring-boot-starter-data-jpa
  • org.springframework.boot:spring-boot-starter-security (암호화를 위해 사용)
  • org.mapstruct:mapstruct:1.5.3.Final
  • com.h2database:h2
  • lombok

패키지 구성은 Layered Architecture로 구성하였습니다.

Config

package com.example.loginexample.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordEncoderConfig {

    @Bean
    public PasswordEncoder BcyPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
package com.example.loginexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;

@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
public class LoginExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(LoginExampleApplication.class, args);
    }

}
  • @SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
    • 해당 프로젝트는 시큐리티를 사용하지 않을 것이기 때문에 SecurityAutoConfiguration를 bean으로 등록되지 않게 설정해줍니다.

Mapper

MemberMapper.class

package com.example.loginexample.mapper;

import com.example.loginexample.domain.member.Member;
import com.example.loginexample.dto.MemberDto;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper(componentModel = "spring")
public interface MemberMapper {
    Member toEntity(MemberDto memberDto);
    MemberDto toDto(Member member);
}

Mapper를 통해 각 계층간 DTO를 통해 데이터를 교환하게해 계층간 결합도를 낮췄습니다.

DTO

AuthenticationDto.class

package com.example.loginexample.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class AuthenticationDto {
    private final String accessToken;
    private final String refreshToken;
    private final String memberId;

    public AuthenticationDto(String accessToken, String refreshToken, String memberId) {
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
        this.memberId = memberId;
    }
}

LoginDto.class

package com.example.loginexample.dto;

import lombok.Data;

@Data
public class LoginDto {
    private String email;
    private String password;
}

MemberDto.class

package com.example.loginexample.dto;

import lombok.Builder;
import lombok.Data;

import java.io.Serializable;

@Data
@Builder
public class MemberDto implements Serializable {
    private final Long id;
    private final String memberId;
    private final String email;
    private final String password;
}

ResponseDto.class

package com.example.loginexample.dto;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ResponseDto<T> {
    private T body;

}

Exception

LoginFailureException.class

package com.example.loginexample.exception;

public class LoginFailureException extends IllegalStateException {

    private static final String MESSAGE = "이메일 혹은 비밀번호를 확인해주세요.";

    public LoginFailureException() {
        super(MESSAGE);
    }
}

Member Domain

Member.class

package com.example.loginexample.domain.member;

import lombok.*;

import javax.persistence.*;

@Entity
@Table(name = "member")
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
public class Member {

    @Id
    @GeneratedValue
    private Long id;
    @Column(name = "member_id", unique = true, nullable = false)
    private String memberId;
    @Column(name = "email", unique = true, nullable = false)
    private String email;
    @Column(name = "password", nullable = false)
    private String password;
}
  • Id는 DB에서 생성하는 PK를 위한 필드입니다.
  • memberID 는 UUID를 통해 생성하여 Member의 고유번호입니다.

MemberReadRepository.class

package com.example.loginexample.domain.member;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByEmail(String email);
    Optional<Member> findByMemberId(String memberId);
}
  • 데이터 접근 로직은 Spring-Data-Jpa를 통해 구성하였습니다.
  • findByEmail : email을 통해 Member를 찾습니다.
  • findByMemberId : MemberID를 통해 Member를 찾습니다.

MemberReadService.class

package com.example.loginexample.domain.member;

import com.example.loginexample.dto.MemberDto;
import com.example.loginexample.mapper.MemberMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MemberReadService {

    private final MemberRepository memberRepository;
    private final MemberMapper memberMapper;

    public MemberDto findMemberByEmail(String email) {
        Membermember = memberRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException());
        return memberMapper.toDto(member);
    }
}

MemberWriteService.class

package com.example.loginexample.domain.member;

import com.example.loginexample.dto.MemberDto;
import com.example.loginexample.mapper.MemberMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
@RequiredArgsConstructor
public class MemberWrtieService {

    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final MemberMapper memberMapper;

    public MemberDto join(MemberDto memberDto) {
        MemberjoinMember = makeMember(memberDto);
        return memberMapper.toDto(memberRepository.save(joinMember));
    }

    private Member makeMember(MemberDto memberDto) {
        Stringpassword = memberDto.getPassword();
        StringencryptPassword = passwordEncoder.encode(password);

        MemberDtojoinMemberDto= MemberDto.builder()
                .id(memberDto.getId())
                .memberId(UUID.randomUUID().toString())
                .email(memberDto.getEmail())
                .password(encryptPassword)
                .build();

        return memberMapper.toEntity(joinMemberDto);
    }
}
  • 명령과 조회를 분리해서 Sevice Layer를 구성하였습니다.
  • join : password를 암호화해서 DB의 저장하는 로직입니다.

AuthMemberFacade

AuthMemberFacade.class

package com.example.loginexample.facade;

import com.example.loginexample.dto.AuthenticationDto;
import com.example.loginexample.dto.MemberDto;

public interface AuthMemberFacade {
    MemberDto loginSession(String email, String password);
}

AuthMemberFacadeImpl.class

package com.example.loginexample.facade;

import com.example.loginexample.domain.member.MemberReadService;
import com.example.loginexample.dto.MemberDto;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthMemberFacadeImpl implements AuthMemberFacade {

    private final MemberReadService memberReadService;
    private final PasswordEncoder passwordEncoder;

    @Override
    public MemberDto loginSession(String email, String password) {
        MemberDto findMember = memberReadService.findMemberByEmail(email);
        if (passwordEncoder.matches(password, findMember.getPassword()))
            return findMember;
        return null;
    }
}
  • loginSession
    • email을 통해 memberReadService의 조회한 후 passwordEncorder를 통해 비밀번호가 일치하는 지 확인합니다.

Presentation Layer

MemberController.class

package com.example.loginexample.presentation;

import com.example.loginexample.domain.member.MemberWrtieService;
import com.example.loginexample.dto.MemberDto;
import com.example.loginexample.dto.ResponseDto;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    private final MemberWrtieService memberWrtieService;

    @PostMapping
    public ResponseEntity<MemberDto> join(@RequestBody MemberDto memberDto) {
        MemberDto joinMember = memberWrtieService.join(memberDto);

        return ResponseEntity
                .ok()
                .body(joinMember);
    }
}

AuthMessage.class

package com.example.loginexample.presentation.auth;

public enum AuthMessage {
    LOGIN_SUCCESS("로그인이 되었습니다."),
    IS_LOGIN_TRUE("로그인이 되어있습니다."),
    IS_LOGIN_FALSE("로그인이 되어 있지 않습니다."),
    INVALID_TOKEN("토큰이 유효하지 않습니다."),
    REISSUE_TOKEN("토큰이 재발급되었습니다."),
    LOGOUT("로그아웃 되었습니다.");

    public final String message;

    AuthMessage(String message) {
        this.message = message;
    }

    @Override
    public String toString() {
        return message;
    }
}

SessionConst.class

package com.example.loginexample.presentation.auth;

public interface SessionConst {
    String LOGIN_MEMBER = "loginMember";
}

LoginCotroller.class

import static com.example.loginexample.presentation.auth.AuthMessage.*;

@RestController
@RequiredArgsConstructor
public class LoginController {

    private final AuthMemberFacade authMemberFacade;

    @PostMapping("/loginV1")
    public ResponseEntity<ResponseDto> loginV1(@RequestBody LoginDto loginDto, HttpServletRequest request) {

        MemberDto loginMember = authMemberFacade.loginSession(loginDto.getEmail(), loginDto.getPassword());

        // 로그인 실패
        if (loginMember == null)
            throw new LoginFailureException();

        // 로그인 성공
        HttpSession session = request.getSession();
        session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
        ResponseDto responseDto = ResponseDto.builder()
                .body(LOGIN_SUCCESS.message)
                .build();

        return ResponseEntity
                .status(HttpStatus.CREATED)
                .body(responseDto);
    }

    @PostMapping("/logoutV1")
    public ResponseEntity<ResponseDto> logoutV1(HttpServletRequest request) {
        request.getSession().invalidate();

        ResponseDto responseDto = ResponseDto.builder()
                .body(LOGOUT.message)
                .build();

        return ResponseEntity
                .status(HttpStatus.OK)
                .body(responseDto);
    }

    @GetMapping("/is-loginV1")
    public ResponseEntity<ResponseDto> isLoginV1(HttpServletRequest request) {
        HttpSession session = request.getSession(false);

        ResponseDto responseDto = ResponseDto
                .builder()
                .body((session != null) ? IS_LOGIN_TRUE.message : IS_LOGIN_FALSE.message)
                .build();

        return ResponseEntity
                .ok()
                .body(responseDto);
    }
}
  • loginV1
    • authMemberFacade.loginSession 을 통해 MemberDto를 반환받습니다.
    • memberDto가 null이라면 LoginFailureException 을 발생시킵니다.
    • 로그인 성공이라면 session 저장소에 memberDto를 저장합니다.
    • 로그인 성공 응답을 클라이언트에게 내려줍니다.
  • logoutV1
    • 세션 저장소에 데이터를 모두 삭제합니다.
  • isLoginV1
    • 로그인이 되어있는지 확인하는 메서드입니다.
    • `request.getSession(false)`
      • 세션이 있다면 세션을 받환하고 없다면 null을 반환합니다.
    • 세션이 null이라면 로그인이 되어있다는 응답을 전송합니다.

1개의 댓글

comment-user-thumbnail
2023년 6월 18일

좋은 글 잘 봤습니다^^

답글 달기