From 7289079f30d1c007be9fc6686556e26a4104c628 Mon Sep 17 00:00:00 2001 From: kkeunii Date: Sun, 30 Nov 2025 23:52:48 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=2010=EC=A3=BC=EC=B0=A8=20=EC=8B=A4?= =?UTF-8?q?=EC=8A=B5=20:=20=EC=84=B8=EC=85=98=20=EA=B8=B0=EB=B0=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workbook03/jerry/build.gradle" | 4 ++ .../jerry/domain/config/SecurityConfig.java" | 49 +++++++++++++++++++ .../domain/converter/MemberConverter.java" | 27 ++++++++++ .../dto/request/MemberLoginReqDto.java" | 3 +- .../example/jerry/domain/entity/Member.java" | 4 ++ .../jerry/domain/service/MemberService.java" | 20 +++++--- .../global/auth/enums/CustomUserDetails.java" | 43 ++++++++++++++++ .../auth/enums/CustomUserDetailsService.java" | 28 +++++++++++ .../jerry/global/auth/enums/Role.java" | 5 ++ 9 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 "\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/config/SecurityConfig.java" create mode 100644 "\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/converter/MemberConverter.java" create mode 100644 "\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/CustomUserDetails.java" create mode 100644 "\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/CustomUserDetailsService.java" create mode 100644 "\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/Role.java" diff --git "a/\354\240\234\353\246\254/workbook03/jerry/build.gradle" "b/\354\240\234\353\246\254/workbook03/jerry/build.gradle" index 882fd15..508a86c 100644 --- "a/\354\240\234\353\246\254/workbook03/jerry/build.gradle" +++ "b/\354\240\234\353\246\254/workbook03/jerry/build.gradle" @@ -46,6 +46,10 @@ dependencies { // Validation implementation 'org.springframework.boot:spring-boot-starter-validation' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/config/SecurityConfig.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/config/SecurityConfig.java" new file mode 100644 index 0000000..a661d89 --- /dev/null +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/config/SecurityConfig.java" @@ -0,0 +1,49 @@ +package com.example.jerry.domain.config; + +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + + private final String[] allowUris = { + "/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(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/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/converter/MemberConverter.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/converter/MemberConverter.java" new file mode 100644 index 0000000..62d6c29 --- /dev/null +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/converter/MemberConverter.java" @@ -0,0 +1,27 @@ +package com.example.jerry.domain.converter; + +import com.example.jerry.domain.dto.request.MemberSignupReqDto; +import com.example.jerry.domain.entity.Member; +import com.example.jerry.global.auth.enums.Role; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class MemberConverter { + + public static Member toMember(MemberSignupReqDto reqDto, + String encodedPassword, + Role role) { + if (reqDto == null) { + return null; + } + + return Member.builder() + .email(reqDto.getEmail()) + .password(encodedPassword) + .name(reqDto.getName()) + .phone(reqDto.getPhone()) + .role(role) + .build(); + } +} diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/dto/request/MemberLoginReqDto.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/dto/request/MemberLoginReqDto.java" index bcd66b5..3736d28 100644 --- "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/dto/request/MemberLoginReqDto.java" +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/dto/request/MemberLoginReqDto.java" @@ -1,10 +1,11 @@ package com.example.jerry.domain.dto.request; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -@Getter @Setter +@Getter @NoArgsConstructor public class MemberLoginReqDto { diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/entity/Member.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/entity/Member.java" index 5e7135f..8ddaf4b 100644 --- "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/entity/Member.java" +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/entity/Member.java" @@ -1,5 +1,6 @@ package com.example.jerry.domain.entity; +import com.example.jerry.global.auth.enums.Role; import jakarta.persistence.*; import lombok.*; @@ -24,6 +25,9 @@ public class Member { @Column(length = 200, nullable = false) private String password; + @Enumerated(EnumType.STRING) + private Role role; + @Column(length = 30, nullable = false) private String name; diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/service/MemberService.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/service/MemberService.java" index ff01ec0..bbe74e0 100644 --- "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/service/MemberService.java" +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/service/MemberService.java" @@ -2,6 +2,8 @@ import java.util.List; import java.util.ArrayList; + +import com.example.jerry.domain.converter.MemberConverter; import com.example.jerry.domain.entity.Food; import com.example.jerry.domain.entity.Member; import com.example.jerry.domain.entity.MemberPreference; @@ -14,7 +16,9 @@ import com.example.jerry.domain.repository.FoodRepository; import com.example.jerry.domain.repository.MemberRepository; import com.example.jerry.domain.repository.MemberPreferenceRepository; +import com.example.jerry.global.auth.enums.Role; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,6 +31,7 @@ public class MemberService { private final MemberRepository memberRepository; private final FoodRepository foodRepository; private final MemberPreferenceRepository memberPreferenceRepository; + private final PasswordEncoder passwordEncoder; // 회원가입 @@ -40,13 +45,14 @@ public MemberResDto signup(MemberSignupReqDto req) { throw new TestException(MemberErrorCode.DUPLICATE_PHONE); } + String encodedPassword = passwordEncoder.encode(req.getPassword()); + // Member 생성 및 저장 - Member member = Member.builder() - .email(req.getEmail()) - .password(req.getPassword()) - .name(req.getName()) - .phone(req.getPhone()) - .build(); + Member member = MemberConverter.toMember( + req, + encodedPassword, + Role.ROLE_USER // 기본 권한 + ); memberRepository.save(member); @@ -81,7 +87,7 @@ public MemberResDto login(MemberLoginReqDto req) { Member member = memberRepository.findByEmail(req.getEmail()) .orElseThrow(() -> new TestException(MemberErrorCode.INVALID_LOGIN)); - if (!member.getPassword().equals(req.getPassword())) { + if (!passwordEncoder.matches(req.getPassword(), member.getPassword())) { throw new TestException(MemberErrorCode.INVALID_LOGIN); } diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/CustomUserDetails.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/CustomUserDetails.java" new file mode 100644 index 0000000..49bc2e0 --- /dev/null +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/CustomUserDetails.java" @@ -0,0 +1,43 @@ +package com.example.jerry.global.auth.enums; + +import com.example.jerry.domain.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() { + // ROLE_... 문자열로 권한 반환 (Role enum은 com.example.jerry.global.auth.enums.Role 에 있음) + return List.of(() -> member.getRole().toString()); + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } + + @Override + public boolean isAccountNonExpired() { return true; } + + @Override + public boolean isAccountNonLocked() { return true; } + + @Override + public boolean isCredentialsNonExpired() { return true; } + + @Override + public boolean isEnabled() { return true; } +} diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/CustomUserDetailsService.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/CustomUserDetailsService.java" new file mode 100644 index 0000000..87bb291 --- /dev/null +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/CustomUserDetailsService.java" @@ -0,0 +1,28 @@ +package com.example.jerry.global.auth.enums; + +import com.example.jerry.domain.entity.Member; +import com.example.jerry.domain.exception.TestException; +import com.example.jerry.domain.exception.code.MemberErrorCode; +import com.example.jerry.domain.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 = memberRepository.findByEmail(username) + .orElseThrow(() -> new TestException(MemberErrorCode.MEMBER_NOT_FOUND)); + + return new CustomUserDetails(member); + } +} diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/Role.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/Role.java" new file mode 100644 index 0000000..3e91c14 --- /dev/null +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/enums/Role.java" @@ -0,0 +1,5 @@ +package com.example.jerry.global.auth.enums; + +public enum Role { + ROLE_ADMIN, ROLE_USER +} \ No newline at end of file From d67dffefdf9fbf3d6233bdb3b2b7bc9f1a41f0a5 Mon Sep 17 00:00:00 2001 From: kkeunii Date: Mon, 1 Dec 2025 00:23:53 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=2010=EC=A3=BC=EC=B0=A8=20=EC=8B=A4?= =?UTF-8?q?=EC=8A=B5=20:=20JWT=20=EB=B0=A9=EC=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workbook03/jerry/build.gradle" | 8 ++ .../jerry/domain/config/SecurityConfig.java" | 23 +++-- .../domain/converter/MemberConverter.java" | 11 +++ .../domain/dto/response/MemberResDto.java" | 13 +-- .../jerry/domain/service/MemberService.java" | 11 +-- .../auth/AuthenticationEntryPointImpl.java" | 39 +++++++++ .../jerry/global/auth/JwtAuthFilter.java" | 81 ++++++++++++++++++ .../example/jerry/global/auth/JwtUtil.java" | 84 +++++++++++++++++++ .../jerry/src/main/resources/application.yml" | 8 +- 9 files changed, 255 insertions(+), 23 deletions(-) create mode 100644 "\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/AuthenticationEntryPointImpl.java" create mode 100644 "\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/JwtAuthFilter.java" create mode 100644 "\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/JwtUtil.java" diff --git "a/\354\240\234\353\246\254/workbook03/jerry/build.gradle" "b/\354\240\234\353\246\254/workbook03/jerry/build.gradle" index 508a86c..34b8194 100644 --- "a/\354\240\234\353\246\254/workbook03/jerry/build.gradle" +++ "b/\354\240\234\353\246\254/workbook03/jerry/build.gradle" @@ -50,6 +50,14 @@ dependencies { // Security 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' + + } tasks.named('test') { diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/config/SecurityConfig.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/config/SecurityConfig.java" index a661d89..555e6de 100644 --- "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/config/SecurityConfig.java" +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/config/SecurityConfig.java" @@ -1,5 +1,9 @@ package com.example.jerry.domain.config; +import com.example.jerry.global.auth.JwtAuthFilter; +import com.example.jerry.global.auth.JwtUtil; +import com.example.jerry.global.auth.enums.CustomUserDetailsService; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -8,13 +12,17 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +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 = { - "/sign-up", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", @@ -28,10 +36,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ) - .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui/index.html", true) - .permitAll() - ) + // 폼로그인 비활성화 + .formLogin(AbstractHttpConfigurer::disable) + // JwtAuthFilter를 UsernamePasswordAuthenticationFilter 앞에 추가 + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) .csrf(AbstractHttpConfigurer::disable) .logout(logout -> logout .logoutUrl("/logout") @@ -42,6 +50,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/converter/MemberConverter.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/converter/MemberConverter.java" index 62d6c29..8b207fa 100644 --- "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/converter/MemberConverter.java" +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/converter/MemberConverter.java" @@ -1,6 +1,7 @@ package com.example.jerry.domain.converter; import com.example.jerry.domain.dto.request.MemberSignupReqDto; +import com.example.jerry.domain.dto.response.MemberResDto; import com.example.jerry.domain.entity.Member; import com.example.jerry.global.auth.enums.Role; import lombok.AccessLevel; @@ -24,4 +25,14 @@ public static Member toMember(MemberSignupReqDto reqDto, .role(role) .build(); } + + public static MemberResDto toMemberResDto(Member member) { + return MemberResDto.builder() + .memberId(member.getMemberId()) + .email(member.getEmail()) + .name(member.getName()) + .phone(member.getPhone()) + .points(member.getPoints()) + .build(); + } } diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/dto/response/MemberResDto.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/dto/response/MemberResDto.java" index 9c2cb92..72f049b 100644 --- "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/dto/response/MemberResDto.java" +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/dto/response/MemberResDto.java" @@ -1,6 +1,5 @@ package com.example.jerry.domain.dto.response; -import com.example.jerry.domain.entity.Member; import lombok.Builder; import lombok.Getter; @@ -13,14 +12,4 @@ public class MemberResDto { private String name; private String phone; private Long points; - - public static MemberResDto from(Member member) { - return MemberResDto.builder() - .memberId(member.getMemberId()) - .email(member.getEmail()) - .name(member.getName()) - .phone(member.getPhone()) - .points(member.getPoints()) - .build(); - } -} \ No newline at end of file +} diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/service/MemberService.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/service/MemberService.java" index bbe74e0..fad2cce 100644 --- "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/service/MemberService.java" +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/domain/service/MemberService.java" @@ -16,6 +16,7 @@ import com.example.jerry.domain.repository.FoodRepository; import com.example.jerry.domain.repository.MemberRepository; import com.example.jerry.domain.repository.MemberPreferenceRepository; +import com.example.jerry.global.auth.JwtUtil; import com.example.jerry.global.auth.enums.Role; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; @@ -32,6 +33,7 @@ public class MemberService { private final FoodRepository foodRepository; private final MemberPreferenceRepository memberPreferenceRepository; private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; // 회원가입 @@ -47,11 +49,10 @@ public MemberResDto signup(MemberSignupReqDto req) { String encodedPassword = passwordEncoder.encode(req.getPassword()); - // Member 생성 및 저장 Member member = MemberConverter.toMember( req, encodedPassword, - Role.ROLE_USER // 기본 권한 + Role.ROLE_USER ); memberRepository.save(member); @@ -77,7 +78,7 @@ public MemberResDto signup(MemberSignupReqDto req) { memberPreferenceRepository.saveAll(preferences); } - return MemberResDto.from(member); + return MemberConverter.toMemberResDto(member); } @@ -91,7 +92,7 @@ public MemberResDto login(MemberLoginReqDto req) { throw new TestException(MemberErrorCode.INVALID_LOGIN); } - return MemberResDto.from(member); + return MemberConverter.toMemberResDto(member); } // 멤버 단일 조회 @@ -100,6 +101,6 @@ public MemberResDto getMember(Long memberId) { Member member = memberRepository.findById(memberId) .orElseThrow(() -> new TestException(MemberErrorCode.MEMBER_NOT_FOUND)); - return MemberResDto.from(member); + return MemberConverter.toMemberResDto(member); } } \ No newline at end of file diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/AuthenticationEntryPointImpl.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/AuthenticationEntryPointImpl.java" new file mode 100644 index 0000000..22b55ee --- /dev/null +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/AuthenticationEntryPointImpl.java" @@ -0,0 +1,39 @@ +package com.example.jerry.global.auth; + +import com.example.jerry.domain.exception.TestException; +import com.example.jerry.global.apiPayload.ApiResponse; +import com.example.jerry.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.io.IOException; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper = new ObjectMapper(); + + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + TestException authException + ) throws IOException, java.io.IOException { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + com.example.jerry.global.apiPayload.ApiResponse errorResponse = ApiResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws java.io.IOException, ServletException { + + } +} \ No newline at end of file diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/JwtAuthFilter.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/JwtAuthFilter.java" new file mode 100644 index 0000000..e15265e --- /dev/null +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/JwtAuthFilter.java" @@ -0,0 +1,81 @@ +package com.example.jerry.global.auth; + +import com.example.jerry.global.apiPayload.ApiResponse; +import com.example.jerry.global.apiPayload.code.GeneralErrorCode; +import com.example.jerry.global.auth.enums.CustomUserDetailsService; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.NonNull; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@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 { + + try { + // 토큰 가져오기 + 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() + ); + + // SecurityContext에 인증 정보 저장 + SecurityContextHolder.getContext().setAuthentication(auth); + } + + filterChain.doFilter(request, response); + + } catch (Exception e) { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + ApiResponse errorResponse = ApiResponse.onFailure( + GeneralErrorCode.UNAUTHORIZED, + null + ); + + ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/JwtUtil.java" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/JwtUtil.java" new file mode 100644 index 0000000..48b352d --- /dev/null +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/java/com/example/jerry/global/auth/JwtUtil.java" @@ -0,0 +1,84 @@ +package com.example.jerry.global.auth; + +import com.example.jerry.global.auth.enums.CustomUserDetails; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.security.core.GrantedAuthority; + +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); + } + + /** 토큰에서 이메일 가져오기 */ + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); + } catch (JwtException e) { + return null; + } + } + + /** 토큰 유효성 확인 */ + 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(",")); + + return Jwts.builder() + .subject(user.getUsername()) + .claim("role", authorities) + .claim("email", user.getUsername()) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plus(expiration))) + .signWith(secretKey) + .compact(); + } + + // 토큰 정보 가져오기 + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} diff --git "a/\354\240\234\353\246\254/workbook03/jerry/src/main/resources/application.yml" "b/\354\240\234\353\246\254/workbook03/jerry/src/main/resources/application.yml" index 055df66..7a9586b 100644 --- "a/\354\240\234\353\246\254/workbook03/jerry/src/main/resources/application.yml" +++ "b/\354\240\234\353\246\254/workbook03/jerry/src/main/resources/application.yml" @@ -16,4 +16,10 @@ spring: ddl-auto: create-drop properties: hibernate: - format_sql: true \ No newline at end of file + format_sql: true + + jwt: + token: + secretKey: ZGh3YWlkc2F2ZXdhZXZ3b2EgMTM5ZXUgMDMxdWMyIHEyMiBAIDAgKTJFVio= + expiration: + access: 14400000 \ No newline at end of file