From b8e9afbb2e05db86fe2bc6cac5e003e27c0f8a9f Mon Sep 17 00:00:00 2001 From: jwpark1211 <1211abc@naver.com> Date: Sat, 25 May 2024 02:54:58 +0900 Subject: [PATCH 1/3] build : add jsonWebToken dependencies --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index 9f7b601..7817635 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-webflux' //webClient implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' //swagger implementation 'org.springframework.boot:spring-boot-starter-data-redis' //redis @@ -37,6 +38,10 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.mockito:mockito-core' testImplementation 'org.mockito:mockito-junit-jupiter' + //JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' } tasks.named('test') { From 2d015aa5b231fe9b7db32475badfb69cca674389 Mon Sep 17 00:00:00 2001 From: jwpark1211 <1211abc@naver.com> Date: Sat, 25 May 2024 02:55:32 +0900 Subject: [PATCH 2/3] fix : password encoder --- src/main/java/capstone/bookitty/initDB.java | 32 +++++++++++---------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/main/java/capstone/bookitty/initDB.java b/src/main/java/capstone/bookitty/initDB.java index 298d57a..166c9d4 100644 --- a/src/main/java/capstone/bookitty/initDB.java +++ b/src/main/java/capstone/bookitty/initDB.java @@ -5,6 +5,7 @@ import jakarta.persistence.EntityManager; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import java.time.LocalDate; @@ -28,24 +29,25 @@ public void init(){ static class InitService{ private final EntityManager em; + private final PasswordEncoder pwEncoder; public void dbInit(){ List members = Arrays.asList( - new Member("김민준", "alswns@gmail.com", "Wo1902!si", null, Gender.MALE, LocalDate.of(1992, 7, 21)), - new Member("이서현", "dltjgus@gmail.com", "Wo1902!si", null, Gender.FEMALE, LocalDate.of(2010, 12, 8)), - new Member("서진호", "tjwlsgn@gmail.com", "Wo1902!si", null, Gender.MALE, LocalDate.of(1971, 8, 28)), - new Member("이선희", "dltjsgml@gmail.com", "Wo1902!si", null, Gender.FEMALE, LocalDate.of(1969, 2, 5)), - new Member("신준서", "tlswnstj@gmail.com", "Wo1902!si", null, Gender.MALE, LocalDate.of(2005, 8, 20)), - new Member("문다연", "ansekdusss@gmail.com", "Wo1902!si", null, Gender.FEMALE, LocalDate.of(1999, 1, 14)), - new Member("윤동현", "dbsehdgus@gmail.com", "Wo1902!si", null, Gender.MALE, LocalDate.of(1989, 7, 3)), - new Member("송지은", "thdwldms@gmail.com", "Wo1902!si", null, Gender.FEMALE, LocalDate.of(1995, 3, 18)), - new Member("김준서", "rlawnstj@gmail.com", "Wo1902!si", null, Gender.MALE, LocalDate.of(2001, 12, 11)), - new Member("임지민", "dlawlals@gmail.com", "Wo1902!si", null, Gender.FEMALE, LocalDate.of(2009, 3, 14)), - new Member("안지성", "dkswltjd@gmail.com", "Wo1902!si", null, Gender.MALE, LocalDate.of(2002, 8, 8)), - new Member("황예린", "ghkddPfls@gmail.com", "Wo1902!si", null, Gender.FEMALE, LocalDate.of(1991, 11, 28)), - new Member("송현우", "thdgusdn@gmail.com", "Wo1902!si", null, Gender.MALE, LocalDate.of(1999, 9, 8)), - new Member("정우진", "wjddnwls@gmail.com", "Wo1902!si", null, Gender.MALE, LocalDate.of(2004, 1, 13)), - new Member("서은우", "tjdnsdmj@gmail.com", "Wo1902!si", null, Gender.MALE, LocalDate.of(2008, 2, 20)) + new Member("김민준", "alswns@gmail.com", pwEncoder.encode("Wo1902!si1"), null, Gender.MALE, LocalDate.of(1992, 7, 21)), + new Member("이서현", "dltjgus@gmail.com", pwEncoder.encode("Wo1902!si2"), null, Gender.FEMALE, LocalDate.of(2010, 12, 8)), + new Member("서진호", "tjwlsgn@gmail.com", pwEncoder.encode("Wo1902!si3"), null, Gender.MALE, LocalDate.of(1971, 8, 28)), + new Member("이선희", "dltjsgml@gmail.com", pwEncoder.encode("Wo1902!si4"), null, Gender.FEMALE, LocalDate.of(1969, 2, 5)), + new Member("신준서", "tlswnstj@gmail.com", pwEncoder.encode("Wo1902!si5"), null, Gender.MALE, LocalDate.of(2005, 8, 20)), + new Member("문다연", "ansekdusss@gmail.com", pwEncoder.encode("Wo1902!si6"), null, Gender.FEMALE, LocalDate.of(1999, 1, 14)), + new Member("윤동현", "dbsehdgus@gmail.com", pwEncoder.encode("Wo1902!si7"), null, Gender.MALE, LocalDate.of(1989, 7, 3)), + new Member("송지은", "thdwldms@gmail.com", pwEncoder.encode("Wo1902!si8"), null, Gender.FEMALE, LocalDate.of(1995, 3, 18)), + new Member("김준서", "rlawnstj@gmail.com", pwEncoder.encode("Wo1902!si9"), null, Gender.MALE, LocalDate.of(2001, 12, 11)), + new Member("임지민", "dlawlals@gmail.com", pwEncoder.encode("Wo1902!si10"), null, Gender.FEMALE, LocalDate.of(2009, 3, 14)), + new Member("안지성", "dkswltjd@gmail.com", pwEncoder.encode("Wo1902!si11"), null, Gender.MALE, LocalDate.of(2002, 8, 8)), + new Member("황예린", "ghkddPfls@gmail.com", pwEncoder.encode("Wo1902!si12"), null, Gender.FEMALE, LocalDate.of(1991, 11, 28)), + new Member("송현우", "thdgusdn@gmail.com", pwEncoder.encode("Wo1902!si13"), null, Gender.MALE, LocalDate.of(1999, 9, 8)), + new Member("정우진", "wjddnwls@gmail.com", pwEncoder.encode("Wo1902!si14"), null, Gender.MALE, LocalDate.of(2004, 1, 13)), + new Member("서은우", "tjdnsdmj@gmail.com", pwEncoder.encode("Wo1902!si15"), null, Gender.MALE, LocalDate.of(2008, 2, 20)) ); for (Member member : members) { From bd1f4b20860894a36760070b683f680bc8ca25b5 Mon Sep 17 00:00:00 2001 From: jwpark1211 <1211abc@naver.com> Date: Sat, 25 May 2024 02:56:09 +0900 Subject: [PATCH 3/3] fix : member login/join -> spring security,jwt --- .../bookitty/common/CustomUserDetails.java | 56 +++++++++ .../common/CustomUserDetailsService.java | 24 ++++ .../bookitty/common/SecurityConfig.java | 66 +++++++++++ .../domain/controller/MemberController.java | 4 +- .../bookitty/domain/dto/MemberDTO.java | 3 + .../bookitty/domain/dto/TokenResponseDTO.java | 19 ++++ .../entity/{Grade.java => Authority.java} | 8 +- .../bookitty/domain/entity/Member.java | 4 +- .../domain/service/MemberService.java | 22 +++- .../java/capstone/bookitty/jwt/JwtFilter.java | 45 ++++++++ .../capstone/bookitty/jwt/JwtProperties.java | 20 ++++ .../java/capstone/bookitty/jwt/JwtToken.java | 14 +++ .../bookitty/jwt/JwtTokenProvider.java | 106 ++++++++++++++++++ 13 files changed, 377 insertions(+), 14 deletions(-) create mode 100644 src/main/java/capstone/bookitty/common/CustomUserDetails.java create mode 100644 src/main/java/capstone/bookitty/common/CustomUserDetailsService.java create mode 100644 src/main/java/capstone/bookitty/common/SecurityConfig.java create mode 100644 src/main/java/capstone/bookitty/domain/dto/TokenResponseDTO.java rename src/main/java/capstone/bookitty/domain/entity/{Grade.java => Authority.java} (50%) create mode 100644 src/main/java/capstone/bookitty/jwt/JwtFilter.java create mode 100644 src/main/java/capstone/bookitty/jwt/JwtProperties.java create mode 100644 src/main/java/capstone/bookitty/jwt/JwtToken.java create mode 100644 src/main/java/capstone/bookitty/jwt/JwtTokenProvider.java diff --git a/src/main/java/capstone/bookitty/common/CustomUserDetails.java b/src/main/java/capstone/bookitty/common/CustomUserDetails.java new file mode 100644 index 0000000..e8daffc --- /dev/null +++ b/src/main/java/capstone/bookitty/common/CustomUserDetails.java @@ -0,0 +1,56 @@ +package capstone.bookitty.common; + +import capstone.bookitty.domain.entity.Member; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; + +public class CustomUserDetails implements UserDetails { + private final Member member; + + public CustomUserDetails(Member member) { + this.member = member; + } + + @Override + public Collection getAuthorities() { + Collection authorities = new ArrayList<>(); + String roles = member.getAuthority().toString(); + for (String role : roles.split(",")) { + authorities.add(() -> role); + } + return authorities; + } + + @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; + } +} \ No newline at end of file diff --git a/src/main/java/capstone/bookitty/common/CustomUserDetailsService.java b/src/main/java/capstone/bookitty/common/CustomUserDetailsService.java new file mode 100644 index 0000000..0848bcd --- /dev/null +++ b/src/main/java/capstone/bookitty/common/CustomUserDetailsService.java @@ -0,0 +1,24 @@ +package capstone.bookitty.common; + +import capstone.bookitty.domain.entity.Member; +import capstone.bookitty.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; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class CustomUserDetailsService implements UserDetailsService { + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email)); + return new CustomUserDetails(member); + } +} \ No newline at end of file diff --git a/src/main/java/capstone/bookitty/common/SecurityConfig.java b/src/main/java/capstone/bookitty/common/SecurityConfig.java new file mode 100644 index 0000000..b2722ce --- /dev/null +++ b/src/main/java/capstone/bookitty/common/SecurityConfig.java @@ -0,0 +1,66 @@ +package capstone.bookitty.common; + +import capstone.bookitty.jwt.JwtFilter; +import capstone.bookitty.jwt.JwtProperties; +import capstone.bookitty.jwt.JwtTokenProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +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.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +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.util.matcher.AntPathRequestMatcher; + +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + private final JwtTokenProvider jwtTokenProvider; + private final JwtProperties jwtProperties; + + public SecurityConfig(JwtTokenProvider jwtTokenProvider, JwtProperties jwtProperties) { + this.jwtTokenProvider = jwtTokenProvider; + this.jwtProperties = jwtProperties; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .headers((headersConfig) -> + headersConfig.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) + .authorizeHttpRequests((authorizeRequests) -> + authorizeRequests + .requestMatchers(antMatcher("/"), + antMatcher("/members/test"), + antMatcher("/members/login"), + antMatcher("/members/new"), + antMatcher("/members/email/**")).permitAll() + //.requestMatchers(antMatcher("")).authenticated() + .anyRequest().permitAll()) + .formLogin(AbstractHttpConfigurer::disable) + .logout((logout) -> + logout + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) + .invalidateHttpSession(true) + .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK)) + .deleteCookies("JSESSIONID") + ) + .addFilterBefore(new JwtFilter(jwtTokenProvider, jwtProperties), UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } + +} \ No newline at end of file diff --git a/src/main/java/capstone/bookitty/domain/controller/MemberController.java b/src/main/java/capstone/bookitty/domain/controller/MemberController.java index cbb0a57..dbda48c 100644 --- a/src/main/java/capstone/bookitty/domain/controller/MemberController.java +++ b/src/main/java/capstone/bookitty/domain/controller/MemberController.java @@ -3,6 +3,7 @@ import capstone.bookitty.domain.dto.ResponseType.BasicResponse; import capstone.bookitty.domain.dto.ResponseType.ResponseCounter; import capstone.bookitty.domain.dto.ResponseType.ResponseString; +import capstone.bookitty.domain.dto.TokenResponseDTO; import capstone.bookitty.domain.service.MemberService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -58,9 +59,8 @@ public ResponseEntity isEmailUnique( public ResponseEntity login( @RequestBody @Valid MemberLoginRequest request ){ - memberService.login(request); return ResponseEntity.ok() - .body(new ResponseString("login success!")); + .body(new ResponseCounter(memberService.login(request))); } @Operation(summary = "id로 회원 조회") diff --git a/src/main/java/capstone/bookitty/domain/dto/MemberDTO.java b/src/main/java/capstone/bookitty/domain/dto/MemberDTO.java index 4b47438..909120b 100644 --- a/src/main/java/capstone/bookitty/domain/dto/MemberDTO.java +++ b/src/main/java/capstone/bookitty/domain/dto/MemberDTO.java @@ -12,11 +12,13 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.security.crypto.password.PasswordEncoder; import java.time.LocalDate; public class MemberDTO { @Data + @NoArgsConstructor @AllArgsConstructor public static class MemberSaveRequest { @NotBlank(message = "Email is a required entry value.") @@ -34,6 +36,7 @@ public static class MemberSaveRequest { private String name; } + @Data public static class MemberLoginRequest { @NotBlank(message = "Email is a required entry value.") diff --git a/src/main/java/capstone/bookitty/domain/dto/TokenResponseDTO.java b/src/main/java/capstone/bookitty/domain/dto/TokenResponseDTO.java new file mode 100644 index 0000000..bff0101 --- /dev/null +++ b/src/main/java/capstone/bookitty/domain/dto/TokenResponseDTO.java @@ -0,0 +1,19 @@ +package capstone.bookitty.domain.dto; + +import capstone.bookitty.jwt.JwtToken; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; + +@Data +@Getter +@Builder +public class TokenResponseDTO { + private Long idx; + private JwtToken jwtToken; + + public TokenResponseDTO(Long idx, JwtToken jwtToken) { + this.idx = idx; + this.jwtToken = jwtToken; + } +} \ No newline at end of file diff --git a/src/main/java/capstone/bookitty/domain/entity/Grade.java b/src/main/java/capstone/bookitty/domain/entity/Authority.java similarity index 50% rename from src/main/java/capstone/bookitty/domain/entity/Grade.java rename to src/main/java/capstone/bookitty/domain/entity/Authority.java index 6b50a7f..45762ff 100644 --- a/src/main/java/capstone/bookitty/domain/entity/Grade.java +++ b/src/main/java/capstone/bookitty/domain/entity/Authority.java @@ -3,10 +3,6 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor -public enum Grade { - ADMIN("관리자"), - USER("사용자") - ; - - private final String grade; +public enum Authority { + ROLE_ADMIN, ROLE_USER } diff --git a/src/main/java/capstone/bookitty/domain/entity/Member.java b/src/main/java/capstone/bookitty/domain/entity/Member.java index 8cd4e41..311f4c8 100644 --- a/src/main/java/capstone/bookitty/domain/entity/Member.java +++ b/src/main/java/capstone/bookitty/domain/entity/Member.java @@ -32,7 +32,7 @@ public class Member { private Gender gender; @Enumerated(EnumType.STRING) - private Grade grade; + private Authority authority; @Builder public Member(String name, String email, String password, String profileImg, @@ -45,7 +45,7 @@ public Member(String name, String email, String password, String profileImg, this.birthDate = birthDate; this.createdAt = LocalDateTime.now(); this.profileImg = "https://bookitty-bucket.s3.ap-northeast-2.amazonaws.com/Jiji.jpeg"; - this.grade = Grade.USER; + this.authority = Authority.ROLE_USER; } public void updateProfile(String profileImg){ diff --git a/src/main/java/capstone/bookitty/domain/service/MemberService.java b/src/main/java/capstone/bookitty/domain/service/MemberService.java index d3c59c8..0d83eef 100644 --- a/src/main/java/capstone/bookitty/domain/service/MemberService.java +++ b/src/main/java/capstone/bookitty/domain/service/MemberService.java @@ -1,11 +1,18 @@ package capstone.bookitty.domain.service; +import capstone.bookitty.domain.dto.TokenResponseDTO; import capstone.bookitty.domain.entity.Member; import capstone.bookitty.domain.repository.MemberRepository; +import capstone.bookitty.jwt.JwtToken; +import capstone.bookitty.jwt.JwtTokenProvider; import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartException; @@ -21,6 +28,9 @@ @RequiredArgsConstructor public class MemberService { private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final AuthenticationManagerBuilder authenticationManagerBuilder; + private final JwtTokenProvider jwtTokenProvider; private final S3Service s3Service; @Transactional @@ -31,7 +41,7 @@ public IdResponse saveMember(MemberSaveRequest request) { Member member = Member.builder() .email(request.getEmail()) .name(request.getName()) - .password(request.getPassword()) + .password(passwordEncoder.encode(request.getPassword())) .birthDate(request.getBirthdate()) .gender(request.getGender()) .build(); @@ -45,11 +55,15 @@ public BoolResponse isEmailUnique(String email) { } @Transactional - public void login(MemberLoginRequest request) { + public TokenResponseDTO login(MemberLoginRequest request) { + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()); + Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); + + JwtToken jwtToken = jwtTokenProvider.generateTokenDto(authentication); Member member = memberRepository.findByEmail(request.getEmail()) .orElseThrow(()-> new EntityNotFoundException("Member not found.")); - if(!member.getPassword().equals(request.getPassword())) - throw new IllegalArgumentException("Invalid login credentials."); + return new TokenResponseDTO(member.getId(), jwtToken); } public MemberInfoResponse getMemberInfoWithId(Long memberId) { diff --git a/src/main/java/capstone/bookitty/jwt/JwtFilter.java b/src/main/java/capstone/bookitty/jwt/JwtFilter.java new file mode 100644 index 0000000..a296d0c --- /dev/null +++ b/src/main/java/capstone/bookitty/jwt/JwtFilter.java @@ -0,0 +1,45 @@ +package capstone.bookitty.jwt; + + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class JwtFilter extends OncePerRequestFilter { + private final JwtTokenProvider jwtTokenProvider; + private final JwtProperties jwtProperties; + + public JwtFilter(JwtTokenProvider jwtTokenProvider, JwtProperties jwtProperties) { + this.jwtTokenProvider = jwtTokenProvider; + this.jwtProperties = jwtProperties; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String jwt = resolveToken(request); + + // 토큰 유효성 검사 + if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) { + Authentication authentication = jwtTokenProvider.getAuthentication(jwt); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } + + // request 헤더에서 토큰 정보 가져오기 + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(jwtProperties.getAccessTokenHeader()); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtProperties.getAuthType())) { + return bearerToken.split(" ")[1].trim(); + } + return null; + } +} diff --git a/src/main/java/capstone/bookitty/jwt/JwtProperties.java b/src/main/java/capstone/bookitty/jwt/JwtProperties.java new file mode 100644 index 0000000..09ab9b6 --- /dev/null +++ b/src/main/java/capstone/bookitty/jwt/JwtProperties.java @@ -0,0 +1,20 @@ +package capstone.bookitty.jwt; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "jwt") +public class JwtProperties { + private String secretKey; + private String authType; + private String authoritiesKey; + private String accessTokenHeader; + private String refreshTokenHeader; + private Long accessTokenExpire; + private Long refreshTokenExpire; +} diff --git a/src/main/java/capstone/bookitty/jwt/JwtToken.java b/src/main/java/capstone/bookitty/jwt/JwtToken.java new file mode 100644 index 0000000..4341128 --- /dev/null +++ b/src/main/java/capstone/bookitty/jwt/JwtToken.java @@ -0,0 +1,14 @@ +package capstone.bookitty.jwt; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +@AllArgsConstructor +public class JwtToken { + private String grantType; + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/capstone/bookitty/jwt/JwtTokenProvider.java b/src/main/java/capstone/bookitty/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..87b94c5 --- /dev/null +++ b/src/main/java/capstone/bookitty/jwt/JwtTokenProvider.java @@ -0,0 +1,106 @@ +package capstone.bookitty.jwt; + +import capstone.bookitty.common.CustomUserDetailsService; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class JwtTokenProvider { + private final JwtProperties jwtProperties; + + private final Key key; + private final CustomUserDetailsService customUserDetailsService; + + public JwtTokenProvider(@Value("${jwt.secretKey}") String secretKey, JwtProperties jwtProperties, CustomUserDetailsService customUserDetailsService) { + this.jwtProperties = jwtProperties; + this.customUserDetailsService = customUserDetailsService; + byte[] keyBytes = secretKey.getBytes(); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + public JwtToken generateTokenDto(Authentication authentication) { + // 권한 + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + long now = (new Date()).getTime(); + + // access token 생성 + Date accessTokenExpiresIn = new Date(now + jwtProperties.getAccessTokenExpire()); + String accessToken = Jwts.builder() + .setSubject(authentication.getName()) + .claim(jwtProperties.getAuthoritiesKey(), authorities) + .setExpiration(accessTokenExpiresIn) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + //refresh token 생성 + Date refreshTokenExpiresIn = new Date(now + jwtProperties.getRefreshTokenExpire()); + String refreshToken = Jwts.builder() + .setExpiration(refreshTokenExpiresIn) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + + return JwtToken.builder() + .grantType(jwtProperties.getAuthType()) + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + } + + public Authentication getAuthentication(String accessToken) { + // 토큰 복호화 + Claims claims = parseClaims(accessToken); + + if (claims.get(jwtProperties.getAuthoritiesKey()) == null) { + throw new RuntimeException("Token with no permissions information"); + } + + Collection authorities = Arrays.stream(claims.get(jwtProperties.getAuthoritiesKey()).toString().split(",")) + .map(SimpleGrantedAuthority::new) + .toList(); + + UserDetails userDetails = customUserDetailsService.loadUserByUsername(claims.getSubject()); + return new UsernamePasswordAuthenticationToken(userDetails, accessToken, userDetails.getAuthorities()); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + log.info("Invalid JWT Token", e); + } catch (ExpiredJwtException e) { + log.info("Expired JWT Token", e); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT Token", e); + } catch (IllegalArgumentException e) { + log.info("JWT claims string is empty.", e); + } + return false; + } + + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } +} \ No newline at end of file