From fb2deec83b5c6e0052eabc5f6414ba87997979fe Mon Sep 17 00:00:00 2001 From: ymkim97 Date: Tue, 5 Sep 2023 19:05:57 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[JT-29]=20refactor:=20Config=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=B0=8F=20=EB=B9=8C=EB=93=9C=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DTO static 메서드 명 수정 --- module-application/build.gradle | 10 +++++ .../member/presentation/MemberController.java | 6 +-- .../src/main/resources/application.yml | 2 +- .../com/devtoon/jtoon/smtp/entity/Mail.java | 4 +- module-domain/build.gradle | 1 - .../member/application/MemberService.java | 12 +++--- .../member/repository/MemberRepository.java | 3 +- .../jtoon/member/request/SignUpDto.java | 37 ------------------- module-internal/build.gradle | 3 -- 9 files changed, 23 insertions(+), 55 deletions(-) delete mode 100644 module-domain/src/main/java/com/devtoon/jtoon/member/request/SignUpDto.java diff --git a/module-application/build.gradle b/module-application/build.gradle index 86ecc015..bafd6d1f 100644 --- a/module-application/build.gradle +++ b/module-application/build.gradle @@ -14,4 +14,14 @@ dependencies { // Bean Validation implementation 'org.springframework.boot:spring-boot-starter-validation' + + // JPA + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' } diff --git a/module-application/src/main/java/com/devtoon/jtoon/member/presentation/MemberController.java b/module-application/src/main/java/com/devtoon/jtoon/member/presentation/MemberController.java index ee90f68b..610b7167 100644 --- a/module-application/src/main/java/com/devtoon/jtoon/member/presentation/MemberController.java +++ b/module-application/src/main/java/com/devtoon/jtoon/member/presentation/MemberController.java @@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.RestController; import com.devtoon.jtoon.member.application.MemberService; -import com.devtoon.jtoon.member.request.SignUpDto; +import com.devtoon.jtoon.member.request.SignUpReq; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -23,8 +23,8 @@ public class MemberController { @PostMapping @ResponseStatus(HttpStatus.CREATED) - public void signUp(@RequestBody @Valid SignUpDto signUpDto) { - memberService.createMember(signUpDto); + public void signUp(@RequestBody @Valid SignUpReq signUpReq) { + memberService.createMember(signUpReq); } @GetMapping("/email-authorization") diff --git a/module-application/src/main/resources/application.yml b/module-application/src/main/resources/application.yml index 92374faf..0bf72a40 100644 --- a/module-application/src/main/resources/application.yml +++ b/module-application/src/main/resources/application.yml @@ -1,3 +1,3 @@ spring: profiles: - include: s3, smtp + include: s3, smtp, iamport, jwt diff --git a/module-domain-smtp/src/main/java/com/devtoon/jtoon/smtp/entity/Mail.java b/module-domain-smtp/src/main/java/com/devtoon/jtoon/smtp/entity/Mail.java index be9d42da..5a69b1af 100644 --- a/module-domain-smtp/src/main/java/com/devtoon/jtoon/smtp/entity/Mail.java +++ b/module-domain-smtp/src/main/java/com/devtoon/jtoon/smtp/entity/Mail.java @@ -19,7 +19,7 @@ private Mail(String subject, String to, String text) { this.text = text; } - public static Mail createEvent(String subject, String to, String text) { + public static Mail forEvent(String subject, String to, String text) { return Mail.builder() .subject(subject) .to(to) @@ -27,7 +27,7 @@ public static Mail createEvent(String subject, String to, String text) { .build(); } - public static Mail createAuthentication(String to, String text) { + public static Mail forAuthentication(String to, String text) { return Mail.builder() .to(to) .text(text) diff --git a/module-domain/build.gradle b/module-domain/build.gradle index fb095f8a..6471bdd9 100644 --- a/module-domain/build.gradle +++ b/module-domain/build.gradle @@ -5,7 +5,6 @@ repositories { dependencies { // JPA implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation project(path: ':module-domain-smtp') // I-AM-PORT implementation 'com.github.iamport:iamport-rest-client-java:0.2.23' diff --git a/module-domain/src/main/java/com/devtoon/jtoon/member/application/MemberService.java b/module-domain/src/main/java/com/devtoon/jtoon/member/application/MemberService.java index 87d2a3b2..81d1725f 100644 --- a/module-domain/src/main/java/com/devtoon/jtoon/member/application/MemberService.java +++ b/module-domain/src/main/java/com/devtoon/jtoon/member/application/MemberService.java @@ -8,7 +8,7 @@ import com.devtoon.jtoon.exception.MemberException; import com.devtoon.jtoon.member.entity.Member; import com.devtoon.jtoon.member.repository.MemberRepository; -import com.devtoon.jtoon.member.request.SignUpDto; +import com.devtoon.jtoon.member.request.SignUpReq; import com.devtoon.jtoon.smtp.application.SmtpService; import com.devtoon.jtoon.smtp.entity.Mail; import java.util.UUID; @@ -24,10 +24,10 @@ public class MemberService { private final PasswordEncoder passwordEncoder; @Transactional - public void createMember(SignUpDto signUpDto) { - validateDuplicateEmail(signUpDto.email()); - String encryptedPassword = encodePassword(signUpDto.password()); - Member member = signUpDto.toEntity(encryptedPassword); + public void createMember(SignUpReq signUpReq) { + validateDuplicateEmail(signUpReq.email()); + String encryptedPassword = encodePassword(signUpReq.password()); + Member member = signUpReq.toEntity(encryptedPassword); memberRepository.save(member); } @@ -35,7 +35,7 @@ public String sendEmailAuthentication(String email) { validateDuplicateEmail(email); UUID uuid = UUID.randomUUID(); String randomUuid = uuid.toString().substring(0, 6); - Mail mail = Mail.createAuthentication(email, randomUuid); + Mail mail = Mail.forAuthentication(email, randomUuid); smtpService.sendMail(mail); return randomUuid; diff --git a/module-domain/src/main/java/com/devtoon/jtoon/member/repository/MemberRepository.java b/module-domain/src/main/java/com/devtoon/jtoon/member/repository/MemberRepository.java index 11bca2bd..ec40db4d 100644 --- a/module-domain/src/main/java/com/devtoon/jtoon/member/repository/MemberRepository.java +++ b/module-domain/src/main/java/com/devtoon/jtoon/member/repository/MemberRepository.java @@ -1,7 +1,5 @@ package com.devtoon.jtoon.member.repository; -import java.util.Optional; - import org.springframework.data.jpa.repository.JpaRepository; import com.devtoon.jtoon.member.entity.Member; @@ -10,4 +8,5 @@ public interface MemberRepository extends JpaRepository { boolean existsByEmail(String email); Optional findByPhone(String phone); + Optional findByEmail(String email); } diff --git a/module-domain/src/main/java/com/devtoon/jtoon/member/request/SignUpDto.java b/module-domain/src/main/java/com/devtoon/jtoon/member/request/SignUpDto.java deleted file mode 100644 index fed0846d..00000000 --- a/module-domain/src/main/java/com/devtoon/jtoon/member/request/SignUpDto.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.devtoon.jtoon.member.request; - -import static com.devtoon.jtoon.global.util.RegExp.*; - -import com.devtoon.jtoon.member.entity.Gender; -import com.devtoon.jtoon.member.entity.LoginType; -import com.devtoon.jtoon.member.entity.Member; -import com.devtoon.jtoon.member.entity.Role; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; - -public record SignUpDto( - @Pattern(regexp = EMAIL_PATTERN) String email, - @Pattern(regexp = PASSWORD_PATTERN) String password, - @NotBlank @Size(max = 10) String name, - @NotBlank @Size(max = 30) String nickname, - @NotNull String gender, - @Pattern(regexp = PHONE_PATTERN) String phone, - @NotNull String loginType -) { - - public Member toEntity(String encryptedPassword) { - return Member.builder() - .email(email) - .password(encryptedPassword) - .name(name) - .nickname(nickname) - .gender(Gender.from(gender)) - .phone(phone) - .role(Role.USER) - .loginType(LoginType.from(loginType)) - .build(); - } -} diff --git a/module-internal/build.gradle b/module-internal/build.gradle index fd78076c..e0c58467 100644 --- a/module-internal/build.gradle +++ b/module-internal/build.gradle @@ -1,7 +1,4 @@ dependencies { // Web implementation 'org.springframework.boot:spring-boot-starter-web' - - // Security - implementation 'org.springframework.boot:spring-boot-starter-security' } From 05a5e7d3cc05c4904b3b6b02b20fd3966d6d3860 Mon Sep 17 00:00:00 2001 From: ymkim97 Date: Tue, 5 Sep 2023 19:06:43 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[JT-29]=20feat:=20Security=20Config=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../config/WebSecurityConfiguration.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) rename {module-internal => module-application}/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java (53%) diff --git a/module-internal/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java b/module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java similarity index 53% rename from module-internal/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java rename to module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java index d0ff6dbd..de0c3fa2 100644 --- a/module-internal/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java +++ b/module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java @@ -4,14 +4,24 @@ 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.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import com.devtoon.jtoon.security.filter.JwtAuthenticationFilter; +import com.devtoon.jtoon.security.jwt.JwtProvider; +import lombok.RequiredArgsConstructor; @Configuration +@RequiredArgsConstructor @EnableWebSecurity public class WebSecurityConfiguration { + private final JwtProvider jwtProvider; + @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); @@ -22,7 +32,11 @@ public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Except http .authorizeHttpRequests(request -> request .anyRequest().permitAll()) - ; + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class) + ; return http.build(); } } From de597155709a2ddb00b6ddc1ad3a495eec536afe Mon Sep 17 00:00:00 2001 From: ymkim97 Date: Tue, 5 Sep 2023 19:08:06 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[JT-29]=20feat:=20Security=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20JWT=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/JwtAuthenticationFilter.java | 42 ++++++++ .../jtoon/security/jwt/JwtProvider.java | 96 +++++++++++++++++++ .../application/CustomUserDetailsService.java | 31 ++++++ .../jwt/domain/CustomUserDetails.java | 51 ++++++++++ 4 files changed, 220 insertions(+) create mode 100644 module-application/src/main/java/com/devtoon/jtoon/security/filter/JwtAuthenticationFilter.java create mode 100644 module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java create mode 100644 module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java create mode 100644 module-application/src/main/java/com/devtoon/jtoon/security/jwt/domain/CustomUserDetails.java diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/filter/JwtAuthenticationFilter.java b/module-application/src/main/java/com/devtoon/jtoon/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 00000000..307c3a4b --- /dev/null +++ b/module-application/src/main/java/com/devtoon/jtoon/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,42 @@ +package com.devtoon.jtoon.security.filter; + +import org.jetbrains.annotations.NotNull; +import org.springframework.http.HttpHeaders; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import com.devtoon.jtoon.security.jwt.JwtProvider; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtProvider jwtProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, + @NotNull FilterChain filterChain) throws ServletException, IOException { + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (header != null && header.startsWith("Bearer")) { + try { + String token = header.split(" ")[1]; + jwtProvider.validateToken(token); + Authentication auth = jwtProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(auth); + } catch (RuntimeException e) { + log.error("Invalid Token", e); + + } + } + filterChain.doFilter(request, response); + } +} diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java new file mode 100644 index 00000000..811d2c02 --- /dev/null +++ b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java @@ -0,0 +1,96 @@ +package com.devtoon.jtoon.security.jwt; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.sql.Date; +import java.time.LocalDate; +import java.util.HashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class JwtProvider { + + @Value("${jwt.secret.key}") + private String salt; + + @Value("${jwt.iss}") + private String iss; + + @Value("${jwt.expire}") + private long expire; + + private Key secretKey; + + private final UserDetailsService userDetailsService; + + @PostConstruct + private void init() { + secretKey = Keys.hmacShaKeyFor(salt.getBytes(StandardCharsets.UTF_8)); + } + + public String generateToken(String email) { + Claims claims = getClaims(email); + + return Jwts.builder() + .setClaims(claims) + .signWith(secretKey, SignatureAlgorithm.HS256) + .setHeader(getHeaders()) + .compact(); + } + + private Map getHeaders() { + Map headers = new HashMap<>(); + headers.put("alg", "HS256"); + headers.put("typ", "JWT"); + return headers; + } + + public void validateToken(String token) { + Jws claimsJws = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build().parseClaimsJws(token); + if (claimsJws.getBody().getExpiration().before(Date.valueOf(LocalDate.now()))) { + throw new RuntimeException("Token Expired"); + } + } + + public Authentication getAuthentication(String token) { + String email = Jwts.parserBuilder() + .setSigningKey(secretKey) + .build().parseClaimsJws(token).getBody().getSubject(); + + UserDetails userDetails = userDetailsService.loadUserByUsername(email); + + return new UsernamePasswordAuthenticationToken(userDetails, " ", userDetails.getAuthorities()); + } + + private Claims getClaims(String email) { + Date now = Date.valueOf(LocalDate.now()); + return Jwts.claims() + .setSubject(email) + .setIssuer(iss) + .setExpiration(getExpireTime()) + .setIssuedAt(now); + } + + private Date getExpireTime() { + Date now = Date.valueOf(LocalDate.now()); + return new Date(now.getTime() + 1000 * 60 * expire); + } +} + diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java new file mode 100644 index 00000000..88d4f237 --- /dev/null +++ b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java @@ -0,0 +1,31 @@ +package com.devtoon.jtoon.security.jwt.application; + +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 com.devtoon.jtoon.member.entity.Member; +import com.devtoon.jtoon.member.repository.MemberRepository; +import com.devtoon.jtoon.security.jwt.domain.CustomUserDetails; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +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("Invalid Email") + ); + + return new CustomUserDetails(member); + } +} +/** + * 로그인 필터 동작 X + * -> + */ diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/domain/CustomUserDetails.java b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/domain/CustomUserDetails.java new file mode 100644 index 00000000..f5b66116 --- /dev/null +++ b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/domain/CustomUserDetails.java @@ -0,0 +1,51 @@ +package com.devtoon.jtoon.security.jwt.domain; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import com.devtoon.jtoon.member.entity.Member; +import java.util.Collection; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class CustomUserDetails implements UserDetails { + + private final Member member; + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(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; + } +} From b7bf95d8706a8ebee67ea3a91cc77892c7283336 Mon Sep 17 00:00:00 2001 From: ymkim97 Date: Tue, 5 Sep 2023 19:09:10 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[JT-29]=20feat:=20JWT=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/application/AuthService.java | 36 ++++++++++++++++++ .../security/presentation/AuthController.java | 25 +++++++++++++ .../jtoon/security/request/LogInReq.java | 13 +++++++ .../jtoon/member/request/SignUpReq.java | 37 +++++++++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java create mode 100644 module-application/src/main/java/com/devtoon/jtoon/security/presentation/AuthController.java create mode 100644 module-application/src/main/java/com/devtoon/jtoon/security/request/LogInReq.java create mode 100644 module-domain/src/main/java/com/devtoon/jtoon/member/request/SignUpReq.java diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java b/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java new file mode 100644 index 00000000..4e08da9c --- /dev/null +++ b/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java @@ -0,0 +1,36 @@ +package com.devtoon.jtoon.security.application; + +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import com.devtoon.jtoon.member.entity.Member; +import com.devtoon.jtoon.member.repository.MemberRepository; +import com.devtoon.jtoon.security.jwt.JwtProvider; +import com.devtoon.jtoon.security.request.LogInReq; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + private final JwtProvider jwtProvider; + + public String login(LogInReq logInReq) { + Member member = memberRepository.findByEmail(logInReq.email()).orElseThrow( + () -> new BadCredentialsException("너 안돼!") + ); + + if (!isPasswordSame(logInReq.password(), member.getPassword())) { + throw new BadCredentialsException("너 안돼!"); + } + + return jwtProvider.generateToken(logInReq.email()); + } + + public boolean isPasswordSame(String rawPassword, String memberPassword) { + return passwordEncoder.matches(rawPassword, memberPassword); + } +} diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/presentation/AuthController.java b/module-application/src/main/java/com/devtoon/jtoon/security/presentation/AuthController.java new file mode 100644 index 00000000..b806d4bf --- /dev/null +++ b/module-application/src/main/java/com/devtoon/jtoon/security/presentation/AuthController.java @@ -0,0 +1,25 @@ +package com.devtoon.jtoon.security.presentation; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.devtoon.jtoon.security.application.AuthService; +import com.devtoon.jtoon.security.request.LogInReq; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + public void login(@RequestBody @Valid LogInReq logInReq, HttpServletResponse response) { + String token = authService.login(logInReq); + + response.setHeader("Set-Cookie", "Bearer " + token); + } +} diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/request/LogInReq.java b/module-application/src/main/java/com/devtoon/jtoon/security/request/LogInReq.java new file mode 100644 index 00000000..6c598e72 --- /dev/null +++ b/module-application/src/main/java/com/devtoon/jtoon/security/request/LogInReq.java @@ -0,0 +1,13 @@ +package com.devtoon.jtoon.security.request; + +import static com.devtoon.jtoon.global.util.RegExp.*; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public record LogInReq( + @Pattern(regexp = EMAIL_PATTERN) String email, + @NotBlank String password +) { + +} diff --git a/module-domain/src/main/java/com/devtoon/jtoon/member/request/SignUpReq.java b/module-domain/src/main/java/com/devtoon/jtoon/member/request/SignUpReq.java new file mode 100644 index 00000000..a0c50eba --- /dev/null +++ b/module-domain/src/main/java/com/devtoon/jtoon/member/request/SignUpReq.java @@ -0,0 +1,37 @@ +package com.devtoon.jtoon.member.request; + +import static com.devtoon.jtoon.global.util.RegExp.*; + +import com.devtoon.jtoon.member.entity.Gender; +import com.devtoon.jtoon.member.entity.LoginType; +import com.devtoon.jtoon.member.entity.Member; +import com.devtoon.jtoon.member.entity.Role; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +public record SignUpReq( + @Pattern(regexp = EMAIL_PATTERN) String email, + @Pattern(regexp = PASSWORD_PATTERN) String password, + @NotBlank @Size(max = 10) String name, + @NotBlank @Size(max = 30) String nickname, + @NotNull String gender, + @Pattern(regexp = PHONE_PATTERN) String phone, + @NotNull String loginType +) { + + public Member toEntity(String encryptedPassword) { + return Member.builder() + .email(email) + .password(encryptedPassword) + .name(name) + .nickname(nickname) + .gender(Gender.from(gender)) + .phone(phone) + .role(Role.USER) + .loginType(LoginType.from(loginType)) + .build(); + } +} From 086828b7527e09942ab78e8714b1d81e09a44e4d Mon Sep 17 00:00:00 2001 From: ymkim97 Date: Tue, 5 Sep 2023 19:48:00 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[JT-29]=20fix:=20Date=20=EB=AF=B8=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/config/WebSecurityConfiguration.java | 3 +++ .../devtoon/jtoon/security/jwt/JwtProvider.java | 14 +++++++------- .../jwt/application/CustomUserDetailsService.java | 4 ---- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java b/module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java index de0c3fa2..e4a197e8 100644 --- a/module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java +++ b/module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java @@ -31,6 +31,9 @@ public PasswordEncoder encoder() { public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Exception { http .authorizeHttpRequests(request -> request + .requestMatchers("/members").permitAll() + .requestMatchers("/members/email-authorization").permitAll() + .requestMatchers("/members/**").hasAuthority("USER") .anyRequest().permitAll()) .csrf(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java index 811d2c02..933cce85 100644 --- a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java +++ b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java @@ -15,12 +15,13 @@ import jakarta.annotation.PostConstruct; import java.nio.charset.StandardCharsets; import java.security.Key; -import java.sql.Date; -import java.time.LocalDate; +import java.util.Date; import java.util.HashMap; import java.util.Map; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Component @RequiredArgsConstructor public class JwtProvider { @@ -64,7 +65,7 @@ public void validateToken(String token) { Jws claimsJws = Jwts.parserBuilder() .setSigningKey(secretKey) .build().parseClaimsJws(token); - if (claimsJws.getBody().getExpiration().before(Date.valueOf(LocalDate.now()))) { + if (claimsJws.getBody().getExpiration().before(new Date())) { throw new RuntimeException("Token Expired"); } } @@ -80,16 +81,15 @@ public Authentication getAuthentication(String token) { } private Claims getClaims(String email) { - Date now = Date.valueOf(LocalDate.now()); + Date now = new Date(); return Jwts.claims() .setSubject(email) .setIssuer(iss) - .setExpiration(getExpireTime()) + .setExpiration(getExpireTime(now)) .setIssuedAt(now); } - private Date getExpireTime() { - Date now = Date.valueOf(LocalDate.now()); + private Date getExpireTime(Date now) { return new Date(now.getTime() + 1000 * 60 * expire); } } diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java index 88d4f237..8b9e3fac 100644 --- a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java +++ b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java @@ -25,7 +25,3 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep return new CustomUserDetails(member); } } -/** - * 로그인 필터 동작 X - * -> - */ From 8842b720cdb1baf721e43c27cc99f1ca4c408c34 Mon Sep 17 00:00:00 2001 From: ymkim97 Date: Tue, 5 Sep 2023 20:18:08 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[JT-29]=20feat:=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=EA=B0=84=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/devtoon/jtoon/security/application/AuthService.java | 4 ++++ .../src/main/java/com/devtoon/jtoon/member/entity/Member.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java b/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java index 4e08da9c..13f77b81 100644 --- a/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java +++ b/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java @@ -3,6 +3,7 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.devtoon.jtoon.member.entity.Member; import com.devtoon.jtoon.member.repository.MemberRepository; @@ -11,6 +12,7 @@ import lombok.RequiredArgsConstructor; @Service +@Transactional(readOnly = true) @RequiredArgsConstructor public class AuthService { @@ -18,6 +20,7 @@ public class AuthService { private final PasswordEncoder passwordEncoder; private final JwtProvider jwtProvider; + @Transactional public String login(LogInReq logInReq) { Member member = memberRepository.findByEmail(logInReq.email()).orElseThrow( () -> new BadCredentialsException("너 안돼!") @@ -27,6 +30,7 @@ public String login(LogInReq logInReq) { throw new BadCredentialsException("너 안돼!"); } + member.updateLastLogin(); return jwtProvider.generateToken(logInReq.email()); } diff --git a/module-domain/src/main/java/com/devtoon/jtoon/member/entity/Member.java b/module-domain/src/main/java/com/devtoon/jtoon/member/entity/Member.java index 4abee474..691ae3af 100644 --- a/module-domain/src/main/java/com/devtoon/jtoon/member/entity/Member.java +++ b/module-domain/src/main/java/com/devtoon/jtoon/member/entity/Member.java @@ -75,4 +75,8 @@ private Member(String email, String password, String name, String nickname, Gend this.role = role; this.loginType = loginType; } + + public void updateLastLogin( ) { + lastLoginDate = LocalDateTime.now(); + } } From c29fb67e45972c3fba87d7e8bc2ba8c7b6bee5aa Mon Sep 17 00:00:00 2001 From: ymkim97 Date: Wed, 6 Sep 2023 15:44:08 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[JT-29]=20refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/application/AuthService.java | 6 ++-- .../config/WebSecurityConfiguration.java | 4 ++- .../filter/JwtAuthenticationFilter.java | 12 +++++-- .../jtoon/security/jwt/JwtProvider.java | 35 ++++++++++--------- .../application/CustomUserDetailsService.java | 5 ++- .../member/application/MemberService.java | 1 + 6 files changed, 37 insertions(+), 26 deletions(-) diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java b/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java index 13f77b81..a9782333 100644 --- a/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java +++ b/module-application/src/main/java/com/devtoon/jtoon/security/application/AuthService.java @@ -22,15 +22,15 @@ public class AuthService { @Transactional public String login(LogInReq logInReq) { - Member member = memberRepository.findByEmail(logInReq.email()).orElseThrow( - () -> new BadCredentialsException("너 안돼!") - ); + Member member = memberRepository.findByEmail(logInReq.email()) + .orElseThrow(() -> new BadCredentialsException("너 안돼!")); if (!isPasswordSame(logInReq.password(), member.getPassword())) { throw new BadCredentialsException("너 안돼!"); } member.updateLastLogin(); + return jwtProvider.generateToken(logInReq.email()); } diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java b/module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java index e4a197e8..a876469b 100644 --- a/module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java +++ b/module-application/src/main/java/com/devtoon/jtoon/security/config/WebSecurityConfiguration.java @@ -10,6 +10,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.servlet.HandlerExceptionResolver; import com.devtoon.jtoon.security.filter.JwtAuthenticationFilter; import com.devtoon.jtoon.security.jwt.JwtProvider; @@ -20,6 +21,7 @@ @EnableWebSecurity public class WebSecurityConfiguration { + private final HandlerExceptionResolver handlerExceptionResolver; private final JwtProvider jwtProvider; @Bean @@ -38,7 +40,7 @@ public SecurityFilterChain securityFilterChain (HttpSecurity http) throws Except .csrf(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(new JwtAuthenticationFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new JwtAuthenticationFilter(handlerExceptionResolver, jwtProvider), UsernamePasswordAuthenticationFilter.class) ; return http.build(); } diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/filter/JwtAuthenticationFilter.java b/module-application/src/main/java/com/devtoon/jtoon/security/filter/JwtAuthenticationFilter.java index 307c3a4b..196fc379 100644 --- a/module-application/src/main/java/com/devtoon/jtoon/security/filter/JwtAuthenticationFilter.java +++ b/module-application/src/main/java/com/devtoon/jtoon/security/filter/JwtAuthenticationFilter.java @@ -5,6 +5,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.HandlerExceptionResolver; import com.devtoon.jtoon.security.jwt.JwtProvider; import jakarta.servlet.FilterChain; @@ -19,11 +20,15 @@ @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final HandlerExceptionResolver handlerExceptionResolver; private final JwtProvider jwtProvider; @Override - protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, - @NotNull FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal( + HttpServletRequest request, + @NotNull HttpServletResponse response, + @NotNull FilterChain filterChain + ) throws ServletException, IOException { String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (header != null && header.startsWith("Bearer")) { @@ -34,7 +39,8 @@ protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServlet SecurityContextHolder.getContext().setAuthentication(auth); } catch (RuntimeException e) { log.error("Invalid Token", e); - + handlerExceptionResolver.resolveException(request, response, null, e); + return; } } filterChain.doFilter(request, response); diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java index 933cce85..3db46c33 100644 --- a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java +++ b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/JwtProvider.java @@ -8,7 +8,6 @@ import org.springframework.stereotype.Component; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; @@ -27,13 +26,13 @@ public class JwtProvider { @Value("${jwt.secret.key}") - private String salt; + private String SALT; @Value("${jwt.iss}") - private String iss; + private String ISS; @Value("${jwt.expire}") - private long expire; + private long EXPIRE; private Key secretKey; @@ -41,7 +40,7 @@ public class JwtProvider { @PostConstruct private void init() { - secretKey = Keys.hmacShaKeyFor(salt.getBytes(StandardCharsets.UTF_8)); + secretKey = Keys.hmacShaKeyFor(SALT.getBytes(StandardCharsets.UTF_8)); } public String generateToken(String email) { @@ -58,39 +57,43 @@ private Map getHeaders() { Map headers = new HashMap<>(); headers.put("alg", "HS256"); headers.put("typ", "JWT"); + return headers; } public void validateToken(String token) { - Jws claimsJws = Jwts.parserBuilder() - .setSigningKey(secretKey) - .build().parseClaimsJws(token); - if (claimsJws.getBody().getExpiration().before(new Date())) { + Date claimsExpiration = parseClaimsBody(token).getExpiration(); + + if (claimsExpiration.before(new Date())) { throw new RuntimeException("Token Expired"); } } public Authentication getAuthentication(String token) { - String email = Jwts.parserBuilder() - .setSigningKey(secretKey) - .build().parseClaimsJws(token).getBody().getSubject(); - - UserDetails userDetails = userDetailsService.loadUserByUsername(email); + String ClaimsEmail = parseClaimsBody(token).getSubject(); + UserDetails userDetails = userDetailsService.loadUserByUsername(ClaimsEmail); return new UsernamePasswordAuthenticationToken(userDetails, " ", userDetails.getAuthorities()); } + private Claims parseClaimsBody(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build().parseClaimsJws(token).getBody(); + } + private Claims getClaims(String email) { Date now = new Date(); + return Jwts.claims() .setSubject(email) - .setIssuer(iss) + .setIssuer(ISS) .setExpiration(getExpireTime(now)) .setIssuedAt(now); } private Date getExpireTime(Date now) { - return new Date(now.getTime() + 1000 * 60 * expire); + return new Date(now.getTime() + 1000 * 60 * EXPIRE); } } diff --git a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java index 8b9e3fac..15738d49 100644 --- a/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java +++ b/module-application/src/main/java/com/devtoon/jtoon/security/jwt/application/CustomUserDetailsService.java @@ -18,9 +18,8 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Member member = memberRepository.findByEmail(email).orElseThrow( - () -> new UsernameNotFoundException("Invalid Email") - ); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("Invalid Email")); return new CustomUserDetails(member); } diff --git a/module-domain/src/main/java/com/devtoon/jtoon/member/application/MemberService.java b/module-domain/src/main/java/com/devtoon/jtoon/member/application/MemberService.java index 81d1725f..801bdf8f 100644 --- a/module-domain/src/main/java/com/devtoon/jtoon/member/application/MemberService.java +++ b/module-domain/src/main/java/com/devtoon/jtoon/member/application/MemberService.java @@ -28,6 +28,7 @@ public void createMember(SignUpReq signUpReq) { validateDuplicateEmail(signUpReq.email()); String encryptedPassword = encodePassword(signUpReq.password()); Member member = signUpReq.toEntity(encryptedPassword); + memberRepository.save(member); }