Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public abstract class AbstractOAuth2LoginSuccessHandler implements Authenticatio
private final UserRepository userRepository;

protected abstract String getEmail(OAuth2User oAuth2User);

protected abstract Long getUserId(OAuth2User oAuth2User);

protected abstract Role getRole(OAuth2User oAuth2User);

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ public class AuthService {
private final JwtUtil jwtUtil;
private final PasswordEncoder passwordEncoder;

public LoginResponse login(LoginRequest loginRequest){
public LoginResponse login(LoginRequest loginRequest) {
User user = userRepository.findByEmail(loginRequest.email())
.filter(m -> passwordEncoder.matches(loginRequest.password(),m.getPassword().getValue()))
.orElseThrow(()-> new CustomException(ErrorCode.USER_NOT_MATCH_LOGIN_INFO));
.filter(m -> passwordEncoder.matches(loginRequest.password(), m.getPassword().getValue()))
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_MATCH_LOGIN_INFO));

String accessToken = jwtUtil.createAccessToken(user.getId(), user.getEmail(), user.getRole());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
public class PasswordConfig {

@Bean
public BCryptPasswordEncoder getPasswordEncoder(){
public BCryptPasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
99 changes: 57 additions & 42 deletions src/main/java/goorm/back/zo6/auth/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
import goorm.back.zo6.auth.util.JwtUtil;
import goorm.back.zo6.user.application.OAuth2UserServiceFactory;
import goorm.back.zo6.user.domain.Role;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.SecurityFilterChain;
Expand All @@ -29,7 +28,6 @@

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {

Expand All @@ -40,63 +38,80 @@ public class SecurityConfig {


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
//cors 설정
http.cors((cors -> cors.configurationSource(configurationSource())));
// csfr disable
http.csrf((auth) -> auth.disable());
// form 로그인 disable
http.formLogin((auth) -> auth.disable());
// HTTP Basic 인증 방식 disable
http.httpBasic((auth) -> auth.disable());
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// CORS 설정 (프론트엔드 도메인 허용 필요 시 커스터마이징)
http.cors(cors -> cors.configurationSource(configurationSource()));

//경로별 인가 작업
http.authorizeHttpRequests((auth) -> auth
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**","/actuator/**").permitAll() // Swagger 관련 경로 허용
.requestMatchers("/api/v1/users/signup","/api/v1/auth/login","/api/v1/users/signup-link","/api/v1/users/check-email").permitAll()
.requestMatchers("/api/v1/users/code","/api/v1/users/verify").permitAll()
.requestMatchers("/api/v1/rekognition/authentication").permitAll()
// CSRF, Form 로그인, HTTP Basic 인증 비활성화 (JWT 기반 인증 사용)
http.csrf(AbstractHttpConfigurer::disable);
http.formLogin(AbstractHttpConfigurer::disable);
http.httpBasic(AbstractHttpConfigurer::disable);

// URL 경로별 접근 제어 설정
http.authorizeHttpRequests(auth -> auth
// Swagger 및 시스템 모니터링 경로 - 문서 확인 및 상태 체크용
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", "/actuator/**").permitAll()
// 사용자 인증 관련 공개 API - 회원가입, 로그인, 이메일 인증 등
.requestMatchers(
"/api/v1/users/signup", "/api/v1/auth/login",
"/api/v1/users/signup-link", "/api/v1/users/check-email",
"/api/v1/users/code", "/api/v1/users/verify"
).permitAll()
// 얼굴 인식 및 Rekognition 인증 관련
.requestMatchers(
"/api/v1/rekognition/authentication",
"/api/v1/face/authentication",
"/api/v1/face/collection"
).permitAll()
// 회의 정보 및 세션 조회 (비로그인 사용자도 접근 가능)
.requestMatchers(
"/conferences", "/sessions",
"/api/v1/conference/**", "/api/v1/conferences/**",
"/api/v1/conferences/image/**"
).permitAll()
// 예약 임시 저장 (비회원도 접근 허용 대상이라면)
.requestMatchers("/api/v1/reservation/temp").permitAll()
.requestMatchers("/conferences").permitAll()
.requestMatchers("/sessions").permitAll()
.requestMatchers("/api/v1/conference/**").permitAll()
.requestMatchers("/api/v1/conferences/**").permitAll()
.requestMatchers("/api/v1/face/authentication").permitAll()
.requestMatchers("/api/v1/conferences/image/**").permitAll()
.requestMatchers("/api/v1/face/authentication","/api/v1/face/collection").permitAll()
// Redis 테스트용 또는 캐시 데이터 확인용 엔드포인트
.requestMatchers("/api/v1/redis").permitAll()
// SSE 실시간 구독 관련 - 로그인 없이도 알림 구독 가능
.requestMatchers("/api/v1/sse/subscribe", "/api/v1/sse/unsubscribe", "/api/v1/sse/last-count").permitAll()
// 관리자 회원가입 (최초 관리자 등록 목적)
.requestMatchers("/api/v1/admin/signup").permitAll()
.requestMatchers("/api/v1/admin/conference/**").hasRole(Role.ADMIN.getRoleName())
.requestMatchers(HttpMethod.GET,"/api/v1/notices/**").permitAll()
.requestMatchers("/api/v1/notices/**").hasRole(Role.ADMIN.getRoleName())
// 관리자 전용 회의 제어 API
.requestMatchers("/api/v1/admin/conference/**").hasAuthority(Role.ADMIN.getRoleName())
// 알림 조회는 누구나 가능
.requestMatchers(HttpMethod.GET, "/api/v1/notices/**").permitAll()
// 알림 등록/수정/삭제는 관리자만 가능
.requestMatchers("/api/v1/notices/**").hasAuthority(Role.ADMIN.getRoleName())
// OAuth2 로그인 (카카오 등)
.requestMatchers("/oauth2/**", "/auth/login/kakao/**").permitAll()
.anyRequest().authenticated());
// 그 외 모든 요청은 인증 필요
.anyRequest().authenticated()
);

//세션 설정 : STATELESS
http.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// 세션 비활성화 - JWT 기반이므로 STATELESS로 설정
http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

// JWTFilter 추가
// JWT 인증 필터 등록 (UsernamePasswordAuthenticationFilter 이전에 실행)
http.addFilterBefore(new JwtAuthFilter(jwtUtil, objectMapper), UsernamePasswordAuthenticationFilter.class);

// Exception handler 추가
http.exceptionHandling(exceptionHandling ->
exceptionHandling
.accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper))
.authenticationEntryPoint(new CustomAuthenticationEntryPoint(objectMapper)));
// 인증/인가 실패 시 처리 핸들러 등록
http.exceptionHandling(exception -> exception
.accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper))
.authenticationEntryPoint(new CustomAuthenticationEntryPoint(objectMapper))
);

// OAuth2 로그인 설정
// OAuth2 로그인 설정 - provider 별 후처리 핸들러 지정
http.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserServiceFactory::loadUser))
.successHandler((request, response, authentication) -> {
OAuth2AuthenticationToken oAuth2Token = (OAuth2AuthenticationToken) authentication;
String provider = oAuth2Token.getAuthorizedClientRegistrationId();

OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
String provider = token.getAuthorizedClientRegistrationId();
AuthenticationSuccessHandler handler = successHandlerFactory.getHandler(provider);
handler.onAuthenticationSuccess(request, response, authentication);
})
);

return http.build();
}

Expand Down
5 changes: 4 additions & 1 deletion src/main/java/goorm/back/zo6/auth/domain/LoginUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

public record LoginUser(Long id, String email, String role) implements UserDetails {

public Long getId(){return id; }
public Long getId() {
return id;
}

@Override
public String getUsername() {
return email;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ public record LoginRequest(
@Schema(description = "비밀번호", example = "12345")
@NotBlank(message = "비밀번호를 입력해 주세요.")
String password
){}
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
@RequiredArgsConstructor
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;

@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
Expand Down
16 changes: 8 additions & 8 deletions src/main/java/goorm/back/zo6/auth/filter/JwtAuthFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
setSecuritySession(loginUser);
filterChain.doFilter(request, response);

}catch (CustomException e){
ErrorCode errorCode = e.getErrorCode();
} catch (CustomException e) {
ErrorCode errorCode = e.getErrorCode();
switch (errorCode) {
case WRONG_TYPE_TOKEN, UNSUPPORTED_TOKEN, EXPIRED_TOKEN, UNKNOWN_TOKEN_ERROR ->
setResponse(response, errorCode);
Expand All @@ -65,30 +65,30 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
}
}

private static void setSecuritySession(LoginUser loginUser){
private static void setSecuritySession(LoginUser loginUser) {
log.info("SessionLoginUser : {}", loginUser.getUsername());

Collection<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(loginUser.getRole().getRoleSecurity()));

Authentication authToken = new UsernamePasswordAuthenticationToken(loginUser,null, authorities);
Authentication authToken = new UsernamePasswordAuthenticationToken(loginUser, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authToken);
}

private LoginUser getUser(String token){
private LoginUser getUser(String token) {
Long userId = jwtUtil.getUserId(token);
String email = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);

return new LoginUser(userId, email, role);
}

private boolean verifyToken(HttpServletRequest request,String token){
private boolean verifyToken(HttpServletRequest request, String token) {
Boolean isValid = (Boolean) request.getAttribute("isTokenValid");
if(isValid != null) return isValid;
if (isValid != null) return isValid;

if (token == null || !jwtUtil.validateToken(token)) {
log.debug("token null or not validate");
request.setAttribute("isTokenValid",false);
request.setAttribute("isTokenValid", false);
return false;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public ResponseEntity<ResponseDto<LoginResponse>> login(@Validated @RequestBody

@DeleteMapping("/logout")
@Operation(summary = "로그아웃", description = "api 요청 시 쿠키를 강제 만료시켜 로그아웃합니다.")
public ResponseEntity<ResponseDto> logout(){
public ResponseEntity<ResponseDto> logout() {
ResponseCookie cookie = CookieUtil.deleteCookie(COOKIE_NAME);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/goorm/back/zo6/auth/util/CookieUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static ResponseCookie createCookie(String name, String value, long cookie
.build();
}

public static ResponseCookie deleteCookie(String name){
public static ResponseCookie deleteCookie(String name) {
return ResponseCookie.from(name, null)
.maxAge(0)
.path("/")
Expand All @@ -26,16 +26,16 @@ public static ResponseCookie deleteCookie(String name){
.build();
}

public static String findToken(HttpServletRequest request){
public static String findToken(HttpServletRequest request) {
String token = null;
Cookie[] cookies = request.getCookies();

if(cookies == null){
if (cookies == null) {
return null;
}

for(Cookie cookie : cookies){
if(cookie.getName().equals("Authorization")){
for (Cookie cookie : cookies) {
if (cookie.getName().equals("Authorization")) {
token = cookie.getValue();
}
}
Expand Down
24 changes: 13 additions & 11 deletions src/main/java/goorm/back/zo6/auth/util/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@ public class JwtUtil {
@Value("${jwt.valid-time}")
private long TOKEN_VALID_TIME;

public JwtUtil(@Value("${jwt.secret}") String secret){
public JwtUtil(@Value("${jwt.secret}") String secret) {
secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}

public String createAccessToken(Long userId, String email, Role role){
public String createAccessToken(Long userId, String email, Role role) {
Date timeNow = new Date(System.currentTimeMillis());
Date expirationTime = new Date(timeNow.getTime() + TOKEN_VALID_TIME);

return Jwts.builder()
.claim("userId", userId)
.claim("email",email)
.claim("role",role.getRoleSecurity())
.claim("email", email)
.claim("role", role.getRoleSecurity())
.setIssuedAt(timeNow)
.setExpiration(expirationTime)
.signWith(secretKey, SignatureAlgorithm.HS256)
Expand All @@ -43,35 +43,37 @@ public Long getUserId(String token) {
return Jwts.parserBuilder().setSigningKey(secretKey).build()
.parseClaimsJws(token).getBody().get("userId", Long.class);
}

public String getUsername(String token) {
return Jwts.parserBuilder().setSigningKey(secretKey).build()
.parseClaimsJws(token).getBody().get("email", String.class);
}

public String getRole(String token) {
return Jwts.parserBuilder().setSigningKey(secretKey).build()
.parseClaimsJws(token).getBody().get("role", String.class);
}

public boolean validateToken(String token){
public boolean validateToken(String token) {
//log.info("토큰 유효성 검증 시작");
return valid(secretKey, token);
}

private boolean valid(SecretKey secretKey, String token){
private boolean valid(SecretKey secretKey, String token) {
if (token == null) {
throw new CustomException(ErrorCode.MISSING_TOKEN);
}
try{
try {
Jws<Claims> claims = Jwts.parserBuilder().setSigningKey(secretKey).build()
.parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
}catch (SignatureException ex){
} catch (SignatureException ex) {
throw new CustomException(ErrorCode.WRONG_TYPE_TOKEN);
}catch (MalformedJwtException ex){
} catch (MalformedJwtException ex) {
throw new CustomException(ErrorCode.UNSUPPORTED_TOKEN);
}catch (ExpiredJwtException ex){
} catch (ExpiredJwtException ex) {
throw new CustomException(ErrorCode.EXPIRED_TOKEN);
}catch (IllegalArgumentException ex){
} catch (IllegalArgumentException ex) {
throw new CustomException(ErrorCode.UNKNOWN_TOKEN_ERROR);
}
}
Expand Down