JWT을 활용한 로그인/ Interceptor를 활용한 인가 처리 구현
Web/Java (Spring+JSP)

JWT을 활용한 로그인/ Interceptor를 활용한 인가 처리 구현

+) Disclaimer : 필자는 현재 Spring boot를 활용한 Rest API를 개발 중.

1. JWT 인증 구현

1) JwtService 클래스 구현

  • JWT token을 생성하고, claim에 담긴 정보를 추출하는 메소드 개발.
package com.example.demo.utils;
//라이브러리 생략

@Service
public class JwtService {

    /*
    JWT 생성
    @param userNum
    @return String
     */
    public String createJwt(int userNum){
        Date now = new Date();
        return Jwts.builder()
                .setHeaderParam("type","jwt")
                .claim("userNum",userNum)
                .setIssuedAt(now)
                .setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24*365))) //발급날짜 계산
                .signWith(SignatureAlgorithm.HS256, Secret.JWT_SECRET_KEY) //signature 부분
                .compact();
    }

    /*
    Header에서 X-ACCESS-TOKEN 으로 JWT 추출
    @return String
     */
    public String getJwt(){
        HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
        return request.getHeader("X-ACCESS-TOKEN");
    }

    /*
    JWT에서 userIdx 추출
    @return int
    @throws BaseException
     */
    public int getUserNum() throws BaseException{
        //1. JWT 추출
        String accessToken = getJwt();
        if(accessToken == null || accessToken.length() == 0){
            throw new BaseException(EMPTY_JWT);
        }

        // 2. JWT parsing
        Jws<Claims> claims;
        try{
            claims = Jwts.parser()
                    .setSigningKey(Secret.JWT_SECRET_KEY)
                    .parseClaimsJws(accessToken);
        } catch (Exception ignored) {
            throw new BaseException(INVALID_JWT);
        }

        // 3. userNum 추출
        return claims.getBody().get("userNum",Integer.class);
    }

}

 

2) JWT 로그인 인증 구현

  • jwt token을 활용한 실제 로그인 과정 구현

1️⃣ AccountProvider : userId, userPW 담은 객체 PostAuthReq을 가져와 입력값에 대한 validation 처리 후, 로그인 처리를 하러 accountProvider로 이동.

//AccountController 부분
/**
     * 유저 로그인
     * [POST] /accounts/auth
     * @return BaseResponse<String>
     */
    @ResponseBody
    @PostMapping("/auth")
    public BaseResponse<PostAuthRes> accountAuth(@RequestBody PostAuthReq postAuthReq) throws BaseException {
			//입력값에 대한 validation
        if(postAuthReq.getUser_id()==null){
            return new BaseResponse<>(POST_USERS_EMPTY_EMAIL);
        }
        if(!isRegexEmail(postAuthReq.getUser_id())){
            return new BaseResponse<>(POST_USERS_INVALID_EMAIL);
        }
        int checkAccountStatus=accountProvider.checkAccountStatus(postAuthReq.getUser_id());
        if(checkAccountStatus==user_deactivated){
            //비교시에 상수처리
            return new BaseResponse<>(ACCOUNT_DEACTIVATED);
        }
        try{
            PostAuthRes postAuthRes=accountProvider.accountAuth(postAuthReq);
            return new BaseResponse<>(postAuthRes);

        }catch(BaseException exception){
            return new BaseResponse<>(exception.getStatus());
        }
    }

2️⃣ AccountProvider

  • 입력값에 대한 비교 처리
  • 기존 db에는 회원가입 시에 pw값을 알고리즘에 따라 encrypt해 저장하였으므로, request로 날아온 pw값을 encrypt한후, 실제 유저 pw와 대조하는 과정 거침
  • 이후 jwt발급하여 로그인 처리.
public PostAuthRes accountAuth(PostAuthReq postAuthReq) throws BaseException {
        Account acc=accountDao.getPwd(postAuthReq.getUser_id());
        String encryptedPwd;
        try{
            encryptedPwd= new SHA256().encrypt(postAuthReq.getUser_pw());

        }catch(Exception ignored) {
            throw new BaseException(PASSWORD_ENCRYPTION_ERROR);
        }

        if(acc.getUser_pw().equals(encryptedPwd)){
            int userNum=acc.getUser_num();
            String jwt=jwtService.createJwt(userNum);
            return new PostAuthRes(userNum,jwt);
        }
        else{
            throw new BaseException(FAILED_TO_LOGIN);
        }
    }

3️⃣ AccountDao

  • userId, userPW DB와 대조하여 각각 필요한 값 select 해옴.
public int checkAccountStatus(String userId){
        String checkAccQuery = "select user_activate_status from User where user_id=?";
        String checkAccParam=userId;
        return this.jdbcTemplate.queryForObject(checkAccQuery,
                int.class,
                checkAccParam);

    }

    public Account getPwd(String userId){
        String getPwdQuery = "select * from User where user_id=?";
        String getPwdParam=userId;
        return this.jdbcTemplate.queryForObject(getPwdQuery,
                (rs,rowNum)-> new Account(
                        rs.getInt("user_num"),
                        rs.getString("user_id"),
                        rs.getString("user_pw"),
                        rs.getInt("user_nation_id"),
                        rs.getString("user_phone"),
                        rs.getInt("user_membership_id"),
                        rs.getDate("user_membership_date"),
                        rs.getTimestamp("user_join_date"),
                        rs.getInt("user_payment_id")
                ),getPwdParam);

    }
  • 차후에 로그인이 수행된 후 처리되어야 할 메소드들에 관해서 JWT 유효성 검증후에 남은 작업 수행.
    • EX) 유저 정보 수정, 프로필 정보 수정, 영상물 리스트 조회..etc
  • 실제 사용 예시
    • postman을 통해서 id, pw를 json 객체로 생성하여 request했다.
    • 일련의 과정을 거친 이후에 user number와 jwt 토큰이 response로 돌아왔다.

2. Interceptor를 활용한 회원 인가 프로세스 구현

interceptor를 활용하기 이전에 필자는  jwt토큰을 검증하는 코드를 모든 컨트롤러 메소드에 일일히 추가해 넣었다. 이는 매우 비효율적이며, 반복적인 코드를 계속 작성하게 한다.

 

- 따라서 http request가 서버로 오는 동시에 자동적으로 먼저 request를 intercept해 jwt 검증 과정을 진행하면 따로 컨트롤러에 코드를 추가할 필요가 없다.

 

 

1️⃣ Interceptor 구현

  • 인터셉터 동작 프로세스
    • 우선 인가가 필요한 api인지 확인한다. 예를 들어 회원정보 변경, 게시물 작성..등등 많은 api들이 인증을 필요로 한다.
    • 만약 인가가 필요하지 않은 api는 noAuth 어노테이션을 포함해주었으니 따로 인증이 필요하지 않으므로 바로 true를 리턴하여 메소드를 실행한다.
    • 만약 인가가 필요한 api라면 “X-ACCESS-TOKEN”에 담긴 useridx값을 추출하여 request값에 포함하고, 다시 컨트롤러로 넘겨 남은 작업을 수행한다.
    • 만약 jwt가 유효하지 않거나 존재하지 않으면 로그인을 다시 수행해야 하므로 로그인 api로 넘겨주고, 추후 인증이후에 다시 해당 페이지로 돌아올수 있도록 redirect_uri를 추가해서 보내준다.
@RequiredArgsConstructor
public class AuthenticationInterceptor implements HandlerInterceptor {
    private final JwtService jwtService;
    private final ObjectMapper objectMapper; //자바 객체를 json으로 serialization

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler) throws Exception {
        boolean check=checkAnnotation(handler, NoAuth.class);
        if(check) return true;

        try{
            int userNumByJwt = jwtService.getUserNum();
            request.setAttribute("userNum",userNumByJwt);
        }catch(BaseException exception){
            String requestURI= request.getRequestURI();

            Map<String,String> map=new HashMap<>();
            map.put("requestURI","/app/accounts/auth?redirectURI="+requestURI);
            //redirectURI는 로그인 절차가 끝내고 다시 시도했던 페이지로 돌아가기 위해 JSON 정보에 포함시킨다.
            String json=objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(map);
            response.getWriter().write(json);

            return false;
        }
        return true;
    }

    private boolean checkAnnotation(Object handler,Class cls){
        HandlerMethod handlerMethod=(HandlerMethod) handler;
        if(handlerMethod.getMethodAnnotation(cls)!=null){ //해당 어노테이션이 존재하면 true.
            return true;
        }
        return false;
    }
}

2️⃣ WebConfig 추가

  • Configuration 파일을 만들어 controller 수행 이전에 interceptor가 작동하도록 해준다.
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {

    private final JwtService jwtService;
    private final ObjectMapper objectMapper;

    @Override
    public void addInterceptors(InterceptorRegistry reg){
        reg.addInterceptor(new AuthenticationInterceptor(jwtService,objectMapper))
                .order(1)
                .addPathPatterns("/**"); //interceptor 작업이 필요한 path를 모두 추가한다.
                //.excludePathPatterns("app/accounts","/app/accounts/auth","app/videos/**");
                // 인가작업에서 제외할 API 경로를 따로 추가할수도 있으나, 일일히 따로 추가하기 어려우므로 어노테이션을 따로 만들어 해결한다.
    }
}

3️⃣ NoAuth 어노테이션 추가

  • 따로 인증이 필요하지 않은 api 메소드 위에 어노테이션 형식으로 추가하기 위해 커스텀 어노테이션을 하나 만들어주도록 하겠다.
  • configuration에 필터로 path를 일일히 추가할수도 있으나, 일일히 체크하는건 비효율적이다.
  • 추후에 enum을 추가하여 회원/ 관리자 등의 role을 생성할 수도 있다.
@Retention(RetentionPolicy.RUNTIME) //어노테이션 레벨을 결정짓는다.
@Target({ElementType.TYPE,ElementType.METHOD})//선언된 어노테이션이 적용될수 있는 위치를 결정. TYPE-class,interface,enum에 적용.
public @interface NoAuth {
}

 

  • 실제 사용 예시
    • Postman을 통해서 인가가 필요한 api를 호출할때, header에 "X-ACCESS-TOKEN" 값으로 앞서 발급받은 jwt 토큰 값을 추가해주었다.
    • 인가가 성공하면 다음 프로세스를 자동으로 실행한다.