Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jacocoTestReport {
}

group = 'com.sprint.mission'
version = '2.0-M9'
version = '2.1-M10'

java {
toolchain {
Expand All @@ -48,6 +48,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'de.codecentric:spring-boot-admin-starter-client:3.4.5'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.nimbusds:nimbus-jose-jwt:10.3'
testImplementation 'org.springframework.security:spring-security-test'

compileOnly 'org.projectlombok:lombok'
Expand All @@ -71,6 +72,7 @@ dependencies {
// dotENV
implementation("io.github.cdimascio:java-dotenv:5.2.2")


}

dependencyManagement {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/sprint/mission/DiscodeitApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableJpaAuditing
@EnableScheduling
public class DiscodeitApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,12 @@ private HttpStatus determineHttpStatus(DiscodeitException exception) {
LOGIN_FAIL -> UNAUTHORIZED;

case NOT_AUTHORIZED -> FORBIDDEN;

case INTERNAL_SERVER_ERROR,
URI_CREATE_FAIL,
SAVE_TO_FILE_STORAGE_FAIL,
BINARY_CONTENT_READ_FAIL -> HttpStatus.INTERNAL_SERVER_ERROR;

BINARY_CONTENT_READ_FAIL,
ACCESS_TOKEN_CREATE_FAIL -> HttpStatus.INTERNAL_SERVER_ERROR;
};
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
package com.sprint.mission.discodeit.controller;

import static com.sprint.mission.discodeit.security.jwt.JwtTokenProvider.*;
import static org.springframework.http.HttpStatus.*;

import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.sprint.mission.discodeit.domain.dto.command.GetNewAccTokenCommand;
import com.sprint.mission.discodeit.domain.dto.command.UpdateRoleCommand;
import com.sprint.mission.discodeit.domain.dto.jwt.JwtDto;
import com.sprint.mission.discodeit.domain.dto.request.UserRoleUpdateRequest;
import com.sprint.mission.discodeit.domain.dto.response.JwtResponse;
import com.sprint.mission.discodeit.domain.dto.user.UserDto;
import com.sprint.mission.discodeit.security.DiscodeitUserDetails;
import com.sprint.mission.discodeit.mapper.AuthMapper;
import com.sprint.mission.discodeit.service.AuthService;

import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -32,6 +37,7 @@
public class AuthController {

private final AuthService authService;
private final AuthMapper authMapper;

@GetMapping("/csrf-token")
public ResponseEntity<Void> getCsrfToken(CsrfToken csrfToken, HttpServletResponse response) {
Expand All @@ -47,9 +53,8 @@ public ResponseEntity<Void> getCsrfToken(CsrfToken csrfToken, HttpServletRespons
}

@GetMapping("/me")
private ResponseEntity<UserDto> me(@AuthenticationPrincipal DiscodeitUserDetails userDetails) {
UserDto userDto = userDetails.getUserDto();

private ResponseEntity<UserDto> me(@CookieValue(REFRESH_TOKEN_COOKIE_NAME) String refreshToken) {
UserDto userDto = authService.getProfile(refreshToken);
return ResponseEntity.ok(userDto);
}

Expand All @@ -62,6 +67,19 @@ private ResponseEntity<UserDto> updateRole(@RequestBody UserRoleUpdateRequest re

}

@PostMapping("/refresh")
ResponseEntity<JwtResponse> refresh(
@CookieValue(REFRESH_TOKEN_COOKIE_NAME) String refreshToken,
HttpServletResponse response
) {
GetNewAccTokenCommand command = GetNewAccTokenCommand.from(refreshToken, response);
JwtDto result = authService.getNewAccToken(command);
JwtResponse jwtResponse = authMapper.toResponse(result);

return ResponseEntity.ok(jwtResponse);

}

private ResponseCookie getCsrfCookie(String tokenValue) {

return ResponseCookie.from("XSRF-TOKEN", tokenValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ public ResponseEntity<ChannelResponse> updatePublicChannel(
public ResponseEntity<List<ChannelResponse>> getAllByUserId(
@Parameter(description = "조회할 User ID")
@RequestParam UUID userId) {

List<ChannelDto> channels = channelService.readAllByUserId(userId);

List<ChannelResponse> body = channels.stream().map(channelMapper::toResponse).toList();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.sprint.mission.discodeit.domain.dto.command;

import jakarta.servlet.http.HttpServletResponse;

public record GetNewAccTokenCommand(String refreshToken, HttpServletResponse response) {
public static GetNewAccTokenCommand from(
String refreshToken,
HttpServletResponse response) {

return new GetNewAccTokenCommand(refreshToken, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.sprint.mission.discodeit.domain.dto.jwt;

import com.sprint.mission.discodeit.domain.dto.user.UserDto;

public record JwtDto(UserDto userDto, String accToken) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.sprint.mission.discodeit.domain.dto.response;

import com.sprint.mission.discodeit.domain.dto.user.UserDto;

public record JwtResponse(UserDto userDto, String accToken) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ public enum ErrorCode {
READ_STATUS_DUPLICATE("이미 존재하는 읽음 상태입니다."),

// VALIDATION_ERROR
VALIDATION_ERROR("요청값이 잘못되었습니다.");
VALIDATION_ERROR("요청값이 잘못되었습니다."),

// JWT_ERROR
ACCESS_TOKEN_CREATE_FAIL("Access 토큰 생성에 실패했습니다.");

private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,16 @@ public static ErrorResponse of(ErrorCode code, int status, Exception e) {
status
);
}

public static ErrorResponse of(DiscodeitException e, int status) {
ErrorCode errorCode = e.getErrorCode();
return new ErrorResponse(
Instant.now(),
errorCode.name(),
errorCode.getMessage(),
Collections.emptyMap(),
e.getClass().getSimpleName(),
status
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.sprint.mission.discodeit.exception.auth;

import com.sprint.mission.discodeit.exception.ErrorCode;
import com.sprint.mission.discodeit.exception.user.UserException;

public class NotAuthenticationException extends UserException {

public NotAuthenticationException() {
super(ErrorCode.NOT_AUTHORIZED);
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/sprint/mission/discodeit/mapper/AuthMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.sprint.mission.discodeit.mapper;

import org.mapstruct.Mapper;

import com.sprint.mission.discodeit.domain.dto.binaryContent.BinaryContentDto;
import com.sprint.mission.discodeit.domain.dto.binaryContent.BinaryContentResponse;
import com.sprint.mission.discodeit.domain.dto.jwt.JwtDto;
import com.sprint.mission.discodeit.domain.dto.response.JwtResponse;

@Mapper(componentModel = "spring")
public interface AuthMapper {
JwtResponse toResponse(JwtDto jwtDto);

BinaryContentResponse toResponse(BinaryContentDto dto);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.sprint.mission.discodeit.domain.enums.Role.*;
import static com.sprint.mission.discodeit.exception.ErrorCode.*;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.security.config.http.SessionCreationPolicy.*;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -22,12 +23,17 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.web.cors.CorsConfiguration;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sprint.mission.discodeit.exception.ErrorResponse;
import com.sprint.mission.discodeit.security.jwt.JwtAuthenticationFilter;
import com.sprint.mission.discodeit.security.jwt.JwtLoginSuccessHandler;
import com.sprint.mission.discodeit.security.jwt.JwtLogoutHandler;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -43,43 +49,34 @@ public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(
HttpSecurity http,
LoginSuccessHandler loginSuccessHandler,
JwtLoginSuccessHandler jwtLoginSuccessHandler,
JwtLogoutHandler jwtLogoutHandler,
LoginFailureHandler loginFailureHandler,
DaoAuthenticationProvider daoAuthenticationProvider,
SessionRegistry sessionRegistry
JwtAuthenticationFilter jwtAuthenticationFilter
) throws Exception {

http
.authenticationProvider(daoAuthenticationProvider)
.authorizeHttpRequests(auth -> auth
// permitAll 경로 설정
.requestMatchers("/api/auth/login", "/error", "/", "/index.html").permitAll()
.requestMatchers("/api/auth/csrf-token").permitAll()
.requestMatchers(SecurityWhitelist.WHITE_LIST.toArray(String[]::new)).permitAll()
.requestMatchers(HttpMethod.POST, "/api/users").permitAll() // 회원 가입
.requestMatchers(
"/css/**",
"/js/**",
"/images/**",
"/webjars/**",
"/favicon.ico",
"/swagger-ui/**",
"/assets/**",
"/actuator/**"
).permitAll()
.anyRequest().authenticated()
)
.formLogin(login -> login
.loginProcessingUrl("/api/auth/login")
.successHandler(loginSuccessHandler)
.successHandler(jwtLoginSuccessHandler)
.failureHandler(loginFailureHandler)
)
.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT))
.addLogoutHandler(jwtLogoutHandler)
.logoutSuccessHandler(
new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT))
)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
.ignoringRequestMatchers("/api/auth/logout")

)
.exceptionHandling(ex -> ex
Expand All @@ -98,14 +95,24 @@ public SecurityFilterChain filterChain(
})

)
.sessionManagement(session -> session
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry)
.sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
)
// .rememberMe(Customizer.withDefaults())
;

// 8) cors 설정 추가
http.cors(cors -> cors.configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*"); // 모든 Origin 허용
config.addAllowedHeader("*"); // 모든 Header 허용
config.addAllowedMethod("*"); // 모든 Method 허용
config.setAllowCredentials(true); // 쿠키/인증정보 허용 (필요할 때만)
return config;
}));

return http.build();
}

Expand Down
Loading