[감독버전] 개발 - 로그인

ohahsis·2024년 5월 13일
post-thumbnail

📍 개발 순서

  • 로그인 model과 dto 작성
  • 토큰 model과 상수 작성
  • 토큰 1차 Service 구현
  • 로그인 Service 구현
  • 로그인 Controller 구현
  • 토큰 2차 Service 구현 : resolver를 이용한 사용자 인증 적용

📍 개발

1. 로그인 model과 dto 작성

AuthUser 모델 작성

src/main/java/ohahsis/dailydirector/auth/model/AuthUser.java

@Data
public class AuthUser {
    private final Long id;

    public Long getId() {
        return this.id;
    }
}

로그인 요청 dto 작성

src/main/java/ohahsis/dailydirector/auth/dto/request/AuthLoginRequest.java

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

로그인 응답 dto 작성

src/main/java/ohahsis/dailydirector/auth/dto/request/AuthLoginResponse.java

@Data
@AllArgsConstructor
public class AuthLoginResponse {
    private String nickname;
    private String token;
}

2. 토큰 model과 상수 작성

jwt secret key 등록

src/main/resources/application.yml

jwt:
  secret:
    key: (생성한 코드 작성)
  • key 생성 방법
    • 맥에서 진행하기 때문에 터미널에서 openSSL 명령어로 생성했다.
    • 다음 명령어를 입력하면 키가 생성된다.
		openssl rand -hex 64

AuthToken 모델 작성

src/main/java/ohahsis/dailydirector/auth/model/AuthToken.java

@Data
public class AuthToken {
    private final String key;
    private final String token;

    public AuthToken(String token) {
        this.key = AUTH_TOKEN_HEADER_KEY;
        this.token = token;
    }
}

토큰 상수 작성

src/main/java/ohahsis/dailydirector/auth/AuthConstants.java

public class AuthConstants {
    public static final String AUTH_TOKEN_HEADER_KEY = "X-FP-AUTH-TOKEN";
}

3. 토큰 1차 Service 구현

TokenService V1 구현

src/main/java/ohahsis/dailydirector/auth/application/TokenService.java

@Slf4j
@Component
@RequiredArgsConstructor
public class TokenService {
    private final UserRepository userRepository;
    private String key;

    @Value("${jwt.secret.key}")
    public void getSecretKey(String secretKey) {
        key = secretKey;
    }

    // login api 에 적용
    public String jwtBuilder(Long id, String nickname) {
        Claims claims = Jwts.claims();
        claims.put("nickname", nickname);
        claims.put("uid", id);
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims)
                // TODO : 유효기간 설정은 다음 MVC에서 진행한다.
                // .setExpiration(new Date(now.getTime() + accessTokenValidMillisecond))
                .signWith(SignatureAlgorithm.HS256, key.getBytes())
                .compact();
    }
}
  • 클레임(Claim): 토큰에 담을 하나의 정보, 한 조각의 정보를 클레임이라고 한다. 클레임은 키-값 의 한 쌍으로 이루어져있다.

4. 로그인 Service 구현

AuthService 구현

src/main/java/ohahsis/dailydirector/auth/application/AuthService.java

@Service
@RequiredArgsConstructor
public class AuthService {
    private final UserRepository userRepository;
    private final TokenService tokenService;

    public AuthLoginResponse login(AuthLoginRequest request) {
        var user = userRepository.findByEmailAndPassword(request.getEmail(), request.getPassword())
                .orElseThrow(() -> new AuthLoginException(ErrorType.FAIL_TO_LOGIN_ERROR));
        var token = tokenService.jwtBuilder(user.getId(), user.getNickname());

        return new AuthLoginResponse(user.getNickname(), token);
    }
}

AuthLoginException 예외 등록

public class AuthLoginException extends BusinessException {
    public AuthLoginException(ErrorType errorType) {
        super(errorType);
    }
}

5. 로그인 Controller 구현

AuthController 구현

src/main/java/ohahsis/dailydirector/auth/presentation/AuthController.java

@RestController
@RequestMapping(path = "/api/auth", produces = MediaType.APPLICATION_JSON_VALUE)
@RequiredArgsConstructor
public class AuthController {
    private final AuthService authService;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody AuthLoginRequest request) {
        var response = authService.login(request);
        return ResponseDto.ok(response);
    }
}

응답 dto 작성

@Data
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED) 
public class ResponseDto<T> implements Serializable { 
    private T data;

    public static <T> ResponseEntity<ResponseDto<T>> ok(T data) {
        return ResponseEntity.ok(new ResponseDto<T>(data));
    }

    public static <T> ResponseEntity<ResponseDto<T>> created(T data) {
        return ResponseEntity.status(HttpStatus.CREATED).body(new ResponseDto<T>(data));
    }

    public static ResponseEntity<Void> noContent() {
        return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
    }
}

6. 토큰 2차 Service 구현 : resolver를 이용한 사용자 인증 적용

TokenService V2 구현

src/main/java/ohahsis/dailydirector/auth/application/TokenService.java

@Slf4j
@Component
@RequiredArgsConstructor
public class TokenService {
    private final UserRepository userRepository;
    private String key;

    @Value("${jwt.secret.key}")
    public void getSecretKey(String secretKey) {
        key = secretKey;
    }

    public void verifyToken(String token) {
        // 토큰을 파싱해서 해당 토큰을 얻고, 토큰이 만료되었으면 에러 발생시킴
        try {
            Jwts.parser().setSigningKey(key.getBytes()).parseClaimsJws(token);
        } catch (Exception e) {
            if (e.getMessage().contains("JWT expired")) {
                throw new AuthorizationException(ErrorType.AUTHORIZATION_ERROR);
            }
            throw new AuthorizationException(ErrorType.AUTHORIZATION_ERROR);   
        }
        
        Long uid = getUserIdFromToken(token);   // token 으로 유저의 id 를 찾아서
        if (!userRepository.existsById(uid)) {  // id 로 DB 조회를 통해 존재 여부 확인
            throw new AuthorizationException(ErrorType.AUTHORIZATION_ERROR);
        }
    }

    // login 외 타 api 에서 AuthUser 검증 시 적용
    public AuthUser getAuthUser(AuthToken token) {
        verifyToken(token.getToken());  // 토큰 관련 문제가 생길 경우 error
        var id = getUserIdFromToken(token.getToken());
        var user =
                userRepository
                        .findById(id)
                        .orElseThrow(
                                () -> new AuthorizationException(ErrorType.AUTHORIZATION_ERROR));
        return new AuthUser(id);
    }

    public Long getUserIdFromToken(String token) {
        return Long.valueOf(
                (Integer)
                        Jwts.parser()
                                .setSigningKey(key.getBytes())
                                .parseClaimsJws(token)
                                .getBody()
                                .get("uid"));
    }

    // login api 에 적용
    public String jwtBuilder(Long id, String nickname) {
        Claims claims = Jwts.claims();
        claims.put("nickname", nickname);
        claims.put("uid", id);
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims)
                // TODO : 유효기간 설정은 다음 MVC에서 진행한다.
                // .setExpiration(new Date(now.getTime() + accessTokenValidMillisecond))
                .signWith(SignatureAlgorithm.HS256, key.getBytes())
                .compact();
    }
}

AuthorizationException 예외 등록

public class AuthorizationException extends BusinessException {
    public AuthorizationException(ErrorType errorType) {
        super(errorType);
    }
}

UserArgumentResolver 구현

src/main/java/ohahsis/dailydirector/config/resolver/UserArgumentResolver.java

@Component
@RequiredArgsConstructor
public class UserArgumentResolver implements HandlerMethodArgumentResolver {
    private final TokenService tokenService;


    // 파라미터 타입에 AuthUser 가 있으면 잡아채서 동작하는 resolver

    @Override
    public boolean supportsParameter(MethodParameter parameter) {   // 개념: 해당 메서드의 매개변수에 대해, 해당 resolver 가 지원하는 것인지를 체크한다.
        return parameter.getParameterType().equals(AuthUser.class); // 사용: 파라미터가 AuthUser.class 를 가지고 있으면 true 를, 아니면 false 를 반환
    }

    @Override
    public Object resolveArgument(                                  // 개념: 매개변수로 넣어줄 값을 제공한다.
            MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        var httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest();

        var accessToken = httpServletRequest.getHeader(AUTH_TOKEN_HEADER_KEY);  // request 의 헤더에서 토큰을 꺼낸다.

        if (accessToken == null) {
            if (parameter.isOptional()) {
                return null;
            }
            accessToken = "";
        }

        var token = new AuthToken(accessToken);

        return tokenService.getAuthUser(token);                     // 사용: token 을 통해 얻은 AuthUser 객체를 반환한다.
    }
}
  • Argument Resolver
    • 컨트롤러 메서드의 파라미터 중 특정 조건에 맞는 파라미터가 있다면, 요청에 들어온 값을 이용해 원하는 객체를 만들어 바인딩해줄 수 있다.

UserArgumentResolver 등록

src/main/java/ohahsis/dailydirector/config/web/WebConfig.java

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
    private final UserArgumentResolver userArgumentResolver;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(userArgumentResolver);
    }
}

📍 결과

로그인 성공 검증

img

이메일 오류 검증

img

비밀번호 오류 검증

img

다음으로

  • 글쓰기 api 를 구현해보자.
profile
백엔드 개발자입니다.

0개의 댓글