728x90

 

 

📌  인증방식의 변화

기존의 시스템에서는 서버 기반의 인증방식을 사용하였다. 하지만 시스템의 규모가 커짐에 따라 서버 기반의 인증 방식은 한계점을 보이기 시작하였고, 토큰 기반의 인증 방식이 등장하게 되었다. 현대 웹서비스에서 API를 이용한 웹서비스를 개발할 때, 토큰을 사용하여 사용자들의 인증 작업을 처리하는 방법이 많이 선호된다고 한다. 

이에 대한 구체적인 이론은 따로 포스팅을 작성해 놓았다.

https://jminie.tistory.com/125?category=1008953 

 

쿠키(cookie) 세션(session) 토큰(token)(JWT) 그리고 캐시(cache)

쿠키, 세션, 캐시, 그리고 토큰에 대해 알아보기 전에 우선 이것들이 왜 필요한지부터 알아보자. 📖 HTTP 프로토콜의 특징 Connectionless(비연결지향) : 클라이언트가 서버에게 요청을 한 후 그에 맞

jminie.tistory.com

 

필자도 로그인 처리 즉 인증처리 방식에서 JWT(Json Web Token)을 이용하여 API를 작성해 보았다.

 

 

 

 

📗  도메인 폴더 구조

Route - Controller - Provider/Service - DAO

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

 

 

위 그림에서는 Service 안에 Provider가 포함되어 있다고 생각하면 된다.

필자는 

  • Provider : Read의 비즈니스 로직 처리
  • Service : Create, Update, Delete 의 로직 처리

방식으로 처리했다.

 

Spring Boot는 Route와 Controller가 모두 Controller에서 처리된다.

 

 

 

 

🔐  로그인 처리 과정

 

Controller

    /**
     * 로그인 API
     * [POST] /users/logIn
     */
    @ResponseBody
    @PostMapping("/logIn")
    public BaseResponse<PostLoginRes> logIn(@RequestBody PostLoginReq postLoginReq){
        try{
            PostLoginRes postLoginRes = userProvider.logIn(postLoginReq);
            return new BaseResponse<>(postLoginRes);
        } catch (BaseException exception){
            return new BaseResponse<>(exception.getStatus());
        }
    }

 

앞서 @RequestMapping("/app/users") 에 따라 @PostMapping을 통해  URI와 POST방식으로 매핑시켜준다.(로그인은 POST방식이므로)

 

이제 이 Controller는 Provider의 logIn메서드를 호출하게 된다.

 

 

 

 

Provider

    public PostLoginRes logIn(PostLoginReq postLoginReq) throws BaseException{
        User user = userDao.getUserInfo(postLoginReq);
        String password;
        try {
            password = new AES128(Secret.USER_INFO_PASSWORD_KEY).decrypt(user.getUserPassword());
        } catch (Exception ignored) {
            throw new BaseException(PASSWORD_DECRYPTION_ERROR);
        }

        if(postLoginReq.getUserPassword().equals(password)){
            int id = userDao.getUserInfo(postLoginReq).getUserId();
            String jwt = jwtService.createJwt(id);
            return new PostLoginRes(id,jwt);
        }
        else{
            throw new BaseException(FAILED_TO_LOGIN);
        }
    }
}

 

다시 이 Provider는 Dao의 getUserInfo를 호출하게 된다.

 

 

 

 

Dao

    public User getUserInfo(PostLoginReq postLoginReq){
        String getPwdQuery = "select userId, userNickname, userPassword, userEmail, status, createdAt, updateAt, ID from User where ID = ?";
        String getPwdParams = postLoginReq.getId();

        return this.jdbcTemplate.queryForObject(getPwdQuery,
                (rs,rowNum)-> new User(
                        rs.getInt("userId"),
                        rs.getString("userNickname"),
                        rs.getString("userPassword"),
                        rs.getString("userEmail"),
                        rs.getString("status"),
                        rs.getDate("createdAt"),
                        rs.getDate("updateAt"),
                        rs.getString("ID")),
                getPwdParams);
    }
}

 

해당 쿼리문은 AWS RDS에 올라가 있는 DB와 연결되어 작동하게 되어 User 테이블에 있는 정보를 불러오게 된다.

 

 

 

 

🔐  JWT를 이용한 인증 

다시 Provider를 보자

    public PostLoginRes logIn(PostLoginReq postLoginReq) throws BaseException{
        User user = userDao.getUserInfo(postLoginReq);
        String password;
        try {
            password = new AES128(Secret.USER_INFO_PASSWORD_KEY).decrypt(user.getUserPassword());
        } catch (Exception ignored) {
            throw new BaseException(PASSWORD_DECRYPTION_ERROR);
        }

        if(postLoginReq.getUserPassword().equals(password)){
            int id = userDao.getUserInfo(postLoginReq).getUserId();
            String jwt = jwtService.createJwt(id);
            return new PostLoginRes(id,jwt);
        }
        else{
            throw new BaseException(FAILED_TO_LOGIN);
        }
    }
}

 

첫 번째 try문을 보면 내가 보유한 Secret Key를 이용해 DB에 저장되어 있던 암호를 복호화한다. 이 과정이 있기 때문에 Secret Key는 절대로 탈취당해서는 안된다. 따라서 프로젝트를 GitHub등에 올릴 때 에도. gitignore을 사용하여 가려놔야 한다.

 

package com.example.demo.config.secret;

public class Secret {
    public static String JWT_SECRET_KEY = "본인이 원하는 Sercret Key로 등록";
}

 

 

 

이제 복호화된 비밀번호를 입력한 비밀번호와 비교하여 일치하는지 여부를 확인한다.

만약 일치한다면 Dao에서 가져온 UserId값과 jwtService에서 생성된 JWT값을 함께 PostLoginRes에 담아 Controller로 돌려주게 된다.

 

만약 일치하지 않다면 정보가 올바르지 않다는 BaseEception을 던져준다.

if(postLoginReq.getUserPassword().equals(password)){
    int id = userDao.getUserInfo(postLoginReq).getUserId();
    String jwt = jwtService.createJwt(id);
    return new PostLoginRes(id,jwt);
}
else{
    throw new BaseException(FAILED_TO_LOGIN);
}

 

 

 

jwtService 

여기서 setExpiration이 눈에 띄는데 바로 JWT 발급 만료기간이다.

개발 단계이기 때문에 이 기간을 길게 해 주었다. 클라이언트가 JWT를 가지고 테스트하는 단계에서 지속적으로 JWT의 발급기간이 만료되어 사용될 수 없다면 귀찮은 상황이 많이 생기기 때문이다. 

하지만 당연히 본 서비스를 시작하게 되면 보안을 위해 발급기간을 조정해야 한다.

    /*
    JWT 생성
    @param userIdx
    @return String
     */
    public String createJwt(int userId){
        Date now = new Date();
        return Jwts.builder()
                .setHeaderParam("type","jwt")
                .claim("userId",userId)
                .setIssuedAt(now)
                .setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24*3650)))
                .signWith(SignatureAlgorithm.HS256, Secret.JWT_SECRET_KEY)
                .compact();
    }

 

 

 

 

 

📌  Validation 처리

  • 값, 형식, 길이 등의 형식적 Validation은 Controller에서 처리한다.
  • DB에서 검증해야 하는 의미적 Validation은 Provider 혹은 Service에서 처리한다.

 

 

형식적 Validation

우선 아이디를 입력하지 않거나 비밀번호를 입력하지 않았을 경우를 대비해 Controller에 형식적 Validation 처리를 해준다.

// 아이디 입력 안했을 때 validation
if(postLoginReq.getId().eauals(""){
    return new BaseResponse<>(POST_USERS_EMPTY_ID);
}

// 비밀번호 입력 안했을 때 validation
if(postLoginReq.getUserPassword().equals(""){
    return new BaseResponse<>(POST_USERS_EMPTY_PASSWORD);
}

 

 

Validaion이 추가된 Controller

    /**
     * 로그인 API
     * [POST] /users/logIn
     */
    @ResponseBody
    @PostMapping("/logIn")
    public BaseResponse<PostLoginRes> logIn(@RequestBody PostLoginReq postLoginReq){
        // 아이디 입력 안했을 때 validation
        if(postLoginReq.getId().equals("")){
            return new BaseResponse<>(POST_USERS_EMPTY_ID);
        }
        // 비밀번호 입력 안했을 때 validation
        if(postLoginReq.getUserPassword().equals("")){
            return new BaseResponse<>(POST_USERS_EMPTY_PASSWORD);
        }
        try{
            PostLoginRes postLoginRes = userProvider.logIn(postLoginReq);
            return new BaseResponse<>(postLoginRes);
        } catch (BaseException exception){
            return new BaseResponse<>(exception.getStatus());
        }
    }

 

 

 

 

 

의미적 Validation

로그인을 시도하였는데 만약 정지된 유저라면 로그인이 되지 않도록 Provider에 의미적 Validation 처리를 해준다.

// 정지된 유저 validation 처리
if(checkStatus(postLoginReq.getId()).equals("Ban")){
    throw new BaseException(POST_USERS_DISABLED_USER);
}

 

 

 

Validation이 추가된 Provider

    public PostLoginRes logIn(PostLoginReq postLoginReq) throws BaseException{
        // 정지된 유저 validation 처리
        if(checkStatus(postLoginReq.getId()).equals("Ban")){
            throw new BaseException(POST_USERS_DISABLED_USER);
        }
        User user = userDao.getUserInfo(postLoginReq);
        String password;
        try {
            password = new AES128(Secret.USER_INFO_PASSWORD_KEY).decrypt(user.getUserPassword());
        } catch (Exception ignored) {
            throw new BaseException(PASSWORD_DECRYPTION_ERROR);
        }
        if(postLoginReq.getUserPassword().equals(password)){
            int id = userDao.getUserInfo(postLoginReq).getUserId();
            String jwt = jwtService.createJwt(id);
            return new PostLoginRes(id,jwt);
        }
        else{
            throw new BaseException(FAILED_TO_LOGIN);
        }
    }
}

 

 

 

 

 

 

👌  정상작동 확인

 

정상 로그인 확인

 

정상적으로 로그인 요청에 성공하고 userId와 JWT를 뱉어주는 것을 확인할 수 있다.

 

 

 

이번엔 Validation이 잘 작동하는지 확인해보자

 

Validation 확인

1. 아이디를 입력하지 않았을 때

 

 

 

 

 

2. 비밀번호를 입력하지 않았을 때

 

 

 

 

 

3. 정지된 유저가 로그인을 시도했을 때

 

DB에 테스트를 위해 status를 Ban 즉 정지로 등록해 놓은 유저로 로그인을 시도해 보았다.

 

이렇게 Validaion도 모두 정상 작동하는 것을 확인하였다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

복사했습니다!