diff --git a/build.gradle b/build.gradle index 9f942cc..e2d275b 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,15 @@ dependencies { testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'com.h2database:h2' + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + // Jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'org.springframework.boot:spring-boot-configuration-processor' + // QueryDSL : OpenFeign implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0" implementation "io.github.openfeign.querydsl:querydsl-core:7.0" diff --git a/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java b/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java index a605017..c0ca18c 100644 --- a/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java +++ b/src/main/java/com/example/umc9th/domain/member/controller/MemberController.java @@ -1,14 +1,22 @@ package com.example.umc9th.domain.member.controller; + import com.example.umc9th.domain.member.dto.req.MemberReqDTO; import com.example.umc9th.domain.member.dto.res.MemberResDTO; import com.example.umc9th.domain.member.exception.code.MemberSuccessCode; import com.example.umc9th.domain.member.service.command.MemberCommandService; +import com.example.umc9th.domain.member.service.query.MemberQueryService; import com.example.umc9th.global.apiPayload.ApiResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.RequestBody; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController @@ -16,6 +24,7 @@ public class MemberController { private final MemberCommandService memberCommandService; + private final MemberQueryService memberQueryService; // 회원가입 @PostMapping("/sign-up") @@ -24,4 +33,28 @@ public ApiResponse signUp( ){ return ApiResponse.onSuccess(MemberSuccessCode.FOUND, memberCommandService.signup(dto)); } + + // 로그인 + @PostMapping("/login") + public ApiResponse login( + @RequestBody @Valid MemberReqDTO.LoginDTO dto + ){ + return ApiResponse.onSuccess(MemberSuccessCode.FOUND, memberQueryService.login(dto)); + } + + @PostMapping("/logout") + public ApiResponse logout(HttpServletRequest request, HttpServletResponse response) { + HttpSession session = request.getSession(false); + if (session != null) session.invalidate(); + + SecurityContextHolder.clearContext(); + + ResponseCookie cookie = ResponseCookie.from("JSESSIONID", "") + .path("/") + .maxAge(0) + .build(); + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); + + return ApiResponse.onSuccess(MemberSuccessCode.FOUND, "로그아웃 완료"); + } } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java b/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java index 58490bf..8a93549 100644 --- a/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java +++ b/src/main/java/com/example/umc9th/domain/member/converter/MemberConverter.java @@ -3,6 +3,7 @@ import com.example.umc9th.domain.member.dto.req.MemberReqDTO; import com.example.umc9th.domain.member.dto.res.MemberResDTO; import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.global.auth.enums.Role; public class MemberConverter { @@ -18,14 +19,30 @@ public static MemberResDTO.JoinDTO toJoinDTO( // DTO -> Entity public static Member toMember( - MemberReqDTO.JoinDTO dto + MemberReqDTO.JoinDTO dto, + String password, + Role role ){ return Member.builder() .name(dto.name()) + .email(dto.email()) // 추가된 코드 + .password(password) // 추가된 코드 + .role(role) // 추가된 코드 .birth(dto.birth()) .address(dto.address()) + .phoneNum(dto.phoneNum()) .detailAddress(dto.specAddress()) .gender(dto.gender()) .build(); } + + + // Entity -> DTO (로그인 응답) 추가 + public static MemberResDTO.LoginDTO toLoginDTO(Member member, String accessToken) { + return MemberResDTO.LoginDTO.builder() + .memberId(member.getId()) + .accessToken(accessToken) + .build(); + } + } \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/member/dto/req/MemberReqDTO.java b/src/main/java/com/example/umc9th/domain/member/dto/req/MemberReqDTO.java index bba0e70..b27bf2e 100644 --- a/src/main/java/com/example/umc9th/domain/member/dto/req/MemberReqDTO.java +++ b/src/main/java/com/example/umc9th/domain/member/dto/req/MemberReqDTO.java @@ -2,6 +2,7 @@ import com.example.umc9th.domain.member.enums.Gender; import com.example.umc9th.global.annotation.ExistFoods; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -9,9 +10,15 @@ import java.util.List; public class MemberReqDTO { + + // 회원가입 public record JoinDTO( @NotBlank String name, + @Email + String email, // 추가된 속성 + @NotBlank + String password, // 추가된 속성 @NotNull Gender gender, @NotNull @@ -20,7 +27,16 @@ public record JoinDTO( String address, @NotNull String specAddress, + @NotBlank String phoneNum, @ExistFoods List preferCategory ){} + + // 로그인 + public record LoginDTO( + @NotBlank + String email, + @NotBlank + String password + ){} } diff --git a/src/main/java/com/example/umc9th/domain/member/dto/res/MemberResDTO.java b/src/main/java/com/example/umc9th/domain/member/dto/res/MemberResDTO.java index fd5279a..4012ef9 100644 --- a/src/main/java/com/example/umc9th/domain/member/dto/res/MemberResDTO.java +++ b/src/main/java/com/example/umc9th/domain/member/dto/res/MemberResDTO.java @@ -5,9 +5,17 @@ import java.time.LocalDateTime; public class MemberResDTO { + // 회원가입 @Builder public record JoinDTO( Long memberId, LocalDateTime createAt ){} + + // 로그인 + @Builder + public record LoginDTO( + Long memberId, + String accessToken + ){} } diff --git a/src/main/java/com/example/umc9th/domain/member/entity/Member.java b/src/main/java/com/example/umc9th/domain/member/entity/Member.java index 246f155..0db8a8a 100644 --- a/src/main/java/com/example/umc9th/domain/member/entity/Member.java +++ b/src/main/java/com/example/umc9th/domain/member/entity/Member.java @@ -6,6 +6,7 @@ import com.example.umc9th.domain.member.enums.Gender; import com.example.umc9th.domain.member.enums.PhoneVerificationStatus; import com.example.umc9th.domain.member.enums.SocialType; +import com.example.umc9th.global.auth.enums.Role; import com.example.umc9th.global.entity.BaseEntity; import jakarta.persistence.*; import lombok.*; @@ -54,9 +55,15 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) private SocialType socialType; - @Column(name = "email", length = 50) + @Column(name = "email", unique = true, length = 50) private String email; + @Column(nullable = false) + private String password; + + @Enumerated(EnumType.STRING) + private Role role; + @Column(name = "phone_verification_status", nullable = false) @Enumerated(EnumType.STRING) @Builder.Default diff --git a/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java b/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java index 64645ae..17e7210 100644 --- a/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java +++ b/src/main/java/com/example/umc9th/domain/member/exception/code/MemberErrorCode.java @@ -12,7 +12,10 @@ public enum MemberErrorCode implements BaseErrorCode { NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "해당 사용자를 찾지 못했습니다."), - ; + + INVALID(HttpStatus.BAD_REQUEST, + "MEMBER400_1", + "유효하지 않은 사용자 요청입니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java b/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java index dd716aa..1c1d7a6 100644 --- a/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/example/umc9th/domain/member/repository/MemberRepository.java @@ -10,4 +10,6 @@ public interface MemberRepository extends JpaRepository { // 활성 상태인 회원 단건 조회 Optional findByIdAndIsActiveTrue(Long id); + Optional findByEmail(String email); + } diff --git a/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandServiceImpl.java b/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandServiceImpl.java index d92605d..016e4e0 100644 --- a/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandServiceImpl.java +++ b/src/main/java/com/example/umc9th/domain/member/service/command/MemberCommandServiceImpl.java @@ -11,8 +11,10 @@ import com.example.umc9th.domain.member.repository.FoodRepository; import com.example.umc9th.domain.member.repository.MemberFoodRepository; import com.example.umc9th.domain.member.repository.MemberRepository; +import com.example.umc9th.global.auth.enums.Role; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -27,19 +29,26 @@ public class MemberCommandServiceImpl implements MemberCommandService{ private final MemberFoodRepository memberFoodRepository; private final FoodRepository foodRepository; + // Password Encoder + private final PasswordEncoder passwordEncoder; + // 회원가입 @Override @Transactional public MemberResDTO.JoinDTO signup( MemberReqDTO.JoinDTO dto ){ + + // 솔트된 비밀번호 생성 + String salt = passwordEncoder.encode(dto.password()); + // 사용자 생성 - Member member = MemberConverter.toMember(dto); + Member member = MemberConverter.toMember(dto, salt, Role.ROLE_USER); // DB 적용 memberRepository.save(member); // 선호 음식 존재 여부 확인 - if (dto.preferCategory().size() > 1){ + if (dto.preferCategory() != null && !dto.preferCategory().isEmpty()) { List memberFoodList = new ArrayList<>(); // 선호 음식 ID별 조회 diff --git a/src/main/java/com/example/umc9th/domain/member/service/query/MemberQuerySercvice.java b/src/main/java/com/example/umc9th/domain/member/service/query/MemberQuerySercvice.java deleted file mode 100644 index f3a3e44..0000000 --- a/src/main/java/com/example/umc9th/domain/member/service/query/MemberQuerySercvice.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.umc9th.domain.member.service.query; - -public interface MemberQuerySercvice { -} diff --git a/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryService.java b/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryService.java new file mode 100644 index 0000000..a38de3d --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryService.java @@ -0,0 +1,9 @@ +package com.example.umc9th.domain.member.service.query; + +import com.example.umc9th.domain.member.dto.req.MemberReqDTO; +import com.example.umc9th.domain.member.dto.res.MemberResDTO; +import jakarta.validation.Valid; + +public interface MemberQueryService { + MemberResDTO.LoginDTO login(MemberReqDTO.@Valid LoginDTO dto); +} diff --git a/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryServiceImpl.java b/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryServiceImpl.java new file mode 100644 index 0000000..09ed077 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/member/service/query/MemberQueryServiceImpl.java @@ -0,0 +1,48 @@ +package com.example.umc9th.domain.member.service.query; + +import com.example.umc9th.domain.member.converter.MemberConverter; +import com.example.umc9th.domain.member.dto.req.MemberReqDTO; +import com.example.umc9th.domain.member.dto.res.MemberResDTO; +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.exception.MemberException; +import com.example.umc9th.domain.member.exception.code.MemberErrorCode; +import com.example.umc9th.domain.member.repository.MemberRepository; +import com.example.umc9th.global.auth.CustomUserDetails; +import com.example.umc9th.global.auth.util.JwtUtil; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MemberQueryServiceImpl implements MemberQueryService{ + + private final MemberRepository memberRepository; + private final JwtUtil jwtUtil; + private final PasswordEncoder encoder; + + @Override + public MemberResDTO.LoginDTO login( + MemberReqDTO.@Valid LoginDTO dto + ) { + + // Member 조회 + Member member = memberRepository.findByEmail(dto.email()) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + + // 비밀번호 검증 + if (!encoder.matches(dto.password(), member.getPassword())){ + throw new MemberException(MemberErrorCode.INVALID); + } + + // JWT 토큰 발급용 UserDetails + CustomUserDetails userDetails = new CustomUserDetails(member); + + // 엑세스 토큰 발급 + String accessToken = jwtUtil.createAccessToken(userDetails); + + // DTO 조립 + return MemberConverter.toLoginDTO(member, accessToken); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/global/auth/AuthenticationEntryPointImpl.java b/src/main/java/com/example/umc9th/global/auth/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..041e24c --- /dev/null +++ b/src/main/java/com/example/umc9th/global/auth/AuthenticationEntryPointImpl.java @@ -0,0 +1,33 @@ +package com.example.umc9th.global.auth; + +import com.example.umc9th.global.apiPayload.ApiResponse; +import com.example.umc9th.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; + +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper = new ObjectMapper(); + + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + ApiResponse errorResponse = ApiResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} diff --git a/src/main/java/com/example/umc9th/global/auth/CustomUserDetails.java b/src/main/java/com/example/umc9th/global/auth/CustomUserDetails.java new file mode 100644 index 0000000..e93f2a1 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/auth/CustomUserDetails.java @@ -0,0 +1,30 @@ +package com.example.umc9th.global.auth; + +import com.example.umc9th.domain.member.entity.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@RequiredArgsConstructor +public class CustomUserDetails implements UserDetails { + + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of(() -> member.getRole().toString()); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } +} diff --git a/src/main/java/com/example/umc9th/global/auth/CustomUserDetailsService.java b/src/main/java/com/example/umc9th/global/auth/CustomUserDetailsService.java new file mode 100644 index 0000000..342080d --- /dev/null +++ b/src/main/java/com/example/umc9th/global/auth/CustomUserDetailsService.java @@ -0,0 +1,29 @@ +package com.example.umc9th.global.auth; + +import com.example.umc9th.domain.member.entity.Member; +import com.example.umc9th.domain.member.exception.MemberException; +import com.example.umc9th.domain.member.exception.code.MemberErrorCode; +import com.example.umc9th.domain.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername( + String username + ) throws UsernameNotFoundException { + // 검증할 Member 조회 + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new MemberException(MemberErrorCode.NOT_FOUND)); + // CustomUserDetails 반환 + return new CustomUserDetails(member); + } +} diff --git a/src/main/java/com/example/umc9th/global/auth/enums/Role.java b/src/main/java/com/example/umc9th/global/auth/enums/Role.java new file mode 100644 index 0000000..673e30c --- /dev/null +++ b/src/main/java/com/example/umc9th/global/auth/enums/Role.java @@ -0,0 +1,5 @@ +package com.example.umc9th.global.auth.enums; + +public enum Role { + ROLE_ADMIN, ROLE_USER +} diff --git a/src/main/java/com/example/umc9th/global/auth/util/JwtAuthFilter.java b/src/main/java/com/example/umc9th/global/auth/util/JwtAuthFilter.java new file mode 100644 index 0000000..826ea67 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/auth/util/JwtAuthFilter.java @@ -0,0 +1,59 @@ +package com.example.umc9th.global.auth.util; + + +import com.example.umc9th.global.auth.CustomUserDetailsService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + // 토큰 가져오기 + String token = request.getHeader("Authorization"); + // token이 없거나 Bearer가 아니면 넘기기 + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + // Bearer이면 추출 + token = token.replace("Bearer ", ""); + // AccessToken 검증하기: 올바른 토큰이면 + if (jwtUtil.isValid(token)) { + // 토큰에서 이메일 추출 + String email = jwtUtil.getEmail(token); + // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성 + UserDetails user = customUserDetailsService.loadUserByUsername(email); + Authentication auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + // 인증 완료 후 SecurityContextHolder에 넣기 + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + + + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/global/auth/util/JwtUtil.java b/src/main/java/com/example/umc9th/global/auth/util/JwtUtil.java new file mode 100644 index 0000000..2bd3308 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/auth/util/JwtUtil.java @@ -0,0 +1,94 @@ +package com.example.umc9th.global.auth.util; + +import com.example.umc9th.global.auth.CustomUserDetails; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + private final Duration accessExpiration; + + public JwtUtil( + @Value("${jwt.token.secretKey}") String secret, + @Value("${jwt.token.expiration.access}") Long accessExpiration + ) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + // AccessToken 생성 + public String createAccessToken(CustomUserDetails user) { + return createToken(user, accessExpiration); + } + + /** 토큰에서 이메일 가져오기 + * + * @param token 유저 정보를 추출할 토큰 + * @return 유저 이메일을 토큰에서 추출합니다 + */ + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기 + } catch (JwtException e) { + return null; + } + } + + /** 토큰 유효성 확인 + * + * @param token 유효한지 확인할 토큰 + * @return True, False 반환합니다 + */ + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException e) { + return false; + } + } + + // 토큰 생성 + private String createToken(CustomUserDetails user, Duration expiration) { + Instant now = Instant.now(); + + // 인가 정보 + String authorities = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + String compact = Jwts.builder() + .subject(user.getUsername()) // User 이메일을 Subject로 + .claim("role", authorities) + .claim("email", user.getUsername()) + .issuedAt(Date.from(now)) // 언제 발급한지 + .expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지 + .signWith(secretKey) // sign할 Key + .compact(); + return compact; + } + + // 토큰 정보 가져오기 + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} diff --git a/src/main/java/com/example/umc9th/global/config/SecurityConfig.java b/src/main/java/com/example/umc9th/global/config/SecurityConfig.java new file mode 100644 index 0000000..a506a77 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/config/SecurityConfig.java @@ -0,0 +1,117 @@ +package com.example.umc9th.global.config; + +import com.example.umc9th.global.auth.AuthenticationEntryPointImpl; +import com.example.umc9th.global.auth.CustomUserDetailsService; +import com.example.umc9th.global.auth.util.JwtAuthFilter; +import com.example.umc9th.global.auth.util.JwtUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@EnableWebSecurity +@Configuration +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + private final String[] allowUris = { + "/login", + "/sign-up", + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + // 폼로그인 비활성화 + .formLogin(AbstractHttpConfigurer::disable) + // JwtAuthFilter를 UsernamePasswordAuthenticationFilter + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) + .csrf(AbstractHttpConfigurer::disable) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ) + .exceptionHandling(exception -> exception.authenticationEntryPoint(authenticationEntryPoint())) + + ; + + return http.build(); + } + + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new AuthenticationEntryPointImpl(); + } + +} + +//@EnableWebSecurity +//@Configuration +//public class SecurityConfig { +// +// private final String[] allowUris = { +// "/sign-up", +// "/login", +// "/logout", +// "/swagger-ui/**", +// "/swagger-resources/**", +// "/v3/api-docs/**", +// }; +// +// @Bean +// public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { +// http +// .authorizeHttpRequests(requests -> requests +// .requestMatchers(allowUris).permitAll() +// .requestMatchers("/admin/**").hasRole("ADMIN") +// .anyRequest().authenticated() +// ) +// .formLogin(form -> form +// .defaultSuccessUrl("/swagger-ui/index.html", true) +// .permitAll() +// ) +// .csrf(AbstractHttpConfigurer::disable) +// .logout(logout -> logout +// .logoutUrl("/logout") +// .logoutSuccessUrl("/login?logout") +// .permitAll() +// ); +// +// return http.build(); +// } +// +// @Bean +// public PasswordEncoder passwordEncoder() { +// return new BCryptPasswordEncoder(); +// } +//} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 92a91fc..a692219 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -21,4 +21,10 @@ spring: logging: level: org.hibernate.SQL: debug - org.hibernate.orm.jdbc.bind: trace \ No newline at end of file + org.hibernate.orm.jdbc.bind: trace + +jwt: + token: + secretKey: ZGh3YWlkc2F2ZXdhZXZ3b2EgMTM5ZXUgMDMxdWMyIHEyMiBAIDAgKTJFVio= + expiration: + access: 14400000 \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index b356347..690a64e 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -8,3 +8,9 @@ spring: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop + +jwt: + token: + secretKey: ZGh3YWlkc2F2ZXdhZXZ3b2EgMTM5ZXUgMDMxdWMyIHEyMiBAIDAgKTJFVio= + expiration: + access: 14400000