Skip to content

Commit

Permalink
Merge pull request #13 from NARAE-FLIWITH/feat/logout
Browse files Browse the repository at this point in the history
로그아웃, 이메일 인증 관련
  • Loading branch information
jjunjji authored May 20, 2024
2 parents e5ca7d8 + c1a7909 commit 09e5c04
Show file tree
Hide file tree
Showing 23 changed files with 320 additions and 65 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ dependencies {
//gpt
implementation 'com.theokanning.openai-gpt3-java:service:0.18.2'

//mail
implementation 'org.springframework.boot:spring-boot-starter-mail'

}

tasks.named('test') {
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/com/narae/fliwith/config/MailConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.narae.fliwith.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import java.util.Properties;

@Configuration
public class MailConfig {
@Value("${spring.mail.username}")
private String id;
@Value("${spring.mail.password}")
private String password;
@Value("${spring.mail.host}")
private String host;
@Value("${spring.mail.port}")
private int port;

@Bean
public JavaMailSender javaMailService() {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();

javaMailSender.setHost(host);
javaMailSender.setUsername(id);
javaMailSender.setPassword(password);
javaMailSender.setPort(port);
javaMailSender.setJavaMailProperties(getMailProperties());
javaMailSender.setDefaultEncoding("UTF-8");
return javaMailSender;
}

private Properties getMailProperties() {
Properties properties = new Properties();
properties.setProperty("mail.transport.protocol", "smtp");
properties.setProperty("mail.smtp.auth", "true");
properties.setProperty("mail.smtp.starttls.enable", "true");
properties.setProperty("mail.debug", "true");
properties.setProperty("mail.smtp.ssl.trust","smtp.gmail.com");
return properties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.http.HttpStatus;
import org.json.JSONObject;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
Expand All @@ -22,7 +23,7 @@ public void handle(HttpServletRequest request, HttpServletResponse response, Acc

private void setResponse(HttpServletResponse response, SecurityExceptionList exceptionCode) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setStatus(HttpStatus.SC_FORBIDDEN);

JSONObject responseJson = new JSONObject();
responseJson.put("timestamp", LocalDateTime.now().withNano(0).toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,22 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
String exception = String.valueOf(request.getAttribute("exception"));
//TODO: 조건별 예외 처리
if("S0004".equals(exception)){
if(exception.equals(SecurityExceptionList.UNKNOWN_ERROR.getErrorCode()))
setResponse(response, SecurityExceptionList.UNKNOWN_ERROR);

else if(exception.equals(SecurityExceptionList.MALFORMED_TOKEN_ERROR.getErrorCode()))
setResponse(response, SecurityExceptionList.MALFORMED_TOKEN_ERROR);

else if(exception.equals(SecurityExceptionList.ILLEGAL_TOKEN_ERROR.getErrorCode()))
setResponse(response, SecurityExceptionList.ILLEGAL_TOKEN_ERROR);

else if(exception.equals(SecurityExceptionList.EXPIRED_TOKEN_ERROR.getErrorCode()))
setResponse(response, SecurityExceptionList.EXPIRED_TOKEN_ERROR);
} else{
setResponse(response, SecurityExceptionList.ACCESS_DENIED);
}

else if(exception.equals(SecurityExceptionList.UNSUPPORTED_TOKEN_ERROR.getErrorCode()))
setResponse(response, SecurityExceptionList.UNSUPPORTED_TOKEN_ERROR);

else setResponse(response, SecurityExceptionList.ACCESS_DENIED);

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/user/**").permitAll()
.requestMatchers("/admin/**").permitAll()
.requestMatchers("/user/profile").authenticated()
.requestMatchers("/user/logout").authenticated()
.anyRequest().authenticated()

)
Expand Down
57 changes: 35 additions & 22 deletions src/main/java/com/narae/fliwith/config/security/util/TokenUtil.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.narae.fliwith.config.security.util;


import static com.narae.fliwith.exception.security.constants.SecurityExceptionList.ACCESS_DENIED;
import static com.narae.fliwith.exception.security.constants.SecurityExceptionList.EXPIRED_TOKEN_ERROR;
import static com.narae.fliwith.exception.security.constants.SecurityExceptionList.ILLEGAL_TOKEN_ERROR;
import static com.narae.fliwith.exception.security.constants.SecurityExceptionList.MALFORMED_TOKEN_ERROR;
import static com.narae.fliwith.exception.security.constants.SecurityExceptionList.UNSUPPORTED_TOKEN_ERROR;

import com.narae.fliwith.config.security.dto.CustomUser;
import com.narae.fliwith.config.security.dto.TokenRes;
import com.narae.fliwith.domain.Token;
import com.narae.fliwith.domain.User;
import com.narae.fliwith.exception.security.ExpiredTokenException;
import com.narae.fliwith.exception.security.InvalidTokenException;
import com.narae.fliwith.exception.user.NotFoundUserException;
import com.narae.fliwith.repository.TokenRepository;
Expand All @@ -15,6 +20,7 @@
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.ServletRequest;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -30,6 +36,7 @@
import java.util.stream.Collectors;

@Component
@Slf4j
public class TokenUtil implements InitializingBean {
private final UserRepository userRepository;
private final TokenRepository tokenRepository;
Expand Down Expand Up @@ -114,44 +121,50 @@ public boolean validateToken(String token, ServletRequest request){
Jws<Claims> claims =Jwts.parser().setSigningKey(key).build().parseClaimsJws(token);
return claims.getBody().getExpiration().after(new Date());
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
request.setAttribute("exception", "4043");
System.out.println("4043");
log.info("잘못된 JWT 서명입니다.");
request.setAttribute("exception", MALFORMED_TOKEN_ERROR.getErrorCode());

} catch (ExpiredJwtException e) {
request.setAttribute("exception", "S0004");
System.out.println("4044");
throw new ExpiredTokenException();
log.info("만료된 JWT 토큰입니다.");
request.setAttribute("exception", EXPIRED_TOKEN_ERROR.getErrorCode());

} catch (UnsupportedJwtException e) {
request.setAttribute("exception", "4045");
System.out.println("4045");
log.info("지원되지 않는 JWT 토큰입니다.");
request.setAttribute("exception", UNSUPPORTED_TOKEN_ERROR.getErrorCode());

} catch (IllegalArgumentException e) {
request.setAttribute("exception", "4046");
System.out.println("4046");
log.info("JWT 토큰이 잘못되었습니다.");
request.setAttribute("exception", ILLEGAL_TOKEN_ERROR.getErrorCode());

} catch (Exception e) {
log.info(e.getMessage());
request.setAttribute("exception", ACCESS_DENIED.getErrorCode());

}
return false;
}

public TokenRes reissue(String token){
//token = refreshToken

//accessToken으로 user를 찾고
//사용자가 보낸 refreshToken으로 user를 찾고
String userEmail = getSubject(token);
User user = userRepository.findByEmail(userEmail).orElseThrow(NotFoundUserException::new);

//찾은 user로 refreshToken을 가져오고
//찾은 user로 저장되어있는 refreshToken을 가져오고
Token preToken = tokenRepository.findByUser(user).orElseThrow(InvalidTokenException::new);

tokenRepository.delete(preToken);
TokenRes tokenRes = token(user);
Token newToken = Token.builder()
.user(user)
.refreshToken(tokenRes.getRefreshToken())
.build();
tokenRepository.save(newToken);
if(preToken.getRefreshToken().equals(token)){
tokenRepository.delete(preToken);
TokenRes tokenRes = token(user);
Token newToken = Token.builder()
.user(user)
.refreshToken(tokenRes.getRefreshToken())
.build();
tokenRepository.save(newToken);

return tokenRes;
}

return tokenRes;
throw new InvalidTokenException();

}

Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/narae/fliwith/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.narae.fliwith.dto.UserReq.*;
import com.narae.fliwith.dto.UserRes.ProfileRes;
import com.narae.fliwith.dto.base.BaseRes;
import com.narae.fliwith.service.MailService;
import com.narae.fliwith.service.UserService;
import jakarta.servlet.ServletRequest;
import lombok.RequiredArgsConstructor;
Expand All @@ -17,6 +18,7 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
Expand All @@ -25,6 +27,7 @@
public class UserController {
private final UserService userService;


@PostMapping("/signup/email")
public ResponseEntity<BaseRes<Void>> signUp(@RequestBody SignUpReq signUpReq){
userService.signUp(signUpReq);
Expand Down Expand Up @@ -58,5 +61,16 @@ public ResponseEntity<BaseRes<TokenRes>> reissue(@RequestHeader(value = "Refresh
return ResponseEntity.ok(BaseRes.create(HttpStatus.OK.value(), "토큰 재발급에 성공했습니다.", userService.reissue(token, request)));
}

@PostMapping("/logout")
public ResponseEntity<BaseRes<Void>> logout(@AuthenticationPrincipal CustomUser customUser) {
userService.logout(customUser.getEmail());
return ResponseEntity.ok(BaseRes.create(HttpStatus.OK.value(), "로그아웃에 성공했습니다."));
}

@GetMapping("/authemail")
public ResponseEntity<BaseRes<Void>> authEmail(@RequestParam String auth) {
userService.updateSignupStatus(auth);
return ResponseEntity.ok(BaseRes.create(HttpStatus.OK.value(), "이메일 인증에 성공했습니다."));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ public class TourController {

private final TourService tourService;
@GetMapping("/tour")
public ResponseEntity<BaseRes<List<TourType>>> getTourByType(@RequestParam String latitude, @RequestParam String longitude, @RequestParam String contentTypeId){
return ResponseEntity.ok(BaseRes.create(HttpStatus.OK.value(), "관광지 목록 조회에 성공했습니다.", tourService.getTourByType(latitude, longitude, contentTypeId)));
public ResponseEntity<BaseRes<List<TourType>>> getTourByType(@AuthenticationPrincipal CustomUser customUser, @RequestParam String latitude, @RequestParam String longitude, @RequestParam String contentTypeId){
return ResponseEntity.ok(BaseRes.create(HttpStatus.OK.value(), "관광지 목록 조회에 성공했습니다.", tourService.getTourByType(
customUser.getEmail(), latitude, longitude, contentTypeId)));
}

@GetMapping("/tour/{contentTypeId}/{contentId}")
public ResponseEntity<BaseRes<TourDetailRes>> getTour(@PathVariable(value = "contentTypeId")String contentTypeId, @PathVariable(value = "contentId") String contentId){
return ResponseEntity.ok(BaseRes.create(HttpStatus.OK.value(), "관광지 상세 조회에 성공했습니다.", tourService.getTour(contentTypeId, contentId)));
public ResponseEntity<BaseRes<TourDetailRes>> getTour(@AuthenticationPrincipal CustomUser customUser, @PathVariable(value = "contentTypeId")String contentTypeId, @PathVariable(value = "contentId") String contentId){
return ResponseEntity.ok(BaseRes.create(HttpStatus.OK.value(), "관광지 상세 조회에 성공했습니다.", tourService.getTour(
customUser.getEmail(), contentTypeId, contentId)));

}

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/narae/fliwith/domain/SignupStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.narae.fliwith.domain;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum SignupStatus {
COMPLETE, ING, FAIL
}
7 changes: 7 additions & 0 deletions src/main/java/com/narae/fliwith/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ public class User {
private Disability disability;
@Enumerated(EnumType.STRING)
private Role role;
@Enumerated(EnumType.STRING)
private SignupStatus signupStatus;
private String auth;
//TODO: 탈퇴상태 추가

public void completeSignup(){
signupStatus = SignupStatus.COMPLETE;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ public enum UserExceptionList {
DUPLICATE_USER_EMAIL("U0001", HttpStatus.CONFLICT, "이미 가입된 이메일입니다."),
LOGIN_FAIL("U0002", HttpStatus.NOT_FOUND, "로그인에 실패했습니다."),
DUPLICATE_USER_NICKNAME("U0003", HttpStatus.CONFLICT, "이미 존재하는 닉네임입니다."),
NOT_FOUND_USER_ERROR("U0004", HttpStatus.NOT_FOUND, "존재하지 않는 사용자입니다.")
NOT_FOUND_USER_ERROR("U0004", HttpStatus.NOT_FOUND, "존재하지 않는 사용자입니다."),
ALREADY_LOGOUT_ERROR("U0005", HttpStatus.NOT_FOUND, "이미 로그아웃한 사용자입니다."),
EMAIL_SEND_ERROR("U0006", HttpStatus.NOT_FOUND, "이메일 발송에 실패했습니다."),
EMAIL_AUTH_ERROR("U0007", HttpStatus.BAD_REQUEST, "유효하지 않은 인증 링크입니다."),
REQUIRE_EMAIL_AUTH("U0008", HttpStatus.UNAUTHORIZED, "이메일 인증이 필요한 사용자입니다.")
;


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
@RequiredArgsConstructor
public enum SecurityExceptionList {
UNKNOWN_ERROR("S0001", HttpStatus.INTERNAL_SERVER_ERROR, "예상치 못한 오류가 발생했습니다."),
ACCESS_DENIED("S0002", HttpStatus.UNAUTHORIZED, "401 접근이 거부되었습니다."),
ACCESS_DENIED_03("S0003", HttpStatus.UNAUTHORIZED, "403 접근이 거부되었습니다."),
ACCESS_DENIED("S0002", HttpStatus.UNAUTHORIZED, "접근이 거부되었습니다."),
ACCESS_DENIED_03("S0003", HttpStatus.FORBIDDEN, "권한이 없는 사용자가 접근하려 했습니다."),
EXPIRED_TOKEN_ERROR("S0004", HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."),
INVALID_TOKEN_ERROR("S0005", HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다.")
INVALID_TOKEN_ERROR("S0005", HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."),
MALFORMED_TOKEN_ERROR("S0006", HttpStatus.UNAUTHORIZED, "잘못된 토큰 서명입니다."),
UNSUPPORTED_TOKEN_ERROR("S0007", HttpStatus.UNAUTHORIZED, "지원되지 않는 토큰입니다."),
ILLEGAL_TOKEN_ERROR("S0008", HttpStatus.UNAUTHORIZED, "토큰이 잘못되었습니다.")
;
private final String errorCode;
private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.narae.fliwith.exception.user;

import static com.narae.fliwith.exception.constants.UserExceptionList.ALREADY_LOGOUT_ERROR;

public class AlreadyLogoutException extends UserException{
public AlreadyLogoutException(){
super(ALREADY_LOGOUT_ERROR.getErrorCode(),
ALREADY_LOGOUT_ERROR.getHttpStatus(),
ALREADY_LOGOUT_ERROR.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.narae.fliwith.exception.user;

import static com.narae.fliwith.exception.constants.UserExceptionList.EMAIL_AUTH_ERROR;

public class EmailAuthException extends UserException{
public EmailAuthException(){
super(EMAIL_AUTH_ERROR.getErrorCode(),
EMAIL_AUTH_ERROR.getHttpStatus(),
EMAIL_AUTH_ERROR.getMessage());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.narae.fliwith.exception.user;

import static com.narae.fliwith.exception.constants.UserExceptionList.EMAIL_SEND_ERROR;

public class EmailSendException extends UserException{
public EmailSendException(){
super(EMAIL_SEND_ERROR.getErrorCode(),
EMAIL_SEND_ERROR.getHttpStatus(),
EMAIL_SEND_ERROR.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.narae.fliwith.exception.user;

import static com.narae.fliwith.exception.constants.UserExceptionList.REQUIRE_EMAIL_AUTH;

public class RequireEmailAuthException extends UserException {
public RequireEmailAuthException(){
super(REQUIRE_EMAIL_AUTH.getErrorCode(),
REQUIRE_EMAIL_AUTH.getHttpStatus(),
REQUIRE_EMAIL_AUTH.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findById(Long id);

Optional<User> findByEmail(String email);
Optional<User> findByAuth(String uuid);
}
Loading

0 comments on commit 09e5c04

Please sign in to comment.