[UMC-3rd & Springboot] rest API 서버 구현 - 로그인 #1

조윤희·2023년 2월 18일
0

UMC_badjang_Server

목록 보기
2/4

API명세서

/users/logIn

User - 회원 도메인

폴더구조
model/Controller - Provider/Service - DAO

  • model: Request와 Response의 형식을 정의 해주는 곳.
  • Controller: Request를 처리하고 Response 해주는 곳. (Provider/Service에 넘겨주고 다시 받아온 결과값을 형식화), 형식적 Validation
  • Provider/Service: 비즈니스 로직 처리, 의미적 Validation
  • DAO: Data Access Object의 줄임말. Query가 작성되어 있는 곳.

UserController

👌필요한 annotation

Annotation
스프링 부트는 어노테이션을 다양하게 아는 것이 중요하다. SpringBoot의 시작점을 알리는 @SpringBootApplication 어노테이션 뿐만 아니라 스프링 부트 어노테이션 등의 키워드로 구글링 해서 스프링 부트에서 자주 사용되는 다양한 어노테이션을 이해하고 외워두자.

어노테이션은 사전적 의미로는 주석이라는 뜻이다. 자바에서 사용될 때의 어노테이션은 코드 사이에 주석처럼 쓰여서 특별한 의미, 기능을 수행하도록 하는 기술이다. 즉, 프로그램에게 추가적인 정보를 제공해주는 메타데이터(meta data: 데이터를 위한 데이터)라고 볼 수 있다.
(요데이터(소스코드)가 어떤의미, 기능으로 사용되는지 알려주는 데이터)

  • @RestController
  • @RequestMapping
  • @Autowired
    -> UserController의 생성자에 @Autowired 어노테이션이 붙어있다. 이는 의존성 주입을 위한 것으로, UserController 뿐만 아니라 다음에 살펴볼 UserService, UserProvider의 생성자에도 각각 붙어 있는 것을 확인할 수 있다. 간단히 요약하면 객체 생성을 자동으로 해주는 역할이다.
  • @ResponseBody
  • @Transactional
    -> 이 어노테이션이 붙은 메서드는 메서드가 포함하고 있는 작업 중에 하나라도 실패할 경우 전체 작업을 취소한다.
    참고로 트랜잭션이란 하나의 단위로 모든 작업들이 성공적으로 완료되어야 작업 묶음의 결과를 적용하고, 어떤 작업에서 오류가 발생했을 때는 이전에 있던 모든 작업들이 성공적이었더라도 없었던 일처럼 완전히 되돌리는 것이 트랜잭션의 개념이다. (추후 더 정리할 예정)
  • @PostMapping
  • @RequestBody
    -> 클라이언트가 서버에게 필요한 데이터를 요청할때 json형태의 데이터를 담아서 서버로 보내면, 서버는 이 어노테이션을 사용하여 http요쳥의 body값 전체가 자바 객체로 변환된 후(객체에 저장) 매핑된 메소드의 파라미터로 전달한다.
  • @RequestParam
    -> 1개의 HTTP Request 파라미터를 받을 수 있는 어노테이션(?뒤의 값)
    해당 닉네임을 같는 유저들의 정보 조회 API [GET] /users? NickName=

👌Validation

서버 API 구성의 기본은 Validation을 잘 처리하는 것이다. 외부에서 어떤 값을 날리든 Validation을 잘 처리하여 서버가 터지는 일이 없도록 유의하자. 값, 형식, 길이 등의 형식적 Validation은 Controller에서, DB에서 검증해야 하는 의미적 Validation은 Provider 혹은 Service에서 처리하면 된다.


👌API 구현

model > PostLoginReq

package com.example.demo.src.user.model;
import lombok.*;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)

public class PostLoginReq {
    private String user_email;
    private String user_password;
}

model > PostLoginRes

package com.example.demo.src.user.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
public class PostLoginRes {
    private int user_idx;
    private String jwt;
}

UserController

Request를 다른 계층에 넘기고 처리된 결과 값을 Response 해주는 로직으로, Request의 형식적 Validation 처리한다. (DB를 거치지 않고도 검사할 수 있는)
이메일과 비밀번호의 빈값 여부와 이메일 정규표현에대한 validation처리를 해주었다.

import com.example.demo.config.BaseException;
import com.example.demo.config.BaseResponse;
import com.example.demo.src.user.model.*;
import com.example.demo.utils.JwtService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.web.bind.annotation.*;

import javax.transaction.Transactional;
import java.util.List;

import static com.example.demo.config.BaseResponseStatus.*;
import static com.example.demo.utils.ValidationRegex.*;
import static org.springframework.transaction.annotation.Isolation.READ_COMMITTED;

/**
     * 로그인 API
     * [POST] /users/logIn
     * @return BaseResponse<PostLoginRes>
     */
    @ResponseBody
    @org.springframework.transaction.annotation.Transactional(propagation = Propagation.REQUIRED, isolation = READ_COMMITTED , rollbackFor = Exception.class)
    @PostMapping("/logIn")
    public BaseResponse<PostLoginRes> logIn(@RequestBody PostLoginReq postLoginReq){
        //이메일 정규표현: 입력받은 이메일이 email@domain.xxx와 같은 형식인지 검사합니다. 형식이 올바르지 않다면 에러 메시지를 보냅니다.
        if (postLoginReq.getUser_email() == null) {
            return new BaseResponse<>(POST_USERS_EMPTY_EMAIL);
        }
        if (postLoginReq.getUser_password() == null) {
            return new BaseResponse<>(POST_USERS_EMPTY_PW);
        }
        if (!isRegexEmail(postLoginReq.getUser_email())) {
            return new BaseResponse<>(POST_USERS_INVALID_EMAIL);
        }
        try{
            // TODO: 로그인 값들에 대한 형식적인 validatin 처리해주셔야합니다.
            PostLoginRes postLoginRes = userProvider.logIn(postLoginReq);
            return new BaseResponse<>(postLoginRes);
        } catch (BaseException exception){
            return new BaseResponse<>(exception.getStatus());
        }
    }

UserProvider

요청한 비밀번호를 암호화하여 데이터베이스에 저장되어있는 암호화된 비밀번호와 비교하는 의미적 validation(DB를 거쳐서 검사해야하는)처리가 이루어지는 가장 중요한 로직이다.

import com.example.demo.config.BaseException;
import com.example.demo.src.user.model.*;
import com.example.demo.utils.JwtService;
import com.example.demo.utils.SHA256;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.rmi.server.ExportException;
import java.util.List;

import static com.example.demo.config.BaseResponseStatus.*;
import static com.example.demo.config.BaseResponseStatus.FAILED_TO_LOGIN;

public PostLoginRes logIn(PostLoginReq postLoginReq) throws BaseException{
        if(checkEmail(postLoginReq.getUser_email()) == 0) {
            throw new BaseException(FAILED_TO_LOGIN);
        }

        User user = userDao.getPwd(postLoginReq);
        String encryptPwd;
        try {
            encryptPwd=new SHA256().encrypt(postLoginReq.getUser_password());
            // 회원가입할 때 비밀번호가 암호화되어 저장되었기 떄문에 로그인을 할때도 암호화된 값끼리 비교를 해야합니다.
        } catch (Exception ignored) {
            throw new BaseException(PASSWORD_ENCRYPTION_ERROR); //비밀번호 암호화 에러
        }
       if(user.getUser_status().equals("STOP")){
            throw new BaseException(INVALID_USER_JWT);
        }
        if (user.getUser_password().equals(encryptPwd)){ //비말번호가 일치한다면 userIdx를 가져온다.
            int user_idx = user.getUser_idx();
            userDao.modifyUserStatusLogIn(user_idx);
            String jwt = jwtService.createJwt(user_idx);
            return new PostLoginRes(user_idx,jwt);
        }
        else{// 없는 이메일 또는 비밀번호가 다르다면 에러메세지를 출력한다.
            throw new BaseException(FAILED_TO_LOGIN);
        }

    }

🔑JWT

JSON Web Token의 약자로 암호화를 해서 쌍방향에서 쓸 수 있는 서명된 토큰을 만들수 있는 수단이다. 유저는 로그인을 할 때 이 토큰을 발급 받아 유저자신을 인증하는 수단으로 사용한다.
JWT의 진행 순서는 다음과 같다
1.클라이언트 사용자가 로그인을 통해 사용자 인증
2.서버에서 서명된(Signed) jwt를 생성하여 클라이언트에 응답
3.클라이언트가 서버에 데이터를 추가적으로 요구할 떄 jwt를 http Header에 첨부
4.서버에서 jwt를 검증

jwt에대한 추가 내용은 다음 사이트를 참고한다.

SHA256이란?

SHA-256은 메시지, 파일, 혹은 데이터 무결성 검증에 널리 사용되는 암호화 해싱 알고리즘(함수)입니다. SHA-256은 넓게는 SHA-2 패밀리에 속하고 변환하기를 원하는 문자들을 256 bit 길이의 key로 변환합니다. SHA-256을 사용하면 문자가 조금만 바뀌어도 해시값이 완전히 변합니다.

정리하면 비밀번호를 암호화하는 하나의 방식이다.

UserDao

로그인 성공시 데이터베이스 상의 user_status를 off->on으로 변경하는 메소드와 요청한 email에 해당되는 user의 암호화된 비밀번호 값을 가져오는 메소드 2가지가 필요하다.

import com.example.demo.src.user.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.util.List;

public int modifyUserStatusLogIn(int user_idx){
        String modifyUserNameQuery = "update User set user_on_off = ? where user_idx = ? ";
        Object[] modifyUserNameParams = new Object[]{"ON", user_idx};

        return this.jdbcTemplate.update(modifyUserNameQuery,modifyUserNameParams);
    }
public User getPwd(PostLoginReq postLoginReq) {
        String getPwdQuery = "select user_idx, user_password, user_email, user_status from User where user_email = ?"; // 해당 email을 만족하는 User의 정보들을 조회한다.
        String getPwdParams = postLoginReq.getUser_email(); // 주입될 email값을 클라이언트의 요청에서 주어진 정보를 통해 가져온다.

        return this.jdbcTemplate.queryForObject(getPwdQuery,
                (rs, rowNum) -> new User(
                        rs.getInt("user_idx"),
                        rs.getString("user_email"),
                        rs.getString("user_password"),
                        rs.getString("user_status")
                ), // RowMapper(위의 링크 참조): 원하는 결과값 형태로 받기
                getPwdParams
        ); // 한 개의 회원정보를 얻기 위한 jdbcTemplate 함수(Query, 객체 매핑 정보, Params)의 결과 반환
    }

Request의 body에서 email정보를 가져와 해당 이메일의 유저 정보를 select하는 쿼리문을 작성한다.

utils > ValidationRegex

이메일 정규 표현식

public static boolean isRegexEmail(String target) {
        String regex = "^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(target);
        return matcher.find();
    }

👌API 테스트

postman


참고자료
https://velog.io/@yukina1418/JWT%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83
https://backend-intro.vlpt.us/4/
https://yjh5369.tistory.com/entry/Spring-Boot%EC%97%90%EC%84%9C-JWT-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95
https://losskatsu.github.io/blockchain/sha256/#1-%EC%84%9C%EB%A1%A0

0개의 댓글