Skip to content

Commit

Permalink
Merge pull request #185 from ITA-OneByte/dev
Browse files Browse the repository at this point in the history
[release] 프론트 요구사항
  • Loading branch information
dpfls0922 authored Jan 13, 2025
2 parents cf736d1 + d58f15a commit 45a6009
Show file tree
Hide file tree
Showing 30 changed files with 691 additions and 430 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package classfit.example.classfit.auth.annotation;

import classfit.example.classfit.auth.security.config.SecurityUtil;
import classfit.example.classfit.common.exception.ClassfitException;
import classfit.example.classfit.common.exception.ClassfitAuthException;
import classfit.example.classfit.common.util.SecurityUtil;
import classfit.example.classfit.member.domain.Member;
import classfit.example.classfit.member.repository.MemberRepository;
import lombok.NonNull;
Expand Down Expand Up @@ -30,6 +30,6 @@ public boolean supportsParameter(MethodParameter parameter) {
public Object resolveArgument(@NonNull MethodParameter parameter, ModelAndViewContainer mavContainer, @NonNull NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Long currentMemberId = SecurityUtil.getCurrentMemberId();
return memberRepository.findById(currentMemberId)
.orElseThrow(() -> new ClassfitException("사용자를 찾을 수 없습니다.", HttpStatus.NOT_FOUND));
.orElseThrow(() -> new ClassfitAuthException("사용자를 찾을 수 없습니다.", HttpStatus.NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
package classfit.example.classfit.auth.dto.request;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

import java.util.Optional;
import java.util.Set;

public record UserRequest
(
@NotBlank(message = "이메일은 공백일 수 없습니다.")
@Email(message = "형식이 올바르지 않습니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
String email,

@NotBlank(message = "비밀번호는 공백일 수 없습니다.")
@Size(min = 8, max = 20, message = "비밀번호는 8 ~ 20자리로 입력해 주세요")
String password
) {

public Optional<String> validate() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();

Set<ConstraintViolation<UserRequest>> violations = validator.validate(this);
return violations.stream()
.map(ConstraintViolation::getMessage)
.findFirst();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package classfit.example.classfit.auth.exception;

import classfit.example.classfit.common.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
ApiResponse.errorResponse(response, "접근 권한이 없습니다.", HttpStatus.UNAUTHORIZED.value());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package classfit.example.classfit.auth.exception;

import classfit.example.classfit.common.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
ApiResponse.errorResponse(response, "접근 권한이 없습니다.", HttpStatus.UNAUTHORIZED.value());
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package classfit.example.classfit.auth.security.custom;

import classfit.example.classfit.common.exception.ClassfitException;
import classfit.example.classfit.common.exception.ClassfitAuthException;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
Expand All @@ -19,26 +18,18 @@ public CustomAuthenticationProvider(UserDetailsService userDetailsService, Passw

@Override
public Authentication authenticate(Authentication authentication) {
try {
Authentication result = super.authenticate(authentication);
CustomUserDetails userDetails = (CustomUserDetails) result.getPrincipal();
Authentication result = super.authenticate(authentication);
CustomUserDetails userDetails = (CustomUserDetails) result.getPrincipal();

if (userDetails.member().getAcademy() == null) {
throw new ClassfitException(
"해당 회원은 학원이 등록되지 않았습니다. 학원을 등록해주세요.",
HttpStatus.UNPROCESSABLE_ENTITY
);
}

return new CustomAuthenticationToken(
authentication.getName(),
authentication.getCredentials().toString(),
result.getAuthorities()
);
} catch (ClassfitException e) {
throw new AuthenticationException(e.getMessage(), e) {
};
if (userDetails.member().getAcademy() == null) {
throw new ClassfitAuthException("해당 회원은 학원이 등록되지 않았습니다.", HttpStatus.UNPROCESSABLE_ENTITY);
}

return new CustomAuthenticationToken(
authentication.getName(),
authentication.getCredentials().toString(),
result.getAuthorities()
);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package classfit.example.classfit.auth.security.custom;

import classfit.example.classfit.common.exception.ClassfitAuthException;
import classfit.example.classfit.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
Expand All @@ -16,6 +18,6 @@ public class CustomUserDetailService implements UserDetailsService {
public CustomUserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return memberRepository.findByEmail(email)
.map(CustomUserDetails::new)
.orElseThrow(() -> new UsernameNotFoundException("해당 계정은 존재하지 않습니다. 이메일: " + email));
.orElseThrow(() -> new ClassfitAuthException("해당 계정은 존재하지 않습니다", HttpStatus.UNAUTHORIZED));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import classfit.example.classfit.auth.dto.request.UserRequest;
import classfit.example.classfit.auth.security.custom.CustomAuthenticationToken;
import classfit.example.classfit.auth.security.jwt.JWTUtil;
import classfit.example.classfit.common.ApiResponse;
import classfit.example.classfit.common.exception.ClassfitAuthException;
import classfit.example.classfit.common.exception.ClassfitException;
import classfit.example.classfit.common.util.CookieUtil;
import classfit.example.classfit.common.util.RedisUtil;
Expand All @@ -15,86 +17,69 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;


@RequiredArgsConstructor
public class CustomLoginFilter extends UsernamePasswordAuthenticationFilter {

{
setFilterProcessesUrl("/api/v1/signin");
}
private static final String CREDENTIAL = "Authorization";
private static final String SECURITY_SCHEMA_TYPE = "Bearer ";
private static final String ACCESS_TOKEN_CATEGORY = "access";
private static final String REFRESH_TOKEN_CATEGORY = "refresh";

private final AuthenticationManager authenticationManager;
private final JWTUtil jwtUtil;
private final RedisUtil redisUtil;

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

try {
ObjectMapper objectMapper = new ObjectMapper();
UserRequest userRequest = objectMapper.readValue(request.getInputStream(), UserRequest.class);

CustomAuthenticationToken authRequest = new CustomAuthenticationToken(userRequest.email(), userRequest.password(), null);
return authenticationManager.authenticate(authRequest);

} catch (IOException e) {
throw new ClassfitException("입력 형식이 잘못되었습니다.", HttpStatus.BAD_REQUEST);
}
UserRequest userRequest = parseRequest(request);
userRequest.validate().ifPresent(errorMessage -> {
throw new ClassfitAuthException(errorMessage, HttpStatus.BAD_REQUEST);
});

CustomAuthenticationToken authRequest = new CustomAuthenticationToken(
userRequest.email(), userRequest.password(), null
);
return authenticationManager.authenticate(authRequest);
}

@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authResult) {

CustomAuthenticationToken customAuth = (CustomAuthenticationToken) authResult;
String role = customAuth.getAuthorities().iterator().next().getAuthority();

Collection<? extends GrantedAuthority> authorities = customAuth.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();
String accessToken = jwtUtil.createJwt(ACCESS_TOKEN_CATEGORY, customAuth.getEmail(), role, 1000 * 60 * 5L);
String refreshToken = jwtUtil.createJwt(REFRESH_TOKEN_CATEGORY, customAuth.getEmail(), role, 1000 * 60 * 60 * 24 * 7L);


String role = auth.getAuthority();
String access = jwtUtil.createJwt("access", customAuth.getEmail(), role, 1000 * 60 * 60 * 3L); // 5분
String refresh = jwtUtil.createJwt("refresh", customAuth.getEmail(), role, 1000 * 60 * 60 * 24 * 7L); // 7일

addRefreshEntity(authResult.getName(), refresh, 1000 * 60 * 60 * 24 * 7L);

res.setHeader("Authorization", "Bearer " + access);
CookieUtil.addCookie(res, "refresh", refresh, 7 * 24 * 60 * 60);
res.setStatus(HttpStatus.OK.value());
redisUtil.setDataExpire(REFRESH_TOKEN_CATEGORY + ":" + customAuth.getEmail(), refreshToken, 60 * 60 * 24 * 7L);
setResponse(res, accessToken, refreshToken);
}

@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException {
if (failed instanceof ClassfitAuthException) {
ApiResponse.errorResponse(res, failed.getMessage(), ((ClassfitAuthException) failed).getHttpStatusCode());
return;
}
ApiResponse.errorResponse(res, "아이디 또는 비밀번호가 잘못되었습니다.", HttpServletResponse.SC_UNAUTHORIZED);
}

if (failed.getCause() instanceof ClassfitException classfitException) {
response.setStatus(classfitException.getHttpStatus().value());
String jsonResponse = String.format(
"{ \"message\": \"%s\", \"status\": %d }",
classfitException.getMessage(),
classfitException.getHttpStatus().value()
);
response.getWriter().write(jsonResponse);
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
String jsonResponse = String.format(
"{ \"message\": \"로그인에 실패하였습니다. 이메일 또는 비밀번호를 확인해주세요.\", \"status\": %d }",
HttpServletResponse.SC_UNAUTHORIZED
);
response.getWriter().write(jsonResponse);
private UserRequest parseRequest(HttpServletRequest request) {
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(request.getInputStream(), UserRequest.class);
} catch (IOException e) {
throw new ClassfitException("입력 형식이 잘못되었습니다.", HttpStatus.BAD_REQUEST);
}
}

private void addRefreshEntity(String email, String refresh, Long expiredMs) {
String redisKey = "refresh:" + email;
redisUtil.setDataExpire(redisKey, refresh, expiredMs);
private void setResponse(HttpServletResponse res, String accessToken, String refreshToken) {
res.setHeader(CREDENTIAL, SECURITY_SCHEMA_TYPE + accessToken);
CookieUtil.setCookie(res, REFRESH_TOKEN_CATEGORY, refreshToken, 7 * 24 * 60 * 60);
res.setStatus(HttpStatus.OK.value());
}
}
Loading

0 comments on commit 45a6009

Please sign in to comment.