diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/2022-Answer-SolutionChallenge.iml b/.idea/2022-Answer-SolutionChallenge.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/2022-Answer-SolutionChallenge.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..4c97bdf --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,461 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5821db8 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/spring/notinote/build.gradle b/spring/notinote/build.gradle index 558d040..3c25ed3 100644 --- a/spring/notinote/build.gradle +++ b/spring/notinote/build.gradle @@ -23,9 +23,17 @@ repositories { } dependencies { + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - //implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:2.3.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' + developmentOnly 'org.springframework.boot:spring-boot-devtools' compileOnly 'org.projectlombok:lombok' runtimeOnly 'mysql:mysql-connector-java' annotationProcessor 'org.projectlombok:lombok' diff --git a/spring/notinote/src/main/java/com/answer/notinote/Config/properties/AppProperties.java b/spring/notinote/src/main/java/com/answer/notinote/Config/properties/AppProperties.java new file mode 100644 index 0000000..1116527 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/Config/properties/AppProperties.java @@ -0,0 +1,57 @@ +package com.answer.notinote.Config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +@ConfigurationProperties(prefix = "app") +public class AppProperties { + + private final Auth auth = new Auth(); + + private final OAuth2 oauth2 = new OAuth2(); + + public static class Auth { + + private String tokenSecret; + + private long tokenExpirationMsec; + + public String getTokenSecret() { + return tokenSecret; + } + + public void setTokenSecret(String tokenSecret) { + this.tokenSecret = tokenSecret; + } + + public long getTokenExpirationMsec() { + return tokenExpirationMsec; + } + + public void setTokenExpirationMsec(long tokenExpirationMsec) { + this.tokenExpirationMsec = tokenExpirationMsec; + } + } + + public static final class OAuth2 { + + private List authorizedRedirectUris = new ArrayList<>(); + + public List getAuthorizedRedirectUris() { + return authorizedRedirectUris; + } + public OAuth2 authorizedRedirectUris(List authorizedRedirectUris) { + this.authorizedRedirectUris = authorizedRedirectUris; + return this; + } + } + + public Auth getAuth() { + return auth; + } + public OAuth2 getOauth2() { + return oauth2; + } +} \ No newline at end of file diff --git a/spring/notinote/src/main/java/com/answer/notinote/Config/properties/CorsProperties.java b/spring/notinote/src/main/java/com/answer/notinote/Config/properties/CorsProperties.java new file mode 100644 index 0000000..864ac76 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/Config/properties/CorsProperties.java @@ -0,0 +1,14 @@ +package com.answer.notinote.Config.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter @Setter +@ConfigurationProperties(prefix = "cors") +public class CorsProperties { + private String allowedOrigins; + private String allowedMethods; + private String allowedHeaders; + private Long maxAge; +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/Config/security/JwtConfig.java b/spring/notinote/src/main/java/com/answer/notinote/Config/security/JwtConfig.java new file mode 100644 index 0000000..5f2e030 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/Config/security/JwtConfig.java @@ -0,0 +1,13 @@ +package com.answer.notinote.Config.security; + +import com.answer.notinote.auth.token.JwtTokenProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JwtConfig { + + @Value("${jwt.secret}") + private String secret; +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/Config/security/WebSecurityConfig.java b/spring/notinote/src/main/java/com/answer/notinote/Config/security/WebSecurityConfig.java new file mode 100644 index 0000000..5b2e209 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/Config/security/WebSecurityConfig.java @@ -0,0 +1,59 @@ +package com.answer.notinote.Config.security; + +import com.answer.notinote.auth.data.RoleType; +import com.answer.notinote.auth.filter.JwtAuthenticationFilter; +import com.answer.notinote.auth.filter.OAuth2AccessTokenAuthenticationFilter; +import com.answer.notinote.auth.handler.OAuth2LoginFailureHandler; +import com.answer.notinote.auth.handler.OAuth2LoginSuccessHandler; +import com.answer.notinote.auth.token.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +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.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * Spring Security 설정 클래스 + */ +@Configuration +@RequiredArgsConstructor +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + private final JwtTokenProvider jwtTokenProvider; + private final OAuth2AccessTokenAuthenticationFilter oAuth2AccessTokenAuthenticationFilter; + private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler; + private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .httpBasic().disable() + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/", "/login/*", "/join", "/join/*").permitAll() + .and() + .authorizeRequests() + .antMatchers("/test/user") + .hasRole("USER") + .and() + .authorizeRequests() + .antMatchers("/test/admin") + .hasRole("ADMIN") + .and() + .authorizeRequests() + .anyRequest() + .authenticated() + .and() + .oauth2Login() + .successHandler(oAuth2LoginSuccessHandler) + .failureHandler(oAuth2LoginFailureHandler) + .and() + .addFilterBefore(oAuth2AccessTokenAuthenticationFilter, + UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/NotinoteApplication.java b/spring/notinote/src/main/java/com/answer/notinote/NotinoteApplication.java index ffbfd66..6724fb0 100644 --- a/spring/notinote/src/main/java/com/answer/notinote/NotinoteApplication.java +++ b/spring/notinote/src/main/java/com/answer/notinote/NotinoteApplication.java @@ -1,9 +1,14 @@ package com.answer.notinote; +import com.answer.notinote.Config.properties.AppProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication +@EnableConfigurationProperties(AppProperties.class) public class NotinoteApplication { public static void main(String[] args) { diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/controller/UserController.java b/spring/notinote/src/main/java/com/answer/notinote/User/controller/UserController.java new file mode 100644 index 0000000..9783838 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/controller/UserController.java @@ -0,0 +1,78 @@ +package com.answer.notinote.User.controller; + +import com.answer.notinote.User.dto.JoinRequestDto; +import com.answer.notinote.auth.token.JwtTokenProvider; +import com.answer.notinote.User.domain.entity.User; +import com.answer.notinote.User.dto.UserRequestDto; +import com.answer.notinote.User.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@RequestMapping("") +public class UserController { + + private final UserService userService; + + private final JwtTokenProvider jwtTokenProvider; + + @GetMapping("/join/{id}") + public ResponseEntity auth_success(@PathVariable("id") long id) { + System.out.println("/join/id 입니다."); + User user = userService.findUserById(id); + return ResponseEntity.ok(user); + } + + // 회원가입 + @PostMapping("/join") + public ResponseEntity join(@RequestBody JoinRequestDto requestDto) { + return ResponseEntity.ok(userService.join(requestDto)); + } + + // 로그인 + @GetMapping("/login/{id}") + public ResponseEntity login(@PathVariable("id") long id) { + User user = userService.findUserById(id); + + String token = jwtTokenProvider.createToken(user.getUemail(), user.getUroleType()); + return ResponseEntity.ok(token); + } + + // token 재발급 + @PostMapping("/refresh") + public String validateRefreshToken(@RequestHeader("REFRESH-TOKEN") String refreshToken) { + return ""; + } + + // 회원정보 수정 + @PatchMapping() + public User update(@RequestParam Long id, @RequestBody UserRequestDto requestDto) { + return userService.update(id, requestDto); + } + + // 이메일로 회원 조회 + @GetMapping("/user/email") + public User readByEmail(@RequestParam String email) { + return userService.findUserByEmail(email); + } + + // 전체 회원 조회 + @GetMapping("/user/list") + public List readAll() { + return userService.findAllUsers(); + } + + // 회원 삭제 + @DeleteMapping("/user") + public Long delete(@RequestParam Long id) { + return userService.delete(id); + } + + //Todo: Logout + + //Todo: find password +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/domain/entity/Timestamped.java b/spring/notinote/src/main/java/com/answer/notinote/User/domain/entity/Timestamped.java new file mode 100644 index 0000000..558962b --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/domain/entity/Timestamped.java @@ -0,0 +1,23 @@ +package com.answer.notinote.User.domain.entity; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +abstract class Timestamped { + + @CreatedDate + private LocalDateTime created_at; + + @LastModifiedDate + private LocalDateTime modified_at; + +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/domain/entity/User.java b/spring/notinote/src/main/java/com/answer/notinote/User/domain/entity/User.java new file mode 100644 index 0000000..a151083 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/domain/entity/User.java @@ -0,0 +1,65 @@ +package com.answer.notinote.User.domain.entity; + +import com.answer.notinote.auth.data.ProviderType; +import com.answer.notinote.auth.data.RoleType; +import com.answer.notinote.User.dto.UserRequestDto; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter @Setter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class User extends Timestamped { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column + private Long uid; + + @Column(length = 20) + private String ufirstname; + + @Column(length = 20) + private String ulastname; + + @Column(nullable = false, length = 50, unique = true) + private String uemail; + + @Column(length = 20) + private String ulanguage; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private ProviderType uproviderType; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, length = 20) + private RoleType uroleType; + + public User(UserRequestDto requestDto) { + this.ufirstname = requestDto.getFirstname(); + this.ulastname = requestDto.getLastname(); + this.uemail = requestDto.getEmail(); + } + + public User(com.answer.notinote.auth.data.dto.UserRequestDto requestDto) { + this.uemail = requestDto.getEmail(); + this.ufirstname = requestDto.getFirstname(); + this.ulastname = requestDto.getLastname(); + this.uproviderType = requestDto.getProviderType(); + this.uroleType = requestDto.getRoleType(); + } + + public String getFullname() { + return this.ufirstname + " " + this.ulastname; + } + + public void update(UserRequestDto requestDto) { + this.ufirstname = requestDto.getFirstname(); + this.ulastname = requestDto.getLastname(); + this.uemail = requestDto.getEmail(); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/domain/repository/UserRepository.java b/spring/notinote/src/main/java/com/answer/notinote/User/domain/repository/UserRepository.java new file mode 100644 index 0000000..d6e68c4 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/domain/repository/UserRepository.java @@ -0,0 +1,17 @@ +package com.answer.notinote.User.domain.repository; + +import com.answer.notinote.User.domain.entity.User; +import com.answer.notinote.auth.data.ProviderType; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import javax.swing.text.html.Option; +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUemail(String uemail); + + Optional findByUproviderTypeAndUemail(ProviderType uproviderType, String uemail); + boolean existsByUemail(String email); +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/dto/JoinRequestDto.java b/spring/notinote/src/main/java/com/answer/notinote/User/dto/JoinRequestDto.java new file mode 100644 index 0000000..f63cb56 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/dto/JoinRequestDto.java @@ -0,0 +1,10 @@ +package com.answer.notinote.User.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class JoinRequestDto { + Long id; + String language; +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/dto/LoginResponseDto.java b/spring/notinote/src/main/java/com/answer/notinote/User/dto/LoginResponseDto.java new file mode 100644 index 0000000..9233e7f --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/dto/LoginResponseDto.java @@ -0,0 +1,19 @@ +package com.answer.notinote.User.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@AllArgsConstructor +@Getter @Setter +public class LoginResponseDto { + private Long id; + private String firstname; + private String lastname; + private String email; + private String access_token; + private String refresh_token; + private List roles; +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/dto/TokenResponseDto.java b/spring/notinote/src/main/java/com/answer/notinote/User/dto/TokenResponseDto.java new file mode 100644 index 0000000..716ee17 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/dto/TokenResponseDto.java @@ -0,0 +1,12 @@ +package com.answer.notinote.User.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@AllArgsConstructor +@Getter @Setter +public class TokenResponseDto { + private String access_token; + private String refresh_token; +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/dto/UserRequestDto.java b/spring/notinote/src/main/java/com/answer/notinote/User/dto/UserRequestDto.java new file mode 100644 index 0000000..fc1b109 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/dto/UserRequestDto.java @@ -0,0 +1,11 @@ +package com.answer.notinote.User.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +public class UserRequestDto { + private String email; + private String firstname; + private String lastname; +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/service/UserService.java b/spring/notinote/src/main/java/com/answer/notinote/User/service/UserService.java new file mode 100644 index 0000000..1e58276 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/service/UserService.java @@ -0,0 +1,70 @@ +package com.answer.notinote.User.service; + +import com.answer.notinote.User.dto.JoinRequestDto; +import com.answer.notinote.auth.data.RoleType; +import com.answer.notinote.User.domain.entity.User; +import com.answer.notinote.User.domain.repository.UserRepository; +import com.answer.notinote.User.dto.UserRequestDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + @Transactional + public User join(JoinRequestDto requestDto) { + User user = userRepository.findById(requestDto.getId()).orElseThrow( + () -> new IllegalArgumentException("id가 존재하지 않습니다.") + ); + + if (user.getUroleType() == RoleType.GUEST) { + user.setUlanguage(requestDto.getLanguage()); + user.setUroleType(RoleType.USER); + userRepository.save(user); + return user; + } + else { + throw new IllegalArgumentException("구글 회원가입 전적이 존재하지 않습니다."); + } + } + + public User update(Long id, UserRequestDto requestDto) { + User user = userRepository.findById(id).orElseThrow( + () -> new IllegalArgumentException("id가 존재하지 않습니다.") + ); + user.update(requestDto); + + return user; + } + + public Long delete(Long id) { + User user = userRepository.findById(id).orElseThrow( + () -> new IllegalArgumentException("id가 존재하지 않습니다.") + ); + userRepository.delete(user); + + return id; + } + + public User findUserById(Long id) { + return userRepository.findById(id).orElseThrow( + () -> new IllegalArgumentException("ID가 존재하지 않습니다.") + ); + } + + public User findUserByEmail(String email) { + return userRepository.findByUemail(email).orElseThrow( + () -> new IllegalArgumentException("이메일이 존재하지 않습니다.") + ); + } + + public List findAllUsers() { + return userRepository.findAll(); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/util/ErrorMessage.java b/spring/notinote/src/main/java/com/answer/notinote/User/util/ErrorMessage.java new file mode 100644 index 0000000..67b1338 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/util/ErrorMessage.java @@ -0,0 +1,12 @@ +package com.answer.notinote.User.util; + +import lombok.AllArgsConstructor; +import java.util.Date; + +@AllArgsConstructor +public class ErrorMessage { + private int status; + private Date date; + private String message; + private String request; +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/util/advice/TokenControllerAdvice.java b/spring/notinote/src/main/java/com/answer/notinote/User/util/advice/TokenControllerAdvice.java new file mode 100644 index 0000000..357c9b3 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/util/advice/TokenControllerAdvice.java @@ -0,0 +1,25 @@ +package com.answer.notinote.User.util.advice; + +import com.answer.notinote.User.util.ErrorMessage; +import com.answer.notinote.User.util.exception.TokenRefreshException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; + +import java.util.Date; + +@RestControllerAdvice +public class TokenControllerAdvice { + @ExceptionHandler(value = TokenRefreshException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ErrorMessage handleTokenRefreshException(TokenRefreshException e, WebRequest request) { + return new ErrorMessage( + HttpStatus.FORBIDDEN.value(), + new Date(), + e.getMessage(), + request.getDescription(false) + ); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/User/util/exception/TokenRefreshException.java b/spring/notinote/src/main/java/com/answer/notinote/User/util/exception/TokenRefreshException.java new file mode 100644 index 0000000..f4b0f4f --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/User/util/exception/TokenRefreshException.java @@ -0,0 +1,13 @@ +package com.answer.notinote.User.util.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.FORBIDDEN) +public class TokenRefreshException extends RuntimeException{ + private static final long version = 1L; + + public TokenRefreshException(String token, String message) { + super(String.format("Failed for [%s]: %s", token, message)); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/data/ProviderType.java b/spring/notinote/src/main/java/com/answer/notinote/auth/data/ProviderType.java new file mode 100644 index 0000000..94efe36 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/data/ProviderType.java @@ -0,0 +1,22 @@ +package com.answer.notinote.auth.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpMethod; + +/** + * 제공하는 SNS 로그인 enum 클래스 + */ +@Getter +@AllArgsConstructor +public enum ProviderType { + GOOGLE( + "google", + "https://www.googleapis.com/oauth2/v3/userinfo", + HttpMethod.GET + ); + + private String socialName; + private String userInfoUrl; + private HttpMethod method; +} \ No newline at end of file diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/data/RoleType.java b/spring/notinote/src/main/java/com/answer/notinote/auth/data/RoleType.java new file mode 100644 index 0000000..35a874d --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/data/RoleType.java @@ -0,0 +1,17 @@ +package com.answer.notinote.auth.data; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 유저 권한 enum 클래스 + */ +@Getter +@AllArgsConstructor +public enum RoleType { + USER("ROLE_USER"), + GUEST("ROLE_GUEST"), + ADMIN("ROLE_ADMIN"); + + private String grantedAuthority; +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/data/dto/UserRequestDto.java b/spring/notinote/src/main/java/com/answer/notinote/auth/data/dto/UserRequestDto.java new file mode 100644 index 0000000..8bff008 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/data/dto/UserRequestDto.java @@ -0,0 +1,18 @@ +package com.answer.notinote.auth.data.dto; + +import com.answer.notinote.auth.data.ProviderType; +import com.answer.notinote.auth.data.RoleType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class UserRequestDto { + private String email; + private String firstname; + private String lastname; + private ProviderType providerType; + private RoleType roleType; +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/data/dto/UserSocialResponseDto.java b/spring/notinote/src/main/java/com/answer/notinote/auth/data/dto/UserSocialResponseDto.java new file mode 100644 index 0000000..fd5555a --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/data/dto/UserSocialResponseDto.java @@ -0,0 +1,13 @@ +package com.answer.notinote.auth.data.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Getter @Setter +@Builder +public class UserSocialResponseDto { + String email; + String firstname; + String lastname; +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/filter/JwtAuthenticationFilter.java b/spring/notinote/src/main/java/com/answer/notinote/auth/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..a681d07 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/filter/JwtAuthenticationFilter.java @@ -0,0 +1,30 @@ +package com.answer.notinote.auth.filter; + +import com.answer.notinote.auth.token.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends GenericFilterBean { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + String token = jwtTokenProvider.resolveToken((HttpServletRequest) request); + if (token != null && jwtTokenProvider.validateToekn(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + chain.doFilter(request, response); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/filter/OAuth2AccessTokenAuthenticationFilter.java b/spring/notinote/src/main/java/com/answer/notinote/auth/filter/OAuth2AccessTokenAuthenticationFilter.java new file mode 100644 index 0000000..de37c4d --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/filter/OAuth2AccessTokenAuthenticationFilter.java @@ -0,0 +1,70 @@ +package com.answer.notinote.auth.filter; + +import com.answer.notinote.auth.token.AccessTokenAuthenticationProvider; +import com.answer.notinote.auth.data.ProviderType; +import com.answer.notinote.auth.token.AccessTokenProviderTypeToken; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Arrays; + +/** + * 로그인 요청 헤더의 Access Token을 식별하는 헤더 + */ +@Component +public class OAuth2AccessTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + + private static final String DEFAULT_OAUTH2_LOGIN_REQUEST_URL_PREFIX = "/login/oauth2"; + private static final String HTTP_METHOD = "GET"; + private static final String ACCESS_TOKEN_HEADER_NAME = "Authorization"; //AccessToken 해더 + + private static final AntPathRequestMatcher DEFAULT_OAUTH2_LOGIN_PATH_REQUEST_MATCHER = + new AntPathRequestMatcher(DEFAULT_OAUTH2_LOGIN_REQUEST_URL_PREFIX +"*", HTTP_METHOD); //=> /oauth2/login/* 의 GET 매핑 + + public OAuth2AccessTokenAuthenticationFilter(AccessTokenAuthenticationProvider accessTokenAuthenticationProvider, + AuthenticationSuccessHandler authenticationSuccessHandler, + AuthenticationFailureHandler authenticationFailureHandler) { + super(DEFAULT_OAUTH2_LOGIN_PATH_REQUEST_MATCHER); + + this.setAuthenticationManager(new ProviderManager(accessTokenAuthenticationProvider)); + this.setAuthenticationSuccessHandler(authenticationSuccessHandler); + this.setAuthenticationFailureHandler(authenticationFailureHandler); + + } + + /** + * 로그인 요청이 들어오면 가장 먼저 작동되는 메소드입니다. + * AuthenticationManager.authenticate()를 호출해 인증을 진행합니다. + * @param request + * @param response + * @return + * @throws AuthenticationException + */ + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + // SNS 로그인 분류 추출 + //ProviderType providerType = extractProviderType(request); + String accessToken = request.getHeader(ACCESS_TOKEN_HEADER_NAME); + + //AuthenticationManager에 인증 요청 전송 + return this.getAuthenticationManager().authenticate(new AccessTokenProviderTypeToken(accessToken, ProviderType.GOOGLE)); + } + + + private ProviderType extractProviderType(HttpServletRequest request) { + return Arrays.stream(ProviderType.values()) + .filter(providerType -> + providerType.getSocialName() + .equals(request.getRequestURI().substring(DEFAULT_OAUTH2_LOGIN_REQUEST_URL_PREFIX.length()))) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("잘못된 URL 주소입니다")); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/handler/OAuth2LoginFailureHandler.java b/spring/notinote/src/main/java/com/answer/notinote/auth/handler/OAuth2LoginFailureHandler.java new file mode 100644 index 0000000..098296d --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/handler/OAuth2LoginFailureHandler.java @@ -0,0 +1,22 @@ +package com.answer.notinote.auth.handler; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class OAuth2LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + System.out.println("로그인 실패! : " + exception.getMessage()); + response.sendRedirect("/"); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/handler/OAuth2LoginSuccessHandler.java b/spring/notinote/src/main/java/com/answer/notinote/auth/handler/OAuth2LoginSuccessHandler.java new file mode 100644 index 0000000..173a1e2 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/handler/OAuth2LoginSuccessHandler.java @@ -0,0 +1,43 @@ +package com.answer.notinote.auth.handler; + +import com.answer.notinote.User.domain.entity.User; +import com.answer.notinote.User.domain.repository.UserRepository; +import com.answer.notinote.auth.data.RoleType; +import com.answer.notinote.auth.userdetails.CustomUserDetails; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { + + private final UserRepository userRepository; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + System.out.println("로그인 성공 !"); + + CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); + User user = userRepository.findByUemail(userDetails.getEmail()).orElseThrow( + () -> new IllegalArgumentException("이메일이 존재하지 않습니다.") + ); + + if (authentication.getAuthorities().stream().anyMatch(s -> s.getAuthority().equals(RoleType.GUEST.getGrantedAuthority()))) { + System.out.println("회원가입으로 이동합니다."); + response.sendRedirect("/join/" + user.getUid()); + return; + } + else { + System.out.println("회원가입한 사용자입니다."); + response.sendRedirect("/login/" + user.getUid()); + } + + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/repository/RefreshTokenRepository.java b/spring/notinote/src/main/java/com/answer/notinote/auth/repository/RefreshTokenRepository.java new file mode 100644 index 0000000..abf669e --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/repository/RefreshTokenRepository.java @@ -0,0 +1,17 @@ +package com.answer.notinote.auth.repository; + +import com.answer.notinote.auth.token.RefreshToken; +import com.answer.notinote.User.domain.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RefreshTokenRepository extends JpaRepository{ + @Override + Optional findById(Long id); + Optional findByToken(String token); + Optional findByUserEmailAndToken(String userId, String token); + void deleteByUserEmail(String userEmail); +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/service/CustomUserDetailsService.java b/spring/notinote/src/main/java/com/answer/notinote/auth/service/CustomUserDetailsService.java new file mode 100644 index 0000000..3a2d01b --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/service/CustomUserDetailsService.java @@ -0,0 +1,24 @@ +package com.answer.notinote.auth.service; + +import com.answer.notinote.auth.userdetails.CustomUserDetails; +import com.answer.notinote.User.domain.entity.User; +import com.answer.notinote.User.domain.repository.UserRepository; +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 UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + User user = userRepository.findByUemail(email) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다.")); + return CustomUserDetails.create(user); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/service/LoadUserService.java b/spring/notinote/src/main/java/com/answer/notinote/auth/service/LoadUserService.java new file mode 100644 index 0000000..4fd3b0b --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/service/LoadUserService.java @@ -0,0 +1,52 @@ +package com.answer.notinote.auth.service; + +import com.answer.notinote.auth.data.dto.UserSocialResponseDto; +import com.answer.notinote.auth.strategy.GoogleLoadStrategy; +import com.answer.notinote.auth.strategy.ProviderLoadStrategy; +import com.answer.notinote.auth.data.ProviderType; +import com.answer.notinote.auth.token.AccessTokenProviderTypeToken; +import com.answer.notinote.auth.userdetails.CustomUserDetails; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +public class LoadUserService { + + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * 해당 ProviderType의 url에 요청을 보내 유저 정보 조회 + * @param authentication + * @return customUserDetails + */ + public CustomUserDetails getOAuth2UserDetails(AccessTokenProviderTypeToken authentication) { + ProviderType providerType = authentication.getProviderType(); + ProviderLoadStrategy providerLoadStrategy = getProviderLoadStrategy(providerType); + + UserSocialResponseDto socialEntity = providerLoadStrategy.getSocialEntity(authentication.getAccessToken()); + + if (socialEntity == null) { + throw new IllegalArgumentException("액세스 토큰이 만료되었습니다."); + } + + return CustomUserDetails.builder() + .email(socialEntity.getEmail()) + .firstname(socialEntity.getFirstname()) + .lastname(socialEntity.getLastname()) + .providerType(providerType) + .build(); + } + + /** + * @param providerType + * @return ProviderLoadStrategy + */ + private ProviderLoadStrategy getProviderLoadStrategy(ProviderType providerType) { + switch (providerType) { + case GOOGLE : return new GoogleLoadStrategy(); + default : throw new IllegalArgumentException("지원하지 않는 로그인 형식입니다"); + } + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/service/RefreshTokenService.java b/spring/notinote/src/main/java/com/answer/notinote/auth/service/RefreshTokenService.java new file mode 100644 index 0000000..2b69086 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/service/RefreshTokenService.java @@ -0,0 +1,36 @@ +package com.answer.notinote.auth.service; + +import com.answer.notinote.auth.token.RefreshToken; +import com.answer.notinote.User.domain.entity.User; +import com.answer.notinote.auth.repository.RefreshTokenRepository; +import com.answer.notinote.User.domain.repository.UserRepository; +import com.answer.notinote.User.util.exception.TokenRefreshException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.time.Instant; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + + private final RefreshTokenRepository refreshTokenRepository; + private final UserRepository userRepository; + + public RefreshToken findByToken(String token) { + return refreshTokenRepository.findByToken(token).orElseThrow( + () -> new IllegalArgumentException("token이 존재하지 않습니다.") + ); + } + + @Transactional + public Long deleteByUid(Long uid) { + User user = userRepository.findById(uid).orElseThrow( + () -> new IllegalArgumentException("유저 ID가 존재하지 않습니다.") + ); + refreshTokenRepository.deleteByUserEmail(user.getUemail()); + return user.getUid(); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/strategy/GoogleLoadStrategy.java b/spring/notinote/src/main/java/com/answer/notinote/auth/strategy/GoogleLoadStrategy.java new file mode 100644 index 0000000..6371c73 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/strategy/GoogleLoadStrategy.java @@ -0,0 +1,30 @@ +package com.answer.notinote.auth.strategy; + +import com.answer.notinote.auth.data.ProviderType; +import com.answer.notinote.auth.data.dto.UserSocialResponseDto; +import org.springframework.http.HttpEntity; +import org.springframework.http.ResponseEntity; + +import java.util.Map; + +public class GoogleLoadStrategy extends ProviderLoadStrategy{ + @Override + protected UserSocialResponseDto sendRequestToSocialSite(HttpEntity request) { + try { + ResponseEntity> response = restTemplate.exchange(ProviderType.GOOGLE.getUserInfoUrl(), + ProviderType.GOOGLE.getMethod(), + request, + RESPONSE_TYPE); + + return UserSocialResponseDto.builder() + .email(response.getBody().get("email").toString()) + .firstname(response.getBody().get("given_name").toString()) + .lastname(response.getBody().get("family_name").toString()) + .build(); + + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/strategy/ProviderLoadStrategy.java b/spring/notinote/src/main/java/com/answer/notinote/auth/strategy/ProviderLoadStrategy.java new file mode 100644 index 0000000..9c4f422 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/strategy/ProviderLoadStrategy.java @@ -0,0 +1,40 @@ +package com.answer.notinote.auth.strategy; + +import com.answer.notinote.auth.data.dto.UserSocialResponseDto; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; +import java.util.Map; + +/** + * 소셜 사이트에 회원 정보 요청 전송 + */ +public abstract class ProviderLoadStrategy { + + ParameterizedTypeReference> RESPONSE_TYPE = new ParameterizedTypeReference<>() {}; + + protected final RestTemplate restTemplate = new RestTemplate(); + + public UserSocialResponseDto getSocialEntity(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + setHeaders(accessToken, headers); + MultiValueMap params = new LinkedMultiValueMap<>(); + + HttpEntity> request = new HttpEntity<>(params, headers); + return sendRequestToSocialSite(request); + } + + protected abstract UserSocialResponseDto sendRequestToSocialSite(HttpEntity request); + + public void setHeaders(String accessToken, HttpHeaders headers) { + headers.set("Authorization", "Bearer " + accessToken); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/token/AccessTokenAuthenticationProvider.java b/spring/notinote/src/main/java/com/answer/notinote/auth/token/AccessTokenAuthenticationProvider.java new file mode 100644 index 0000000..4e408c2 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/token/AccessTokenAuthenticationProvider.java @@ -0,0 +1,52 @@ +package com.answer.notinote.auth.token; + +import com.answer.notinote.auth.data.RoleType; +import com.answer.notinote.auth.data.dto.UserRequestDto; +import com.answer.notinote.auth.service.LoadUserService; +import com.answer.notinote.auth.userdetails.CustomUserDetails; +import com.answer.notinote.User.domain.entity.User; +import com.answer.notinote.User.domain.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +/** + * AccessToken의 인증을 진행하는 클래스 + */ +@RequiredArgsConstructor +@Component +public class AccessTokenAuthenticationProvider implements AuthenticationProvider { + + private final LoadUserService loadUserService; + private final UserRepository userRepository; + + /** + * 전달받은 access token으로 회원 정보 조회 + * @param authentication + * @return + * @throws AuthenticationException + */ + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + CustomUserDetails oAuth2User = loadUserService.getOAuth2UserDetails((AccessTokenProviderTypeToken) authentication); + + // DB에서 회원 조회 (없으면 생성) + User user = saveOrGet(oAuth2User); + oAuth2User.setRoles(user.getUroleType().name()); + + return new AccessTokenProviderTypeToken(oAuth2User, oAuth2User.getAuthorities()); + } + + private User saveOrGet(CustomUserDetails oAuth2User) { + return userRepository.findByUemail(oAuth2User.getEmail()) + .orElseGet(() -> userRepository.save(new User(new UserRequestDto(oAuth2User.getEmail(), oAuth2User.getFirstname(), oAuth2User.getLastname(), oAuth2User.getProviderType(), RoleType.GUEST)))); + } + + @Override + public boolean supports(Class authentication) { + return AccessTokenProviderTypeToken.class.isAssignableFrom(authentication); + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/token/AccessTokenProviderTypeToken.java b/spring/notinote/src/main/java/com/answer/notinote/auth/token/AccessTokenProviderTypeToken.java new file mode 100644 index 0000000..0729b13 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/token/AccessTokenProviderTypeToken.java @@ -0,0 +1,41 @@ +package com.answer.notinote.auth.token; + +import com.answer.notinote.auth.data.ProviderType; +import lombok.Builder; +import lombok.Getter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +@Getter +public class AccessTokenProviderTypeToken extends AbstractAuthenticationToken { + + private Object principal; // OAuth2UserDetails + private String accessToken; + private ProviderType providerType; + + @Builder + public AccessTokenProviderTypeToken(Object principal, Collection authorities) { + super(authorities); + this.principal = principal; + super.setAuthenticated(true); + } + + public AccessTokenProviderTypeToken(String accessToken, ProviderType providerType) { + super(null); + this.accessToken = accessToken; + this.providerType = providerType; + setAuthenticated(false); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return this.principal; + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/token/JwtToken.java b/spring/notinote/src/main/java/com/answer/notinote/auth/token/JwtToken.java new file mode 100644 index 0000000..788682e --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/token/JwtToken.java @@ -0,0 +1,86 @@ +package com.answer.notinote.auth.token; + +import io.jsonwebtoken.*; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.security.Key; +import java.util.Date; + +@Slf4j +@RequiredArgsConstructor +public class JwtToken { + + @Getter + private final String token; + private final Key key; + + private static final String AUTHORITIES_KEY = "role"; + + JwtToken(String id, Date expiry, Key key) { + this.key = key; + this.token = createToken(id, expiry); + } + + JwtToken(String id, String role, Date expiry, Key key) { + this.key = key; + this.token = createToken(id, role, expiry); + } + + private String createToken(String id, Date expiry) { + return Jwts.builder() + .setSubject(id) + .signWith(key, SignatureAlgorithm.HS256) + .setExpiration(expiry) + .compact(); + } + + private String createToken(String id, String role, Date expiry) { + return Jwts.builder() + .setSubject(id) + .claim(AUTHORITIES_KEY, role) + .signWith(key, SignatureAlgorithm.HS256) + .setExpiration(expiry) + .compact(); + } + + public boolean validate() { + return this.getTokenClaims() != null; + } + + public Claims getTokenClaims() { + try { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (SecurityException e) { + log.info("Invalid JWT signature."); + } catch (MalformedJwtException e) { + log.info("Invalid JWT token."); + } catch (ExpiredJwtException e) { + log.info("Expired JWT token."); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT token."); + } catch (IllegalArgumentException e) { + log.info("JWT token compact of handler are invalid."); + } + return null; + } + + public Claims getExpiredTokenClaims() { + try { + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException e) { + log.info("Expired JWT token."); + return e.getClaims(); + } + return null; + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/token/JwtTokenProvider.java b/spring/notinote/src/main/java/com/answer/notinote/auth/token/JwtTokenProvider.java new file mode 100644 index 0000000..1a4bca9 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/token/JwtTokenProvider.java @@ -0,0 +1,73 @@ +package com.answer.notinote.auth.token; + +import com.answer.notinote.auth.data.RoleType; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +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 javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import java.util.Base64; +import java.util.Date; +import java.util.List; + +@RequiredArgsConstructor +@Component +public class JwtTokenProvider { + private String secretKey = "notinotenotinotenotinotenotinotenotinotenotinote"; + + // 토큰 유효시간 30분 + private long tokenValidTime = 30 * 60 * 1000L; + private final UserDetailsService userDetailsService; + + @PostConstruct + protected void init() { + secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); + } + + // jwt 토큰 생성 + public String createToken(String email, RoleType roles) { + Claims claims = Jwts.claims().setSubject(email); + claims.put("roles", roles); + Date now = new Date(); + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + tokenValidTime)) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + // 토큰에서 인증 정보 조회 + public Authentication getAuthentication(String token) { + UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserEmail(token)); + return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); + } + + // 토큰에서 회원 정보 추출 + public String getUserEmail(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + // Header에서 token 값 불러오기 + public String resolveToken(HttpServletRequest request) { + return request.getHeader("JWT-TOKEN"); + } + + // 토큰의 유효성 & 만료일자 확인 + public boolean validateToekn(String token) { + try { + Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return !claims.getBody().getExpiration().before(new Date()); + } catch (Exception e) { + return false; + } + } +} \ No newline at end of file diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/token/RefreshToken.java b/spring/notinote/src/main/java/com/answer/notinote/auth/token/RefreshToken.java new file mode 100644 index 0000000..afa27a5 --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/token/RefreshToken.java @@ -0,0 +1,30 @@ +package com.answer.notinote.auth.token; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class RefreshToken { + @JsonIgnore + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(nullable = false, length = 30, unique = true) + private String userEmail; + + @Column(nullable = false, unique = true) + private String token; + + public RefreshToken(String userEmail, String token) { + this.userEmail = userEmail; + this.token = token; + } +} diff --git a/spring/notinote/src/main/java/com/answer/notinote/auth/userdetails/CustomUserDetails.java b/spring/notinote/src/main/java/com/answer/notinote/auth/userdetails/CustomUserDetails.java new file mode 100644 index 0000000..f46d4fb --- /dev/null +++ b/spring/notinote/src/main/java/com/answer/notinote/auth/userdetails/CustomUserDetails.java @@ -0,0 +1,80 @@ +package com.answer.notinote.auth.userdetails; + +import com.answer.notinote.User.domain.entity.User; +import com.answer.notinote.auth.data.ProviderType; +import io.jsonwebtoken.lang.Assert; +import lombok.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.*; + +/** + * 스프링 시큐리티 내부에서 사용되는 User Entity + */ +@Getter @Setter +@Builder +@AllArgsConstructor +public class CustomUserDetails implements UserDetails { + private String email; + private String firstname; + private String lastname; + private ProviderType providerType; + private Set authorities; + + public CustomUserDetails(String firstname, String lastname, String email, ProviderType providerType) { + this.firstname = firstname; + this.lastname = lastname; + this.email = email; + this.providerType = providerType; + } + + public static UserDetails create(User user) { + return new CustomUserDetails(user.getUfirstname(), user.getUlastname(), user.getUemail(), user.getUproviderType()); + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return this.firstname + this.lastname; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + public void setRoles(String... roles) { + List authorities = new ArrayList<>(roles.length); + for (String role : roles) { + Assert.isTrue(!role.startsWith("ROLE_"), role + " cannot start with ROLE_ (it is automatically add)"); + authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); + } + this.authorities = Set.copyOf(authorities); + } +} diff --git a/spring/notinote/src/main/resources/static/index.html b/spring/notinote/src/main/resources/static/index.html new file mode 100644 index 0000000..8e6ac37 --- /dev/null +++ b/spring/notinote/src/main/resources/static/index.html @@ -0,0 +1,19 @@ + + + + Insert title here + + + + + + + \ No newline at end of file