Skip to content

Commit

Permalink
✨ Implement Account Login Process (#8)
Browse files Browse the repository at this point in the history
Email, Password를 통한 Account Login Logic 구현했습니다. 로그인 성공 시 Cookie에 Jwt Token 발급됩니다.

Related: #5
  • Loading branch information
L-U-Ready authored Oct 13, 2024
1 parent f9216f9 commit 156f1a9
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package the_monitor.application.dto.request;

import jakarta.validation.constraints.NotBlank;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class AccountLoginRequest {

@NotBlank(message = "email은 필수입니다.")
private String email;

@NotBlank(message = "password는 필수입니다.")
private String password;

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.servlet.http.HttpServletResponse;
import the_monitor.application.dto.request.AccountCreateRequest;
import the_monitor.application.dto.request.AccountLoginRequest;
import the_monitor.domain.model.Account;

import java.io.IOException;
Expand All @@ -15,4 +16,6 @@ public interface AccountService {

void verifyEmail(String certifiedKey, HttpServletResponse response) throws IOException;

String accountLogin(AccountLoginRequest request, HttpServletResponse response);

}
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package the_monitor.application.serviceImpl;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import the_monitor.application.dto.request.AccountCreateRequest;
import the_monitor.application.dto.request.AccountLoginRequest;
import the_monitor.application.service.AccountService;
import the_monitor.application.service.EmailService;
import the_monitor.common.ApiException;
import the_monitor.common.ErrorStatus;
import the_monitor.domain.model.Account;
import the_monitor.domain.repository.AccountRepository;
import the_monitor.infrastructure.jwt.JwtProvider;

import java.io.IOException;
import java.util.UUID;
Expand All @@ -24,6 +27,7 @@ public class AccountServiceImpl implements AccountService {

private final AccountRepository accountRepository;
private final EmailService emailService;
private final JwtProvider jwtProvider;

@Override
public Account findAccountById(Long id) {
Expand Down Expand Up @@ -74,6 +78,25 @@ public void verifyEmail(String certifiedKey, HttpServletResponse response) throw

}

@Override
public String accountLogin(AccountLoginRequest request, HttpServletResponse response) {

Account account = accountRepository.findByEmail(request.getEmail());

if (account == null) throw new ApiException(ErrorStatus._ACCOUNT_NOT_FOUND);

if (!account.getPassword().equals(request.getPassword())) throw new ApiException(ErrorStatus._WRONG_PASSWORD);

if (account.isEmailVerified()) {

jwtProvider.setAddCookieToken(account, response);

return "로그인 성공";
}

return "이메일 인증 필요";
}

private String generateCertifiedKey() {
return UUID.randomUUID().toString(); // UUID 생성
}
Expand Down
1 change: 1 addition & 0 deletions src/main/java/the_monitor/common/ErrorStatus.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum ErrorStatus implements BaseErrorCode {

// 일반 응답
_ACCOUNT_NOT_FOUND(HttpStatus.NOT_FOUND, "ACCOUNT404", "해당 계정을 찾을 수 없습니다."),
_WRONG_PASSWORD(HttpStatus.BAD_REQUEST, "ACCOUNT400", "비밀번호가 일치하지 않습니다."),
_INVALID_CERTIFIED_KEY(HttpStatus.BAD_REQUEST, "ACCOUNT400", "유효하지 않은 인증키입니다."),
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ public interface AccountRepository extends JpaAccountRepository {

Account findByEmailCertificationKey(String certifiedKey);

Account findByEmail(String email);

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

if (isPublicUrl(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}

String accessToken = resolveTokenFromCookie(request); // 쿠키에서 JWT 토큰 추출

if (accessToken == null) {
request.setAttribute("exception", ErrorStatus._JWT_NOT_FOUND);
filterChain.doFilter(request, response);
Expand Down Expand Up @@ -67,6 +69,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
log.info("===================== EXPIRED ACCESS-TOKEN");
request.setAttribute("exception", ErrorStatus._JWT_EXPIRED);
break;

}

filterChain.doFilter(request, response);
Expand All @@ -91,6 +94,7 @@ private boolean isPublicUrl(String requestUrl) {
requestUrl.equals("/api/v1/accounts") ||
requestUrl.equals("/api/v1/accounts/createAccount") ||
requestUrl.equals("/api/v1/accounts/verify") ||
requestUrl.equals("/api/v1/accounts/login") ||
requestUrl.startsWith("/api/kindergartens/**") ||
requestUrl.startsWith("/swagger-ui/**") ||
requestUrl.startsWith("/swagger-resources/**") ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class JwtExceptionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

try {
filterChain.doFilter(request, response);
} catch (ApiException e) {
Expand All @@ -33,4 +34,5 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
}

}

}
48 changes: 48 additions & 0 deletions src/main/java/the_monitor/infrastructure/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
Expand All @@ -33,15 +36,47 @@ public class JwtProvider {
public JwtProvider(@Value("${jwt.secret_key}") String secretKey,
@Value("${jwt.access_token_expire}") Long accessTokenExpire,
@Value("${jwt.refresh_token_expire}") Long refreshTokenExpire) {

this.key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // 안전한 256비트 비밀 키 생성
this.ACCESS_TOKEN_EXPIRE_TIME = accessTokenExpire;
this.REFRESH_TOKEN_EXPIRE_TIME = refreshTokenExpire;

}

public void setAddCookieToken(Account account, HttpServletResponse response) {

// AccessToken과 RefreshToken 생성
String accessToken = generateAccessToken(account);
String refreshToken = generateRefreshToken(account);

int accessTokenExpireTime = Math.toIntExact(ACCESS_TOKEN_EXPIRE_TIME / 1000);
int refreshTokenExpireTime = Math.toIntExact(REFRESH_TOKEN_EXPIRE_TIME / 1000);

// 쿠키에 AccessToken 저장
Cookie accessTokenCookie = new Cookie("accessToken", accessToken);
accessTokenCookie.setHttpOnly(true); // HttpOnly 설정
accessTokenCookie.setSecure(true); // HTTPS에서만 전송
accessTokenCookie.setPath("/"); // 모든 경로에서 쿠키 접근 가능
accessTokenCookie.setMaxAge(accessTokenExpireTime); // 만료 시간 설정

// 쿠키에 RefreshToken 저장
Cookie refreshTokenCookie = new Cookie("refreshToken", refreshToken);
refreshTokenCookie.setHttpOnly(true);
refreshTokenCookie.setSecure(true);
refreshTokenCookie.setPath("/");
refreshTokenCookie.setMaxAge(refreshTokenExpireTime);

// 응답에 쿠키 추가
response.addCookie(accessTokenCookie);
response.addCookie(refreshTokenCookie);

}

/**
* JWT 토큰 생성 (회원가입 또는 로그인 후 호출)
*/
public String generateAccessToken(Account account) {

Date expiredAt = new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE_TIME);

return Jwts.builder()
Expand All @@ -51,9 +86,11 @@ public String generateAccessToken(Account account) {
.setExpiration(expiredAt)
.signWith(key, SignatureAlgorithm.HS256)
.compact();

}

public String generateRefreshToken(Account account) {

Date expiredAt = new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRE_TIME);
return Jwts.builder()
.claim("account_id", account.getId()) // User의 ID를 포함
Expand All @@ -62,6 +99,7 @@ public String generateRefreshToken(Account account) {
.setExpiration(expiredAt)
.signWith(key, SignatureAlgorithm.HS256)
.compact();

}

/**
Expand All @@ -75,6 +113,7 @@ public Long getAccountId(String token) {
* JWT 토큰에서 Claims(토큰 정보)를 파싱
*/
public Claims parseClaims(String token) {

try {
return Jwts.parserBuilder()
.setSigningKey(key)
Expand All @@ -84,12 +123,14 @@ public Claims parseClaims(String token) {
} catch (ExpiredJwtException e) {
throw new ApiException(ErrorStatus._JWT_EXPIRED);
}

}

/**
* JWT 토큰 유효성 검증
*/
public String validateToken(String token) {

try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return "VALID";
Expand All @@ -98,12 +139,14 @@ public String validateToken(String token) {
} catch (SignatureException | MalformedJwtException e) {
return "INVALID";
}

}

/**
* JWT 토큰을 기반으로 Authentication 객체 생성 (User 정보 포함)
*/
public Authentication getAuthentication(String token) {

Claims claims = parseClaims(token);

// JWT에서 클레임을 가져옵니다.
Expand All @@ -113,24 +156,29 @@ public Authentication getAuthentication(String token) {
// 사용자 정보 기반으로 Authentication 객체 생성
UserDetails userDetails = new org.springframework.security.core.userdetails.User(email, "", new ArrayList<>());
return new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities());

}

/**
* SecurityContext에 인증 객체 설정
*/
private void setContextHolder(Authentication authentication) {

SecurityContextHolder.getContext().setAuthentication(authentication);

}

/**
* JWT 토큰의 만료 시간을 가져옵니다.
*/
public Long getExpiration(String token) {

Date expiration = Jwts.parserBuilder().setSigningKey(key)
.build().parseClaimsJws(token).getBody().getExpiration();

long now = new Date().getTime();
return expiration.getTime() - now;

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,27 @@ public static BCryptPasswordEncoder bCryptPasswordEncoder() {
}

private static final String[] WHITE_LIST_URL = {

// Application URLs
"/api/v1/**",
"/v3/api-docs/**",
"/swagger-resources/**",
"/swagger-ui/**",
"/favicon.ico"

};

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {

return web -> web.ignoring()
.requestMatchers(WHITE_LIST_URL);

}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

http
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
Expand All @@ -73,10 +78,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class);

return http.build();

}

@Bean
public CorsConfigurationSource corsConfigurationSource() {

CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(List.of("*"));
// 모든 메서드 허용
Expand All @@ -93,5 +100,7 @@ public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;

}

}
13 changes: 6 additions & 7 deletions src/main/java/the_monitor/presentation/AccountController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import the_monitor.application.dto.request.AccountCreateRequest;
import the_monitor.application.dto.request.AccountLoginRequest;
import the_monitor.application.service.AccountService;
import the_monitor.common.ApiResponse;

Expand All @@ -30,14 +31,12 @@ public void verifyEmail(@RequestParam("certifiedKey") String certifiedKey, HttpS
accountService.verifyEmail(certifiedKey, response);

}
//
// @GetMapping("/sendVerificationEmail")
// public ApiResponse<String> sendVerificationEmail(@RequestParam("email") String email) {
//
// return ApiResponse.onSuccess(accountService.sendVerificationEmail(email));
//
// }

@GetMapping("/login")
public ApiResponse<String> Login(@RequestBody @Valid AccountLoginRequest request, HttpServletResponse response) {

return ApiResponse.onSuccess(accountService.accountLogin(request, response));

}

}

0 comments on commit 156f1a9

Please sign in to comment.