Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

시큐리티 인증 URL 추가 및 에러 처리 추가 #199

Merged
merged 13 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion src/main/java/balancetalk/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -25,6 +26,26 @@ public class SecurityConfig {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

private static final String[] PUBLIC_GET = {
// h2 database
"/h2-console/**",
// swagger
"/swagger-ui/**", "/v3/api-docs/**",
"/members/duplicate",
"/posts", "/posts/{postId}", "/posts/{postId}/vote", "/posts/{postId}/comments",
"/notices", "/notices/{noticeId}"
};

private static final String[] PUBLIC_POST = {
"/members/join", "/members/login",
"/email/request", "/email/verify",
"/posts/{postId}/vote"
};

private static final String[] PUBLIC_PUT = {
"/posts/{postId}/vote"
};

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
Expand All @@ -48,7 +69,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// 세션 사용 X (jwt 사용)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(request -> request
.requestMatchers("/**", "/h2-console/**").permitAll()
.requestMatchers(HttpMethod.GET, PUBLIC_GET).permitAll()
.requestMatchers(HttpMethod.POST, PUBLIC_POST).permitAll()
.requestMatchers(HttpMethod.PUT, PUBLIC_PUT).permitAll()
.anyRequest().authenticated()
)
// jwtFilter 먼저 적용
Expand Down
19 changes: 13 additions & 6 deletions src/main/java/balancetalk/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,24 @@ public enum ErrorCode {
MIME_TYPE_NULL(BAD_REQUEST, "MIME 타입이 null입니다."),
FILE_UPLOAD_FAILED(BAD_REQUEST, "파일 업로드에 실패했습니다."),
FILE_SIZE_EXCEEDED(BAD_REQUEST, "파일 크기가 초과되었습니다."),
EXPIRED_JWT_TOKEN(BAD_REQUEST, "만료된 토큰 입니다."),
INVALID_JWT_TOKEN(BAD_REQUEST, "유효하지 않은 토큰입니다."),
EXCEED_MAX_DEPTH(BAD_REQUEST, "답글에 답글을 달 수 없습니다."),
INVALID_REFRESH_TOKEN(BAD_REQUEST, "유효하지 않은 리프레시 토큰입니다."),
PAGE_NUMBER_ZERO(BAD_REQUEST, "페이지 번호는 0보다 커야합니다."),
PAGE_SIZE_ZERO(BAD_REQUEST, "페이지 사이즈는 0보다 커야합니다."),
EXCEED_VALIDATION_LENGTH(BAD_REQUEST, "입력값이 제약 조건에 맞지 않습니다."),
EMPTY_JWT_TOKEN(BAD_REQUEST, "토큰 값이 존재하지 않습니다."),


// 401
MISMATCHED_EMAIL_OR_PASSWORD(UNAUTHORIZED, "이메일 또는 비밀번호가 잘못되었습니다."),
AUTHENTICATION_ERROR(UNAUTHORIZED, "인증 오류가 발생했습니다."),
BAD_CREDENTIAL_ERROR(UNAUTHORIZED, "로그인에 실패했습니다."),
UNAUTHORIZED_LOGOUT(UNAUTHORIZED, "로그아웃을 위해서는 인증이 필요합니다."),
AUTHENTICATION_REQUIRED(UNAUTHORIZED, "인증이 필요합니다."),
UNAUTHORIZED_CREATE_NOTICE(UNAUTHORIZED, "공지사항 작성 권한이 없습니다."),
VERIFY_CODE_MISMATCH(UNAUTHORIZED, "인증 번호가 일치하지 않습니다."),
EXPIRED_JWT_TOKEN(UNAUTHORIZED, "만료된 토큰 입니다."),
INVALID_JWT_TOKEN(UNAUTHORIZED, "유효하지 않은 토큰입니다"),


// 403
FORBIDDEN_POST_DELETE(FORBIDDEN, "해당 게시글은 삭제 권한이 없습니다."),
Expand All @@ -58,17 +62,20 @@ public enum ErrorCode {
NOT_FOUND_COMMENT_AT_THAT_POST(NOT_FOUND, "해당 게시글에 존재하지 않는 댓글입니다."),
NOT_FOUND_NOTICE(NOT_FOUND, "존재하지 않는 공지사항입니다."),


// 409
ALREADY_VOTE(CONFLICT, "이미 투표한 게시글입니다."),
ALREADY_LIKE_COMMENT(CONFLICT, "이미 추천을 누른 댓글입니다."),
ALREADY_LIKE_POST(CONFLICT, "이미 추천을 누른 게시글입니다."),
ALREADY_CANCEL_LIKE_POST(CONFLICT, "이미 추천 취소를 누른 게시글입니다"),
ALREADY_REGISTERED_NICKNAME(CONFLICT, "이미 등록된 닉네임입니다."),
ALREADY_REGISTERED_EMAIL(CONFLICT, "이미 존재하는 이메일 입니다. 다른 이메일을 입력해주세요."),
SAME_NICKNAME(CONFLICT, "변경하려는 닉네임이 현재와 동일합니다. 다른 닉네임을 입력해주세요."),
SAME_PASSWORD(CONFLICT, "변경하려는 비밀번호가 현재와 동일합니다. 다른 비밀번호를 입력해주세요."),


// 500
REDIS_CONNECTION_FAIL(INTERNAL_SERVER_ERROR, "Redis 연결에 실패했습니다."),
DUPLICATE_EMAIL(INTERNAL_SERVER_ERROR, "이미 존재하는 이메일 입니다. 다른 이메일을 입력해주세요"),
AUTHORIZATION_CODE_MISMATCH(INTERNAL_SERVER_ERROR, "인증 번호가 일치하지 않습니다.");
REDIS_CONNECTION_FAIL(INTERNAL_SERVER_ERROR, "Redis 연결에 실패했습니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package balancetalk.global.jwt;

import balancetalk.global.exception.ErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
Expand All @@ -8,11 +10,24 @@
import org.springframework.stereotype.Component;

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

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");

ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> jsonMessage = new HashMap<>();

jsonMessage.put("httpStatus", "UNAUTHORIZED");
jsonMessage.put("message", ErrorCode.AUTHENTICATION_REQUIRED.getMessage());
String result = objectMapper.writeValueAsString(jsonMessage);

response.getWriter().write(result);
}
}
14 changes: 3 additions & 11 deletions src/main/java/balancetalk/global/jwt/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
package balancetalk.global.jwt;

import balancetalk.global.exception.BalanceTalkException;
import balancetalk.global.exception.ErrorCode;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import java.io.IOException;

@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
Expand All @@ -24,18 +21,13 @@ public class JwtAuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);

try {
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (RedisConnectionFailureException e) {
SecurityContextHolder.clearContext();
throw new BalanceTalkException(ErrorCode.REDIS_CONNECTION_FAIL);
} catch (ExpiredJwtException e) {
log.error(e.getMessage());
throw new BalanceTalkException(ErrorCode.EXPIRED_JWT_TOKEN);
} catch (Exception e) {
request.setAttribute("exception" , e.getMessage());
}
chain.doFilter(request, response);
}
Expand Down
34 changes: 10 additions & 24 deletions src/main/java/balancetalk/global/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import balancetalk.global.redis.application.RedisService;
import balancetalk.module.member.dto.TokenDto;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.SignatureException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -78,52 +79,37 @@ public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userPrincipal);

return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());

}

// http 헤더로부터 bearer 토큰 가져옴
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7); // 실제 토큰만 추출
return bearerToken.substring(7);
}
return null;
}


public String getPayload(String token) {
return tokenToJws(token).getBody().getSubject();
}

private Jws<Claims> tokenToJws(final String token) {
try {
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
} catch (final IllegalArgumentException | MalformedJwtException e) {
throw new IllegalArgumentException("Token이 null이거나 Token 파싱 오류");
} catch (final SignatureException e) {
throw new IllegalArgumentException("토큰의 시크릿 키가 일치하지 않습니다.");
} catch (final ExpiredJwtException e) {
throw new IllegalArgumentException("만료된 토큰 입니다.");
}
validateToken(token);
return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody().getSubject();
}

// 토큰 유효성, 만료일자 확인
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
log.error(e.getMessage());
throw new IllegalArgumentException("토큰 만료");
} catch (JwtException e) {
log.error(e.getMessage());
throw new IllegalArgumentException("유효하지 않은 JWT");
throw new BalanceTalkException(ErrorCode.EXPIRED_JWT_TOKEN);
} catch (IllegalArgumentException | MalformedJwtException e) {
throw new BalanceTalkException(ErrorCode.EMPTY_JWT_TOKEN);
} catch (SignatureException e) {
throw new BalanceTalkException(ErrorCode.INVALID_JWT_TOKEN);
}
}

private void validateAuthentication(Authentication authentication) {
if (authentication == null) {
throw new IllegalArgumentException("유저 정보가 존재하지 않습니다.");
throw new BalanceTalkException(ErrorCode.NOT_FOUND_MEMBER);
}
}

Expand Down
6 changes: 0 additions & 6 deletions src/main/java/balancetalk/global/jwt/JwtUtils.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ public void verifyCode(EmailVerification request) {
String redisValue = redisService.getValues(request.getEmail());
Optional.ofNullable(redisValue)
.filter(code -> code.equals(request.getVerificationCode()))
.orElseThrow(() -> new BalanceTalkException(ErrorCode.AUTHORIZATION_CODE_MISMATCH));
.orElseThrow(() -> new BalanceTalkException(ErrorCode.VERIFY_CODE_MISMATCH));
}

private void validateEmail(String email) {
Optional<Member> member = memberRepository.findByEmail(email);
if (member.isPresent()) {
throw new BalanceTalkException(ErrorCode.DUPLICATE_EMAIL);
throw new BalanceTalkException(ErrorCode.ALREADY_REGISTERED_EMAIL);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class CommentResponse {
private Long selectedOptionId;

@Schema(description = "댓글 추천 수", example = "24")
private int likeCount;
private int likesCount;

@Schema(description = "댓글 생성 날짜")
private LocalDateTime createdAt;
Expand All @@ -44,7 +44,7 @@ public static CommentResponse fromEntity(Comment comment, Long balanceOptionId)
.memberName(comment.getMember().getNickname())
.postId(comment.getPost().getId())
.selectedOptionId(balanceOptionId)
.likeCount(comment.getLikes().size())
.likesCount(comment.getLikes().size())
.createdAt(comment.getCreatedAt())
.lastModifiedAt(comment.getLastModifiedAt())
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ public class MemberService {
@Transactional
public Long join(final JoinRequest joinRequest) {
joinRequest.setPassword(passwordEncoder.encode(joinRequest.getPassword()));
if (memberRepository.existsByNickname(joinRequest.getNickname())) {
throw new BalanceTalkException(ErrorCode.ALREADY_REGISTERED_NICKNAME);
}
if (memberRepository.existsByEmail(joinRequest.getEmail())) {
throw new BalanceTalkException(ErrorCode.ALREADY_REGISTERED_EMAIL);
}
Member member = joinRequest.toEntity();
return memberRepository.save(member).getId();
}
Expand Down Expand Up @@ -73,12 +79,18 @@ public List<MemberResponse> findAll() {
@Transactional
public void updateNickname(final String newNickname, HttpServletRequest request) {
Member member = extractMember(request);
if (member.getNickname().equals(newNickname)) {
throw new BalanceTalkException(ErrorCode.SAME_NICKNAME);
}
member.updateNickname(newNickname);
}

@Transactional
public void updatePassword(final String newPassword, HttpServletRequest request) {
Member member = extractMember(request);
if (passwordEncoder.matches(newPassword, member.getPassword())){
throw new BalanceTalkException(ErrorCode.SAME_PASSWORD);
}
member.updatePassword(passwordEncoder.encode(newPassword));
}

Expand All @@ -97,14 +109,9 @@ public void delete(final LoginRequest loginRequest, HttpServletRequest request)

@Transactional
public void logout(){
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails) principal).getUsername();
if (redisService.getValues(username) == null) {
throw new BalanceTalkException(ErrorCode.UNAUTHORIZED_LOGOUT);
}
redisService.deleteValues(username);
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
redisService.deleteValues(username);
}

public void verifyNickname(String nickname) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String username);
boolean existsByNickname(String nickname);
boolean existsByEmail(String email);
void deleteByEmail(String email);
}
3 changes: 3 additions & 0 deletions src/main/java/balancetalk/module/post/dto/PostResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ public class PostResponse {
@Schema(description = "게시글 카테고리", example = "CASUAL")
private PostCategory category;

@Schema(description = "선택지 옵션 리스트", example = "[{\"title\": \"선택지 제목1\", \"description\": \"선택지 내용1\" , \"storedFileName\": null}," +
"{\"title\": \"선택지 제목2\", \"description\": \"선택지 내용2\", \"storedFileName\": null}]")
private List<BalanceOptionDto> balanceOptions;

@Schema(description = "태그 리스트", example = "[\"태그1\", \"태그2\", \"태그3\"]")
private List<PostTagDto> postTags;

@JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package balancetalk.module.vote.application;

import static balancetalk.global.exception.ErrorCode.*;
import static balancetalk.global.utils.SecurityUtils.*;

import balancetalk.global.exception.BalanceTalkException;
import balancetalk.global.utils.SecurityUtils;
import balancetalk.module.member.domain.Member;
import balancetalk.module.member.domain.MemberRepository;
import balancetalk.module.post.domain.BalanceOption;
Expand Down Expand Up @@ -59,7 +59,7 @@ private BalanceOption getBalanceOption(VoteRequest voteRequest) {
}

private Vote voteForMember(VoteRequest voteRequest, Post post, BalanceOption balanceOption) {
Member member = SecurityUtils.getCurrentMember(memberRepository);
Member member = getCurrentMember(memberRepository);

if (member.hasVoted(post)) {
throw new BalanceTalkException(ALREADY_VOTE);
Expand Down Expand Up @@ -96,7 +96,7 @@ public Vote updateVote(Long postId, VoteRequest voteRequest) {
throw new BalanceTalkException(UNMODIFIABLE_VOTE);
}
BalanceOption newSelectedOption = getBalanceOption(voteRequest);
Member member = SecurityUtils.getCurrentMember(memberRepository);
Member member = getCurrentMember(memberRepository);
Vote participatedVote = getParticipatedVote(post, member);

return participatedVote.changeBalanceOption(newSelectedOption);
Expand Down
Loading