Skip to content

Commit

Permalink
✨ feat: 로그인 API 작성 (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
hyunmin0317 committed Feb 28, 2024
1 parent b29864e commit d7b64d9
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.smunity.petition.domain.account.jwt.exception;

import com.smunity.petition.global.common.code.BaseErrorCode;
import com.smunity.petition.global.common.exception.GeneralException;
import lombok.Getter;

@Getter
public class SecurityCustomException extends GeneralException {

private final Throwable cause;

public SecurityCustomException(BaseErrorCode errorCode) {
super(errorCode);
this.cause = null;
}

public SecurityCustomException(BaseErrorCode errorCode, Throwable cause) {
super(errorCode);
this.cause = cause;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.smunity.petition.domain.account.jwt.exception;

import com.smunity.petition.global.common.ApiResponse;
import com.smunity.petition.global.common.code.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum TokenErrorCode implements BaseErrorCode {

INVALID_TOKEN(HttpStatus.BAD_REQUEST, "SEC4001", "잘못된 형식의 토큰입니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "SEC4010", "인증이 필요합니다."),
TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "SEC4011", "토큰이 만료되었습니다."),
TOKEN_SIGNATURE_ERROR(HttpStatus.UNAUTHORIZED, "SEC4012", "토큰이 위조되었거나 손상되었습니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "SEC4030", "권한이 없습니다."),
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "SEC4041", "토큰이 존재하지 않습니다."),
INTERNAL_SECURITY_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SEC5000", "인증 처리 중 서버 에러가 발생했습니다."),
INTERNAL_TOKEN_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "SEC5001", "토큰 처리 중 서버 에러가 발생했습니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;

@Override
public ApiResponse<Void> getErrorResponse() {
return ApiResponse.onFailure(code, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.smunity.petition.domain.account.jwt.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.smunity.petition.domain.account.jwt.dto.JwtDto;
import com.smunity.petition.domain.account.jwt.userdetails.CustomUserDetails;
import com.smunity.petition.domain.account.jwt.util.HttpResponseUtil;
import com.smunity.petition.domain.account.jwt.util.JwtUtil;
import com.smunity.petition.global.common.ApiResponse;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.Map;

@Slf4j
@RequiredArgsConstructor
public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {

private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil;

@Override
public Authentication attemptAuthentication(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response
) throws AuthenticationException {
log.info("[*] Login Filter");

Map<String, Object> requestBody;
try {
requestBody = getBody(request);
} catch (IOException e) {
throw new AuthenticationServiceException("Error of request body.");
}

log.info("[*] Request Body : " + requestBody);

String username = (String) requestBody.get("username");
String password = (String) requestBody.get("password");

UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password,
null);

return authenticationManager.authenticate(authToken);
}

@Override
protected void successfulAuthentication(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain chain,
@NonNull Authentication authentication) throws IOException {
log.info("[*] Login Success");

CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();

log.info("[*] Login with " + customUserDetails.getUsername());

JwtDto jwtDto = new JwtDto(
jwtUtil.createJwtAccessToken(customUserDetails),
jwtUtil.createJwtRefreshToken(customUserDetails)
);

HttpResponseUtil.setSuccessResponse(response, HttpStatus.CREATED, jwtDto);
}

@Override
protected void unsuccessfulAuthentication(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull AuthenticationException failed) throws IOException {

logger.info("[*] Login Fail");

String errorMessage;
if (failed instanceof BadCredentialsException) {
errorMessage = "Bad credentials";
} else if (failed instanceof LockedException) {
errorMessage = "Account is locked";
} else if (failed instanceof DisabledException) {
errorMessage = "Account is disabled";
} else if (failed instanceof UsernameNotFoundException) {
errorMessage = "Account not found";
} else if (failed instanceof AuthenticationServiceException) {
errorMessage = "Error occurred while parsing request body";
} else {
errorMessage = "Authentication failed";
}
HttpResponseUtil.setErrorResponse(
response, HttpStatus.UNAUTHORIZED,
ApiResponse.onFailure(
HttpStatus.BAD_REQUEST.name(),
errorMessage,
null
)
);
}

private Map<String, Object> getBody(HttpServletRequest request) throws IOException {

StringBuilder stringBuilder = new StringBuilder();
String line;

try (BufferedReader bufferedReader = request.getReader()) {
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
}

String requestBody = stringBuilder.toString();
ObjectMapper objectMapper = new ObjectMapper();

return objectMapper.readValue(requestBody, Map.class);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.smunity.petition.domain.account.jwt.userdetails;

import com.smunity.petition.domain.account.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class CustomUserDetails implements UserDetails {

private final String username;
private final String password;
private final Boolean isStaff;

public CustomUserDetails(User user) {
username = user.getUserName();
password = user.getPassword();
isStaff = user.getIsStaff();
}

public CustomUserDetails(String username, String password, Boolean isStaff) {
this.username = username;
this.password = password;
this.isStaff = isStaff;
}

public Boolean getStaff() {
return isStaff;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.smunity.petition.domain.account.jwt.userdetails;

import com.smunity.petition.domain.account.entity.User;
import com.smunity.petition.domain.account.exception.AccountsExceptionHandler;
import com.smunity.petition.domain.account.repository.user.UserRepository;
import com.smunity.petition.global.common.code.status.ErrorCode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUserName(username)
.orElseThrow(() -> new AccountsExceptionHandler(ErrorCode.USER_NOT_FOUND));

log.info("[*] User found : " + user.getUserName());

return new CustomUserDetails(user);
}

public User userDetailsToUser(UserDetails userDetails) {
return userRepository.findByUserName(userDetails.getUsername())
.orElseThrow(() -> new AccountsExceptionHandler(ErrorCode.USER_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.smunity.petition.domain.account.jwt.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.smunity.petition.global.common.ApiResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;

import java.io.IOException;

@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class HttpResponseUtil {

private static final ObjectMapper objectMapper = new ObjectMapper();

public static void setSuccessResponse(HttpServletResponse response, HttpStatus httpStatus, Object body) throws
IOException {
log.info("[*] Success Response");
String responseBody = objectMapper.writeValueAsString(ApiResponse.onSuccess(body));
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(httpStatus.value());
response.setCharacterEncoding("UTF-8");
response.getWriter().write(responseBody);
}

public static void setErrorResponse(HttpServletResponse response, HttpStatus httpStatus, Object body) throws
IOException {
log.info("[*] Failure Response");
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(httpStatus.value());
response.setCharacterEncoding("UTF-8");
objectMapper.writeValue(response.getOutputStream(), body);
}

}

0 comments on commit d7b64d9

Please sign in to comment.