diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 63529fa..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 63e9001..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index 712ab9d..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 0758221..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/sonarlint.xml b/.idea/sonarlint.xml deleted file mode 100644 index 20c6282..0000000 --- a/.idea/sonarlint.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/1/f/1f1763f358c257ea3515417352b82b0165647ad1 b/.idea/sonarlint/issuestore/1/f/1f1763f358c257ea3515417352b82b0165647ad1 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/3/9/39cad08c773d7ff3afa8cab80e1be505f01dcd62 b/.idea/sonarlint/issuestore/3/9/39cad08c773d7ff3afa8cab80e1be505f01dcd62 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/4/2/42a0fcc2a3cd24d3b748eb564abf71c4902524fc b/.idea/sonarlint/issuestore/4/2/42a0fcc2a3cd24d3b748eb564abf71c4902524fc deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/4/4/442292b8a7efeabbe4cc176709b833b1792140ec b/.idea/sonarlint/issuestore/4/4/442292b8a7efeabbe4cc176709b833b1792140ec deleted file mode 100644 index 714ce3e..0000000 --- a/.idea/sonarlint/issuestore/4/4/442292b8a7efeabbe4cc176709b833b1792140ec +++ /dev/null @@ -1,2 +0,0 @@ - -exml:S125"Remove this commented out code.(뜐81J$4a92d10d-fe36-4fe1-ba06-fa48a4226181 \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/5/d/5d837c4ed85ecaaf932c506e80ff5d7b9f3d590d b/.idea/sonarlint/issuestore/5/d/5d837c4ed85ecaaf932c506e80ff5d7b9f3d590d deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/7/e/7e85a541f9d0eed2e9f7a135b9ddde3c8491c114 b/.idea/sonarlint/issuestore/7/e/7e85a541f9d0eed2e9f7a135b9ddde3c8491c114 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/9/9/99ff850f1b0eb7d304829d82185c5e52f6b6ee84 b/.idea/sonarlint/issuestore/9/9/99ff850f1b0eb7d304829d82185c5e52f6b6ee84 deleted file mode 100644 index cc75114..0000000 --- a/.idea/sonarlint/issuestore/9/9/99ff850f1b0eb7d304829d82185c5e52f6b6ee84 +++ /dev/null @@ -1,4 +0,0 @@ - -O -java:S2699 -"-Add at least one assertion to this test case.(81 \ No newline at end of file diff --git a/.idea/sonarlint/issuestore/c/7/c709f6995f23dae0da365bff70068f8288fa89e0 b/.idea/sonarlint/issuestore/c/7/c709f6995f23dae0da365bff70068f8288fa89e0 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/issuestore/index.pb b/.idea/sonarlint/issuestore/index.pb deleted file mode 100644 index 8307d9a..0000000 --- a/.idea/sonarlint/issuestore/index.pb +++ /dev/null @@ -1,17 +0,0 @@ - -K -.github/workflows/maven.yml,c\7\c709f6995f23dae0da365bff70068f8288fa89e0 -~ -Nsrc/test/java/com/t3t/authenticationapi/AuthenticationApiApplicationTests.java,9\9\99ff850f1b0eb7d304829d82185c5e52f6b6ee84 -7 -pom.xml,4\4\442292b8a7efeabbe4cc176709b833b1792140ec -r -Bsrc/main/java/com/t3t/authenticationapi/index/IndexController.java,3\9\39cad08c773d7ff3afa8cab80e1be505f01dcd62 -y -Isrc/main/java/com/t3t/authenticationapi/AuthenticationApiApplication.java,4\2\42a0fcc2a3cd24d3b748eb564abf71c4902524fc -8 -mvnw.cmd,1\f\1f1763f358c257ea3515417352b82b0165647ad1 -4 -mvnw,5\d\5d837c4ed85ecaaf932c506e80ff5d7b9f3d590d -F -authentication-api.iml,7\e\7e85a541f9d0eed2e9f7a135b9ddde3c8491c114 \ No newline at end of file diff --git a/.idea/sonarlint/securityhotspotstore/1/f/1f1763f358c257ea3515417352b82b0165647ad1 b/.idea/sonarlint/securityhotspotstore/1/f/1f1763f358c257ea3515417352b82b0165647ad1 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/securityhotspotstore/3/9/39cad08c773d7ff3afa8cab80e1be505f01dcd62 b/.idea/sonarlint/securityhotspotstore/3/9/39cad08c773d7ff3afa8cab80e1be505f01dcd62 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/securityhotspotstore/4/2/42a0fcc2a3cd24d3b748eb564abf71c4902524fc b/.idea/sonarlint/securityhotspotstore/4/2/42a0fcc2a3cd24d3b748eb564abf71c4902524fc deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/securityhotspotstore/4/4/442292b8a7efeabbe4cc176709b833b1792140ec b/.idea/sonarlint/securityhotspotstore/4/4/442292b8a7efeabbe4cc176709b833b1792140ec deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/securityhotspotstore/5/d/5d837c4ed85ecaaf932c506e80ff5d7b9f3d590d b/.idea/sonarlint/securityhotspotstore/5/d/5d837c4ed85ecaaf932c506e80ff5d7b9f3d590d deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/securityhotspotstore/7/e/7e85a541f9d0eed2e9f7a135b9ddde3c8491c114 b/.idea/sonarlint/securityhotspotstore/7/e/7e85a541f9d0eed2e9f7a135b9ddde3c8491c114 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/securityhotspotstore/9/9/99ff850f1b0eb7d304829d82185c5e52f6b6ee84 b/.idea/sonarlint/securityhotspotstore/9/9/99ff850f1b0eb7d304829d82185c5e52f6b6ee84 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/securityhotspotstore/c/7/c709f6995f23dae0da365bff70068f8288fa89e0 b/.idea/sonarlint/securityhotspotstore/c/7/c709f6995f23dae0da365bff70068f8288fa89e0 deleted file mode 100644 index e69de29..0000000 diff --git a/.idea/sonarlint/securityhotspotstore/index.pb b/.idea/sonarlint/securityhotspotstore/index.pb deleted file mode 100644 index 496b36c..0000000 --- a/.idea/sonarlint/securityhotspotstore/index.pb +++ /dev/null @@ -1,17 +0,0 @@ - -K -.github/workflows/maven.yml,c\7\c709f6995f23dae0da365bff70068f8288fa89e0 -~ -Nsrc/test/java/com/t3t/authenticationapi/AuthenticationApiApplicationTests.java,9\9\99ff850f1b0eb7d304829d82185c5e52f6b6ee84 -r -Bsrc/main/java/com/t3t/authenticationapi/index/IndexController.java,3\9\39cad08c773d7ff3afa8cab80e1be505f01dcd62 -y -Isrc/main/java/com/t3t/authenticationapi/AuthenticationApiApplication.java,4\2\42a0fcc2a3cd24d3b748eb564abf71c4902524fc -7 -pom.xml,4\4\442292b8a7efeabbe4cc176709b833b1792140ec -8 -mvnw.cmd,1\f\1f1763f358c257ea3515417352b82b0165647ad1 -4 -mvnw,5\d\5d837c4ed85ecaaf932c506e80ff5d7b9f3d590d -F -authentication-api.iml,7\e\7e85a541f9d0eed2e9f7a135b9ddde3c8491c114 \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/HELP.md b/HELP.md deleted file mode 100644 index a999a81..0000000 --- a/HELP.md +++ /dev/null @@ -1,29 +0,0 @@ -# Read Me First -The following was discovered as part of building this project: - -* The original package name 'com.t3t.authentication-api' is invalid and this project uses 'com.t3t.authenticationapi' instead. - -# Getting Started - -### Reference Documentation -For further reference, please consider the following sections: - -* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) -* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.7.18/maven-plugin/reference/html/) -* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.7.18/maven-plugin/reference/html/#build-image) -* [Thymeleaf](https://docs.spring.io/spring-boot/docs/2.7.18/reference/htmlsingle/#web.servlet.spring-mvc.template-engines) -* [Spring Data JPA](https://docs.spring.io/spring-boot/docs/2.7.18/reference/htmlsingle/#data.sql.jpa-and-spring-data) -* [Spring Web](https://docs.spring.io/spring-boot/docs/2.7.18/reference/htmlsingle/#web) -* [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/2.7.18/reference/htmlsingle/#using.devtools) -* [Spring REST Docs](https://docs.spring.io/spring-restdocs/docs/current/reference/html5/) - -### Guides -The following guides illustrate how to use some features concretely: - -* [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/) -* [Handling Form Submission](https://spring.io/guides/gs/handling-form-submission/) -* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) -* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) -* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) -* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) - diff --git a/authentication-api.iml b/authentication-api.iml deleted file mode 100644 index 6f62642..0000000 --- a/authentication-api.iml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index fb42084..f6631ce 100644 --- a/pom.xml +++ b/pom.xml @@ -17,19 +17,29 @@ 11 - org.springframework.boot - spring-boot-starter-thymeleaf + spring-boot-starter-data-jpa org.springframework.boot spring-boot-starter-web - + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + org.springframework.boot spring-boot-devtools @@ -56,6 +66,29 @@ spring-restdocs-mockmvc test + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-data-redis + + + com.h2database + h2 + test + + + org.json + json + 20210307 + + + io.lettuce + lettuce-core + + diff --git a/src/main/java/com/t3t/authenticationapi/AuthenticationApiApplication.java b/src/main/java/com/t3t/authenticationapi/AuthenticationApiApplication.java index b73ca0a..87870f7 100644 --- a/src/main/java/com/t3t/authenticationapi/AuthenticationApiApplication.java +++ b/src/main/java/com/t3t/authenticationapi/AuthenticationApiApplication.java @@ -4,6 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication +//@EnableRedisHttpSession public class AuthenticationApiApplication { public static void main(String[] args) { diff --git a/src/main/java/com/t3t/authenticationapi/account/auth/CustomUserDetails.java b/src/main/java/com/t3t/authenticationapi/account/auth/CustomUserDetails.java new file mode 100644 index 0000000..2fba28e --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/auth/CustomUserDetails.java @@ -0,0 +1,64 @@ +package com.t3t.authenticationapi.account.auth; + +import com.t3t.authenticationapi.account.dto.UserEntity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; + +public class CustomUserDetails implements UserDetails { + private final UserEntity userEntity; + + public CustomUserDetails(UserEntity userEntity) { + this.userEntity = userEntity; + } + + @Override + public Collection getAuthorities() { + Collection collection = new ArrayList<>(); + + collection.add(new GrantedAuthority() { + @Override + public String getAuthority() { + return userEntity.getRole(); + } + }); + + return collection; + } + + @Override + public String getPassword() { + return userEntity.getPassword(); + } + + @Override + public String getUsername() { + return userEntity.getUsername(); + } + + public String getUserId(){ + return userEntity.getUserId(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/common/GlobalExceptionHandler.java b/src/main/java/com/t3t/authenticationapi/account/common/GlobalExceptionHandler.java new file mode 100644 index 0000000..fe2c981 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/common/GlobalExceptionHandler.java @@ -0,0 +1,34 @@ +package com.t3t.authenticationapi.account.common; + +import com.t3t.authenticationapi.account.exception.CookieNotExistException; +import com.t3t.authenticationapi.account.exception.TokenAlreadyExistsException; +import com.t3t.authenticationapi.account.exception.TokenHasExpiredException; +import com.t3t.authenticationapi.account.exception.TokenNotExistsException; +import com.t3t.authenticationapi.model.response.BaseResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(TokenNotExistsException.class) + public ResponseEntity> handleTokenNotExistsException(TokenNotExistsException tokenNotExistsException){ + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new BaseResponse().message(tokenNotExistsException.getMessage())); + } + + @ExceptionHandler(TokenHasExpiredException.class) + public ResponseEntity> handleTokenHasExpiredException(TokenHasExpiredException tokenHasExpiredException){ + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new BaseResponse().message(tokenHasExpiredException.getMessage())); + } + + @ExceptionHandler(TokenAlreadyExistsException.class) + public ResponseEntity> handleTokenAlreadyExistsException(TokenAlreadyExistsException tokenAlreadyExistsException){ + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new BaseResponse().message(tokenAlreadyExistsException.getMessage())); + } + + @ExceptionHandler(CookieNotExistException.class) + public ResponseEntity> handleCookieNotExistsException(CookieNotExistException cookieNotExistException){ + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new BaseResponse().message(cookieNotExistException.getMessage())); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/component/JWTUtils.java b/src/main/java/com/t3t/authenticationapi/account/component/JWTUtils.java new file mode 100644 index 0000000..6423db8 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/component/JWTUtils.java @@ -0,0 +1,67 @@ +package com.t3t.authenticationapi.account.component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +@Component +public class JWTUtils { + private Key key; + public JWTUtils(@Value("${spring.security.key}") String secret) { + byte[] byteSecretKEy = Decoders.BASE64.decode(secret); + key = Keys.hmacShaKeyFor(byteSecretKEy); + } + + public String getUserName(String token){ + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().get("username", String.class); + } + + public String getRole(String token){ + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().get("role", String.class); + } + + public String getCategory(String token){ + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().get("category", String.class); + } + + public String getUUID(String token){ + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().get("uuid", String.class); + } + // 만료되었으면 true, 아니면 false + public Boolean isExpired(String token){ + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getExpiration().before(new Date()); + } + + public String createJwt(String category, String id, String role, String uuid, Long expiredMs){ + Claims claims = Jwts.claims(); + return Jwts.builder() + .claim("username", id) + .claim("role", role) + .claim("category", category) + .claim("uuid", uuid) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiredMs)) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + public Boolean checkReIssue(String token){ + Date expiration = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getExpiration(); + LocalDateTime localDateTime = LocalDateTime.ofInstant(expiration.toInstant(), ZoneId.systemDefault()); + Duration duration = Duration.between(LocalDateTime.now(), localDateTime); + + long diffSec = Math.abs(duration.toSeconds()); + + return diffSec > 0 && diffSec <= 300; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/config/RedisConfig.java b/src/main/java/com/t3t/authenticationapi/account/config/RedisConfig.java new file mode 100644 index 0000000..da40586 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/config/RedisConfig.java @@ -0,0 +1,52 @@ +package com.t3t.authenticationapi.account.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + + +@Configuration +@EnableRedisRepositories +public class RedisConfig { + @Value("${spring.redis.host}") + private String host; + + @Value("${spring.redis.port}") + private int port; + + @Value("${spring.redis.database}") + private int database; + + @Value("${spring.redis.password}") + private String password; + + /** + * RedisServer에 연결을 생성하는데 사용되는 클래스 + * getConnection() 호출될 때 마다 새로운 LettuceConnection 생성 + * Thread-safe 하다 + * 동기, 비동기, 리액티브 api 모두 가능 + */ + + @Bean + public RedisConnectionFactory redisConnectionFactory(){ + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(host, port); + configuration.setPassword(password); + configuration.setDatabase(database); + return new LettuceConnectionFactory(configuration); + } + + @Bean + public RedisTemplate redisTemplate(){ + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/config/SecurityConfig.java b/src/main/java/com/t3t/authenticationapi/account/config/SecurityConfig.java new file mode 100644 index 0000000..fedecb7 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/config/SecurityConfig.java @@ -0,0 +1,60 @@ +package com.t3t.authenticationapi.account.config; + +import com.t3t.authenticationapi.account.component.JWTUtils; +import com.t3t.authenticationapi.account.filter.CommonExceptionFilter; +import com.t3t.authenticationapi.account.filter.CustomLogoutFilter; +import com.t3t.authenticationapi.account.filter.LoginFilter; +import com.t3t.authenticationapi.account.service.TokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + private final AuthenticationConfiguration authenticationConfiguration; + private final JWTUtils jwtUtils; + private final TokenService tokenService; + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder(){ + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf().disable() + .formLogin().disable() + .httpBasic().disable() + .authorizeRequests((auth) -> auth + .antMatchers("/login").permitAll() + .antMatchers("/refresh").permitAll() + .antMatchers("/logout").authenticated() + .anyRequest().authenticated()) + .logout(logout -> logout + .logoutUrl("/logout") // logout 담당 url + .logoutSuccessUrl("/index")) // logout 성공시 redirect 할 url + .addFilterBefore(new CommonExceptionFilter(), LoginFilter.class) + .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtils, tokenService), UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new CustomLogoutFilter(jwtUtils, tokenService), LogoutFilter.class) + .sessionManagement((session) -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + return http.build(); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/controller/LoginController.java b/src/main/java/com/t3t/authenticationapi/account/controller/LoginController.java new file mode 100644 index 0000000..e0f84e7 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/controller/LoginController.java @@ -0,0 +1,22 @@ +package com.t3t.authenticationapi.account.controller; + +import com.t3t.authenticationapi.account.service.DefaultUserDetailsService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; + +@RestController +public class LoginController { + private final DefaultUserDetailsService service; + + public LoginController(DefaultUserDetailsService service) { + this.service = service; + } + + @PostMapping("/login") + public String doLogin(HttpServletRequest request){ + // LoginFilter 수행시 successfulAuthentication 메소드가 수행되고 해당 메소드에서 응답이 커밋됨 + return "login"; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/controller/LogoutController.java b/src/main/java/com/t3t/authenticationapi/account/controller/LogoutController.java new file mode 100644 index 0000000..27c6d27 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/controller/LogoutController.java @@ -0,0 +1,12 @@ +package com.t3t.authenticationapi.account.controller; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class LogoutController { + @PostMapping("/logout") + public String logout(){ + return "logout"; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/controller/RefreshController.java b/src/main/java/com/t3t/authenticationapi/account/controller/RefreshController.java new file mode 100644 index 0000000..57b05fa --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/controller/RefreshController.java @@ -0,0 +1,21 @@ +package com.t3t.authenticationapi.account.controller; + +import com.t3t.authenticationapi.account.service.DefaultRefreshService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@RestController +@RequiredArgsConstructor +public class RefreshController { + private final DefaultRefreshService defaultRefreshService; + + @PostMapping("/refresh") + public ResponseEntity refresh(HttpServletRequest request, HttpServletResponse response) { + return defaultRefreshService.refresh(request,response); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/dao/RedisDao.java b/src/main/java/com/t3t/authenticationapi/account/dao/RedisDao.java new file mode 100644 index 0000000..043e798 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/dao/RedisDao.java @@ -0,0 +1,35 @@ +package com.t3t.authenticationapi.account.dao; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +public class RedisDao { + private final RedisTemplate redisTemplate; + + public RedisDao(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + public void setValues(String key, String data) { + ValueOperations values = redisTemplate.opsForValue(); + values.set(key, data); + } + + public void setValues(String key, String data, Duration duration) { + ValueOperations values = redisTemplate.opsForValue(); + values.set(key, data, duration); + } + + public String getValues(String key) { + ValueOperations values = redisTemplate.opsForValue(); + return values.get(key); + } + + public void deleteValues(String key) { + redisTemplate.delete(key); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/dto/LoginDto.java b/src/main/java/com/t3t/authenticationapi/account/dto/LoginDto.java new file mode 100644 index 0000000..d847824 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/dto/LoginDto.java @@ -0,0 +1,11 @@ +package com.t3t.authenticationapi.account.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class LoginDto { + private String username; + private String password; +} diff --git a/src/main/java/com/t3t/authenticationapi/account/dto/UserEntity.java b/src/main/java/com/t3t/authenticationapi/account/dto/UserEntity.java new file mode 100644 index 0000000..62ee62a --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/dto/UserEntity.java @@ -0,0 +1,13 @@ +package com.t3t.authenticationapi.account.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class UserEntity { + private String username; + private String userId; + private String password; + private String role; +} diff --git a/src/main/java/com/t3t/authenticationapi/account/dto/UserEntityDto.java b/src/main/java/com/t3t/authenticationapi/account/dto/UserEntityDto.java new file mode 100644 index 0000000..1dc8690 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/dto/UserEntityDto.java @@ -0,0 +1,8 @@ +package com.t3t.authenticationapi.account.dto; + +public interface UserEntityDto { + String getUsername(); + String getUserId(); + String getPassword(); + String getRole(); +} diff --git a/src/main/java/com/t3t/authenticationapi/account/entity/Account.java b/src/main/java/com/t3t/authenticationapi/account/entity/Account.java new file mode 100644 index 0000000..f4cdac0 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/entity/Account.java @@ -0,0 +1,28 @@ +package com.t3t.authenticationapi.account.entity; + +import com.t3t.authenticationapi.member.entity.Member; +import lombok.*; +import lombok.experimental.SuperBuilder; + +import javax.persistence.*; + +@Getter +@Entity@Table(name = "accounts") +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Inheritance(strategy = InheritanceType.JOINED) +public class Account { + @Id + @Column(name = "account_id") + private String id; + @JoinColumn(name = "member_id") + @ManyToOne + private Member member; + @Column(name = "deleted") + private int deleted; + + public Account(String id, Member member){ + this.id = id; + this.member = member; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/entity/BlackList.java b/src/main/java/com/t3t/authenticationapi/account/entity/BlackList.java new file mode 100644 index 0000000..b28c61a --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/entity/BlackList.java @@ -0,0 +1,15 @@ +package com.t3t.authenticationapi.account.entity; + +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@RedisHash(value = "BlackList") +public class BlackList { + @Id + private String blackList; +} diff --git a/src/main/java/com/t3t/authenticationapi/account/entity/BookstoreAccount.java b/src/main/java/com/t3t/authenticationapi/account/entity/BookstoreAccount.java new file mode 100644 index 0000000..64683f2 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/entity/BookstoreAccount.java @@ -0,0 +1,19 @@ +package com.t3t.authenticationapi.account.entity; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +import javax.persistence.*; + +@Getter +@Entity@Table(name = "bookstore_accounts") +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class BookstoreAccount extends Account{ + @Column(name = "account_password") + private String password; + + public BookstoreAccount(String password) { + this.password = password; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/entity/OAuthAccount.java b/src/main/java/com/t3t/authenticationapi/account/entity/OAuthAccount.java new file mode 100644 index 0000000..788603c --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/entity/OAuthAccount.java @@ -0,0 +1,20 @@ +package com.t3t.authenticationapi.account.entity; + +import lombok.*; +import lombok.experimental.SuperBuilder; + +import javax.persistence.*; + +@Getter +@Entity@Table(name = "oauth_accounts") +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OAuthAccount extends Account{ + @ManyToOne + @JoinColumn(name = "oauth_provider_id") + private OAuthProvider oAuthProvider; + + public OAuthAccount(OAuthProvider oAuthProvider){ + this.oAuthProvider = oAuthProvider; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/entity/OAuthProvider.java b/src/main/java/com/t3t/authenticationapi/account/entity/OAuthProvider.java new file mode 100644 index 0000000..ec43373 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/entity/OAuthProvider.java @@ -0,0 +1,24 @@ +package com.t3t.authenticationapi.account.entity; + +import lombok.*; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Getter +@Entity@Table(name = "oauth_providers") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OAuthProvider { + @Id + @Column(name = "oauth_provider_id") + private Integer id; + @Column(name = "oauth_provider_name") + private String name; + @Builder + public OAuthProvider(Integer id, String name){ + this.id = id; + this.name = name; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/entity/Refresh.java b/src/main/java/com/t3t/authenticationapi/account/entity/Refresh.java new file mode 100644 index 0000000..967ba5c --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/entity/Refresh.java @@ -0,0 +1,19 @@ +package com.t3t.authenticationapi.account.entity; + +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + + +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@RedisHash(value = "refresh", timeToLive = 1800) +public class Refresh { + @Id + private String token; + @Indexed + private String uuid; +} diff --git a/src/main/java/com/t3t/authenticationapi/account/exception/CookieNotExistException.java b/src/main/java/com/t3t/authenticationapi/account/exception/CookieNotExistException.java new file mode 100644 index 0000000..4f14612 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/exception/CookieNotExistException.java @@ -0,0 +1,7 @@ +package com.t3t.authenticationapi.account.exception; + +public class CookieNotExistException extends NullPointerException{ + public CookieNotExistException(String s) { + super(s); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/exception/JsonFieldNotMatchException.java b/src/main/java/com/t3t/authenticationapi/account/exception/JsonFieldNotMatchException.java new file mode 100644 index 0000000..8006c3c --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/exception/JsonFieldNotMatchException.java @@ -0,0 +1,9 @@ +package com.t3t.authenticationapi.account.exception; + +import java.io.IOException; + +public class JsonFieldNotMatchException extends RuntimeException{ + public JsonFieldNotMatchException(IOException e) { + super(e); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/exception/TokenAlreadyExistsException.java b/src/main/java/com/t3t/authenticationapi/account/exception/TokenAlreadyExistsException.java new file mode 100644 index 0000000..b9bddf8 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/exception/TokenAlreadyExistsException.java @@ -0,0 +1,7 @@ +package com.t3t.authenticationapi.account.exception; + +public class TokenAlreadyExistsException extends RuntimeException{ + public TokenAlreadyExistsException(String e) { + super(e); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/exception/TokenHasExpiredException.java b/src/main/java/com/t3t/authenticationapi/account/exception/TokenHasExpiredException.java new file mode 100644 index 0000000..c4bd8e4 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/exception/TokenHasExpiredException.java @@ -0,0 +1,7 @@ +package com.t3t.authenticationapi.account.exception; + +public class TokenHasExpiredException extends RuntimeException{ + public TokenHasExpiredException(String s) { + super(s); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/exception/TokenNotExistsException.java b/src/main/java/com/t3t/authenticationapi/account/exception/TokenNotExistsException.java new file mode 100644 index 0000000..46ad056 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/exception/TokenNotExistsException.java @@ -0,0 +1,7 @@ +package com.t3t.authenticationapi.account.exception; + +public class TokenNotExistsException extends RuntimeException{ + public TokenNotExistsException(String s) { + super(s); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/filter/CommonExceptionFilter.java b/src/main/java/com/t3t/authenticationapi/account/filter/CommonExceptionFilter.java new file mode 100644 index 0000000..4b87a0f --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/filter/CommonExceptionFilter.java @@ -0,0 +1,45 @@ +package com.t3t.authenticationapi.account.filter; + +import com.t3t.authenticationapi.account.exception.JsonFieldNotMatchException; +import com.t3t.authenticationapi.account.exception.TokenAlreadyExistsException; +import com.t3t.authenticationapi.account.exception.TokenNotExistsException; +import org.json.JSONObject; +import org.springframework.http.MediaType; +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 javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class CommonExceptionFilter extends GenericFilterBean { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + try { + chain.doFilter(request, response); + } catch(Exception e){ + handleException((HttpServletRequest) request, (HttpServletResponse) response, chain, e); + } + } + + private void handleException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Exception e) throws IOException, ServletException { + response.setContentType(String.valueOf(MediaType.APPLICATION_JSON)); + if (e instanceof JsonFieldNotMatchException) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.getWriter().write(new JSONObject().put("errorMessage","Use Proper Field").toString()); + } else if (e instanceof TokenAlreadyExistsException) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.getWriter().write(new JSONObject().put("errorMessage",e.getMessage()).toString()); + } else if (e instanceof TokenNotExistsException){ + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.getWriter().write(new JSONObject().put("errorMessage",e.getMessage()).toString()); + } else { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + response.getWriter().write(new JSONObject().put("errorMessage",e.getMessage()).toString()); + } + } + +} diff --git a/src/main/java/com/t3t/authenticationapi/account/filter/CustomLogoutFilter.java b/src/main/java/com/t3t/authenticationapi/account/filter/CustomLogoutFilter.java new file mode 100644 index 0000000..51e9945 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/filter/CustomLogoutFilter.java @@ -0,0 +1,61 @@ +package com.t3t.authenticationapi.account.filter; + +import com.t3t.authenticationapi.account.component.JWTUtils; +import com.t3t.authenticationapi.account.service.TokenService; +import lombok.RequiredArgsConstructor; +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 javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +@RequiredArgsConstructor +public class CustomLogoutFilter extends GenericFilterBean { + private final JWTUtils jwtUtils; + private final TokenService tokenService; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + doFilter((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, filterChain); + } + + public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + if (!request.getRequestURI().matches("^\\/logout$")) { + filterChain.doFilter(request, response); + return; + } + if (!request.getMethod().equals("POST")) { + filterChain.doFilter(request, response); + return; + } + + if(request.getHeader("Authority").isEmpty()){ + filterChain.doFilter(request, response); + return; + } + + String access = request.getHeader("Authority").trim().split(" ")[1]; + + if (Objects.isNull(access)) { + filterChain.doFilter(request, response); + return; + } + if (jwtUtils.isExpired(access)) { + filterChain.doFilter(request, response); + return; + } + + String uuid = jwtUtils.getUUID(access); + tokenService.saveBlackListToken(access); + if(tokenService.refreshTokenExistsByUUID(uuid)){ + tokenService.removeRefreshTokenByUUID(uuid); // redis에 있는 RTK 제거 + } + + response.setStatus(HttpServletResponse.SC_NO_CONTENT); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/filter/LoginFilter.java b/src/main/java/com/t3t/authenticationapi/account/filter/LoginFilter.java new file mode 100644 index 0000000..96e93c4 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/filter/LoginFilter.java @@ -0,0 +1,94 @@ +package com.t3t.authenticationapi.account.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.t3t.authenticationapi.account.auth.CustomUserDetails; +import com.t3t.authenticationapi.account.component.JWTUtils; +import com.t3t.authenticationapi.account.dto.LoginDto; +import com.t3t.authenticationapi.account.entity.Refresh; +import com.t3t.authenticationapi.account.exception.JsonFieldNotMatchException; +import com.t3t.authenticationapi.account.service.TokenService; +import lombok.RequiredArgsConstructor; +import org.json.JSONObject; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.util.StreamUtils; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Iterator; +import java.util.UUID; + +@RequiredArgsConstructor +public class LoginFilter extends UsernamePasswordAuthenticationFilter { + private final AuthenticationManager authenticationManager; + private final JWTUtils jwtUtils; + private final TokenService tokenService; + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + LoginDto loginDto = null; + + try { + ObjectMapper mapper = new ObjectMapper(); + ServletInputStream inputStream = request.getInputStream(); + String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); + loginDto = mapper.readValue(messageBody, LoginDto.class); + }catch (IOException e){ + throw new JsonFieldNotMatchException(e); + } + + String username = loginDto.getUsername(); + String password = loginDto.getPassword(); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null); + return authenticationManager.authenticate(authToken); + } + + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) { + CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); + + String userId = customUserDetails.getUserId(); + + Collection authorities = authentication.getAuthorities(); + Iterator iterator = authorities.iterator(); + GrantedAuthority grantedAuthority = iterator.next(); + + String role = grantedAuthority.getAuthority(); + + String uuid = UUID.randomUUID().toString(); + String access = jwtUtils.createJwt("access", userId, role, uuid, 900000l); // 15분 + String refresh = jwtUtils.createJwt("refresh", userId, role, uuid,1800000l); // 30분 + + tokenService.saveRefreshToken(Refresh.builder().token(refresh).uuid(uuid).build()); + + response.addHeader("Authority", "Bearer " + access); + response.setStatus(HttpServletResponse.SC_OK); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { + String errorMessage = null; + if(failed instanceof BadCredentialsException){ + errorMessage = "invalid id or password"; + }else{ + errorMessage = "auth failed"; + } + + response.setContentType(String.valueOf(MediaType.APPLICATION_JSON)); + response.setStatus(401); + response.getWriter().write(new JSONObject().put("error", errorMessage).toString()); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/repository/AccountRepository.java b/src/main/java/com/t3t/authenticationapi/account/repository/AccountRepository.java new file mode 100644 index 0000000..8c72f01 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/repository/AccountRepository.java @@ -0,0 +1,13 @@ +package com.t3t.authenticationapi.account.repository; + +import com.t3t.authenticationapi.account.dto.UserEntityDto; +import com.t3t.authenticationapi.account.entity.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +public interface AccountRepository extends JpaRepository { + @Query("SELECT a.id as username, m.id as userId , b.password as password, m.role as role FROM Account a " + + "INNER JOIN a.member m " + + "INNER JOIN BookstoreAccount b ON a.id = b.id " + + "WHERE a.id = :id") + UserEntityDto loadUserEntity(String id); +} diff --git a/src/main/java/com/t3t/authenticationapi/account/repository/BlackListRepository.java b/src/main/java/com/t3t/authenticationapi/account/repository/BlackListRepository.java new file mode 100644 index 0000000..71f760c --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/repository/BlackListRepository.java @@ -0,0 +1,7 @@ +package com.t3t.authenticationapi.account.repository; + +import com.t3t.authenticationapi.account.entity.BlackList; +import org.springframework.data.repository.CrudRepository; + +public interface BlackListRepository extends CrudRepository { +} diff --git a/src/main/java/com/t3t/authenticationapi/account/repository/BookStoreAccountRepository.java b/src/main/java/com/t3t/authenticationapi/account/repository/BookStoreAccountRepository.java new file mode 100644 index 0000000..bc87f33 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/repository/BookStoreAccountRepository.java @@ -0,0 +1,7 @@ +package com.t3t.authenticationapi.account.repository; + +import com.t3t.authenticationapi.account.entity.BookstoreAccount; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BookStoreAccountRepository extends JpaRepository { +} diff --git a/src/main/java/com/t3t/authenticationapi/account/repository/OAuthAccountRepository.java b/src/main/java/com/t3t/authenticationapi/account/repository/OAuthAccountRepository.java new file mode 100644 index 0000000..57e9065 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/repository/OAuthAccountRepository.java @@ -0,0 +1,7 @@ +package com.t3t.authenticationapi.account.repository; + +import com.t3t.authenticationapi.account.entity.OAuthAccount; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OAuthAccountRepository extends JpaRepository { +} diff --git a/src/main/java/com/t3t/authenticationapi/account/repository/OAuthProviderRepository.java b/src/main/java/com/t3t/authenticationapi/account/repository/OAuthProviderRepository.java new file mode 100644 index 0000000..af88f58 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/repository/OAuthProviderRepository.java @@ -0,0 +1,7 @@ +package com.t3t.authenticationapi.account.repository; + +import com.t3t.authenticationapi.account.entity.OAuthProvider; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface OAuthProviderRepository extends JpaRepository { +} diff --git a/src/main/java/com/t3t/authenticationapi/account/repository/RefreshRepository.java b/src/main/java/com/t3t/authenticationapi/account/repository/RefreshRepository.java new file mode 100644 index 0000000..0e92a11 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/repository/RefreshRepository.java @@ -0,0 +1,12 @@ +package com.t3t.authenticationapi.account.repository; + +import com.t3t.authenticationapi.account.entity.Refresh; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; + +public interface RefreshRepository extends CrudRepository { + Optional findByUuid(String uuid); + + boolean existsByUuid(String uuid); +} diff --git a/src/main/java/com/t3t/authenticationapi/account/service/DefaultRefreshService.java b/src/main/java/com/t3t/authenticationapi/account/service/DefaultRefreshService.java new file mode 100644 index 0000000..783a465 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/service/DefaultRefreshService.java @@ -0,0 +1,60 @@ +package com.t3t.authenticationapi.account.service; + +import com.t3t.authenticationapi.account.component.JWTUtils; +import com.t3t.authenticationapi.account.exception.TokenNotExistsException; +import io.jsonwebtoken.ExpiredJwtException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class DefaultRefreshService { + private final JWTUtils jwtUtils; + private final TokenService tokenService; + + public ResponseEntity refresh(HttpServletRequest request, HttpServletResponse response) { + if (Objects.isNull(request.getHeader("Authority"))) { + throw new TokenNotExistsException("Access Token Not Exists"); + } + String access = request.getHeader("Authority").trim().split(" ")[1]; + String newAccess = null; + String refresh = null; + + // access 만료 + try{ + // refresh 만료시 "expired" + refresh = tokenService.findRefreshByUUID(jwtUtils.getUUID(access)); + }catch (ExpiredJwtException e){ + String expiredUUID = e.getClaims().get("uuid", String.class); + refresh = tokenService.findRefreshByUUID(expiredUUID); + // Refresh 토큰이 살아 있는 경우 + if (!jwtUtils.isExpired(refresh)) { + newAccess = jwtUtils.createJwt("access", jwtUtils.getUserName(refresh), + jwtUtils.getRole(refresh), jwtUtils.getUUID(refresh), 900000l); + response.addHeader("Authority", "Bearer " + newAccess); + return ResponseEntity.ok().build(); + } + } + + String userId = jwtUtils.getUserName(access); + String role = jwtUtils.getRole(access); + String uuid = jwtUtils.getUUID(access); + + // access 만료 전, refresh 만료 전 + // http 요청이 accesstoken 만료 5분 전이라 자동 로그인 처리 + if (jwtUtils.checkReIssue(access) && tokenService.refreshTokenExists(refresh)) { + newAccess = jwtUtils.createJwt("access", userId, role, uuid, 900000l); + response.addHeader("Authority", "Bearer " + newAccess); + return ResponseEntity.ok().build(); + } + + // 그 외의 경우는 + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/t3t/authenticationapi/account/service/DefaultUserDetailsService.java b/src/main/java/com/t3t/authenticationapi/account/service/DefaultUserDetailsService.java new file mode 100644 index 0000000..4fe7cc0 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/service/DefaultUserDetailsService.java @@ -0,0 +1,40 @@ +package com.t3t.authenticationapi.account.service; + +import com.t3t.authenticationapi.account.auth.CustomUserDetails; +import com.t3t.authenticationapi.account.dto.UserEntity; +import com.t3t.authenticationapi.account.dto.UserEntityDto; +import com.t3t.authenticationapi.account.repository.AccountRepository; +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.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class DefaultUserDetailsService implements UserDetailsService { + private final AccountRepository accountRepository; + + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + UserEntityDto userEntityDto = accountRepository.loadUserEntity(username); + + if(Objects.isNull(userEntityDto)){ + throw new UsernameNotFoundException("User Not Found"); + } + + UserEntity userEntity = new UserEntity(); + userEntity.setUsername(userEntityDto.getUsername()); + userEntity.setUserId(userEntityDto.getUserId()); + userEntity.setPassword(userEntityDto.getPassword()); // 이게 맞는데 현재 회원에 password가 암호화 되어있지 않음 +// userEntity.setPassword(bCryptPasswordEncoder.encode(userEntityDto.getPassword())); + userEntity.setRole(userEntityDto.getRole()); + + return new CustomUserDetails(userEntity); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/account/service/TokenService.java b/src/main/java/com/t3t/authenticationapi/account/service/TokenService.java new file mode 100644 index 0000000..eba0587 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/account/service/TokenService.java @@ -0,0 +1,63 @@ +package com.t3t.authenticationapi.account.service; + +import com.t3t.authenticationapi.account.entity.BlackList; +import com.t3t.authenticationapi.account.entity.Refresh; +import com.t3t.authenticationapi.account.exception.TokenAlreadyExistsException; +import com.t3t.authenticationapi.account.exception.TokenNotExistsException; +import com.t3t.authenticationapi.account.repository.BlackListRepository; +import com.t3t.authenticationapi.account.repository.RefreshRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class TokenService { + private final RefreshRepository refreshRepository; + private final BlackListRepository blackListRepository; + + public void saveRefreshToken(Refresh refresh){ + if(refreshRepository.existsById(refresh.toString())){ + throw new TokenAlreadyExistsException("Token Already Exists"); + } + refreshRepository.save(refresh); + } + public void saveBlackListToken(String blackList){ + if(blackListRepository.existsById(blackList)){ + throw new TokenAlreadyExistsException("Token Already Exists"); + } + blackListRepository.save(BlackList.builder().blackList(blackList).build()); + } + + public void removeRefreshToken(String refresh){ + if(!refreshRepository.existsById(refresh)){ + throw new TokenNotExistsException("Token Not Exists"); + } + Refresh newRefresh = refreshRepository.findById(refresh).get(); + refreshRepository.delete(newRefresh); + } + + public void removeRefreshTokenByUUID(String uuid){ + Optional optionalRefresh = refreshRepository.findByUuid(uuid); + if(!optionalRefresh.isPresent()){ + throw new TokenNotExistsException("Token Not Exists"); + } + refreshRepository.delete(optionalRefresh.get()); + } + + public boolean refreshTokenExists(String refresh){ + return refreshRepository.existsById(refresh); + } + + public String findRefreshByUUID(String uuid){ + if(refreshRepository.findByUuid(uuid).isEmpty()){ + throw new TokenNotExistsException("Expired"); + } + return refreshRepository.findByUuid(uuid).get().getToken(); + } + + public Boolean refreshTokenExistsByUUID(String uuid){ + return refreshRepository.existsByUuid(uuid); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/index/IndexController.java b/src/main/java/com/t3t/authenticationapi/index/IndexController.java deleted file mode 100644 index e7b5714..0000000 --- a/src/main/java/com/t3t/authenticationapi/index/IndexController.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.t3t.authenticationapi.index; - -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; - -@Controller -public class IndexController { - @GetMapping(value = {"/index.html","/"}) - public String index(){ - return "index"; - } -} - - - diff --git a/src/main/java/com/t3t/authenticationapi/member/entity/Member.java b/src/main/java/com/t3t/authenticationapi/member/entity/Member.java new file mode 100644 index 0000000..d39e7b9 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/member/entity/Member.java @@ -0,0 +1,54 @@ +package com.t3t.authenticationapi.member.entity; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import java.time.LocalDate; + +@Getter +@Entity@Table(name = "members") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member { + @Id + @Column(name = "member_id") + private Long id; + @Column(name = "grade_id") + private Integer gradeId; + @Column(name = "member_name") + private String name; + @Column(name = "member_phone") + private String phone; + @Column(name = "member_email") + private String email; + @Column(name = "member_birthdate") + private LocalDate birthdate; + @Column(name = "member_latest_login") + private LocalDate latestLogin; + @Column(name = "member_point") + private Integer point; + @Column(name = "member_status") + private String status; + @Column(name = "member_role") + private String role; + @Builder + public Member(Long id, Integer gradeId, String name, String phone, String email, + LocalDate birthdate, LocalDate latestLogin, Integer point, + String status, String role) { + this.id = id; + this.gradeId = gradeId; + this.name = name; + this.phone = phone; + this.email = email; + this.birthdate = birthdate; + this.latestLogin = latestLogin; + this.point = point; + this.status = status; + this.role = role; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/member/repository/MemberRepository.java b/src/main/java/com/t3t/authenticationapi/member/repository/MemberRepository.java new file mode 100644 index 0000000..2fc2a7e --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/member/repository/MemberRepository.java @@ -0,0 +1,9 @@ +package com.t3t.authenticationapi.member.repository; + +import com.t3t.authenticationapi.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface MemberRepository extends JpaRepository { + +} diff --git a/src/main/java/com/t3t/authenticationapi/model/response/BaseResponse.java b/src/main/java/com/t3t/authenticationapi/model/response/BaseResponse.java new file mode 100644 index 0000000..f65b8b6 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/model/response/BaseResponse.java @@ -0,0 +1,38 @@ +package com.t3t.authenticationapi.model.response; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@NoArgsConstructor +public class BaseResponse { + private String message; + private T data; + + @Builder + public BaseResponse(String message, T data) { + this.message = message; + this.data = data; + } + + public static BaseResponse success(String message, T data) { + return BaseResponse.builder() + .message(message) + .data(data) + .build(); + } + + public BaseResponse data(T data) { + this.data = data; + return this; + } + + public BaseResponse message(String message) { + this.message = message; + return this; + } + +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..628f4d4 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,30 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://133.186.223.228:3306/t3team?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 + username: t3team + password: uPJQz6QaL6@6h]BG + security: + key: sakdjA24HSdflasbdglag2yhsdrg342TASGASd58aw4t3AWEIGzsoigbaWEIGHP3tug0ajw4s23a8th24tgaw2854yq3p48ghaa294 + redis: + host: 133.186.223.228 + password: "*N2vya7H@muDTwdNMR!" + port: 6379 + database: 20 + + jpa: + open-in-view: true + hibernate: + ddl-auto: none + naming: + physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + use-new-id-generator-mappings: false + show-sql: true + properties: + hibernate.format_sql: true + dialect: org.hibernate.dialect.MySQL8InnoDBDialect + + +logging: + level: + org.hibernate.SQL: debug \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html deleted file mode 100644 index aae355d..0000000 --- a/src/main/resources/templates/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - welcome page - - -

hello spring!!

- - \ No newline at end of file diff --git a/src/test/java/com/t3t/authenticationapi/account/repository/AccountRepositoryTest.java b/src/test/java/com/t3t/authenticationapi/account/repository/AccountRepositoryTest.java new file mode 100644 index 0000000..eedfd74 --- /dev/null +++ b/src/test/java/com/t3t/authenticationapi/account/repository/AccountRepositoryTest.java @@ -0,0 +1,54 @@ +package com.t3t.authenticationapi.account.repository; + +import com.t3t.authenticationapi.account.dto.UserEntityDto; +import com.t3t.authenticationapi.account.entity.BookstoreAccount; +import com.t3t.authenticationapi.member.entity.Member; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import java.time.LocalDate; +@DataJpaTest +class AccountRepositoryTest { + @Autowired + private TestEntityManager entityManager; + @Autowired + private AccountRepository accountRepository; + + @Test + public void testLoadUserEntity(){ + Member member = Member.builder() + .id(3l) + .name("foo") + .phone("010-1234-5678") + .point(100) + .role("USER") + .email("2@Mail.com") + .birthdate(LocalDate.now()) + .status("ACTIVE") + .latestLogin(LocalDate.now()) + .gradeId(1) + .build(); + + entityManager.persist(member); + + BookstoreAccount bookstoreAccount = BookstoreAccount.builder() + .id("user") + .password("password") + .member(member) + .build(); + + entityManager.persist(bookstoreAccount); + + + UserEntityDto entityDto = accountRepository.loadUserEntity(bookstoreAccount.getId()); + + Assertions.assertThat(entityDto.getUserId()).isEqualTo("3"); + Assertions.assertThat(entityDto.getUsername()).isEqualTo("user"); + Assertions.assertThat(entityDto.getPassword()).isEqualTo("password"); + Assertions.assertThat(entityDto.getRole()).isEqualTo("USER"); + + } +} \ No newline at end of file diff --git a/src/test/java/com/t3t/authenticationapi/account/repository/RefreshRepositoryTest.java b/src/test/java/com/t3t/authenticationapi/account/repository/RefreshRepositoryTest.java new file mode 100644 index 0000000..71083cf --- /dev/null +++ b/src/test/java/com/t3t/authenticationapi/account/repository/RefreshRepositoryTest.java @@ -0,0 +1,53 @@ +package com.t3t.authenticationapi.account.repository; + +import com.t3t.authenticationapi.account.entity.Refresh; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +class RefreshRepositoryTest { + @Autowired + private RefreshRepository refreshRepository; + + private Refresh refresh = null; + @BeforeEach + public void set(){ + refresh = Refresh.builder() + .token("r.r.r") + .uuid("1") + .build(); + + refreshRepository.save(refresh); + } + + @AfterEach + public void tearDown(){ + refreshRepository.delete(refresh); + } + @Test + public void findRefreshTest(){ + Refresh newRefresh = refreshRepository.findById("r.r.r").get(); + + Assertions.assertThat(newRefresh.getToken()).isEqualTo("r.r.r"); + Assertions.assertThat(newRefresh.getUuid()).isEqualTo("1"); + } + + @Test + public void findByUUIDTest(){ + String uuid = "1"; + + Assertions.assertThat(refresh.getUuid()).isEqualTo("1"); + } + + @Test + public void cannotFindByUUIDTest(){ + String uuid = "2"; + Assertions.not(refresh.getUuid()).equals(uuid); + } +} \ No newline at end of file diff --git a/src/test/java/com/t3t/authenticationapi/account/service/DefaultRefreshServiceTest.java b/src/test/java/com/t3t/authenticationapi/account/service/DefaultRefreshServiceTest.java new file mode 100644 index 0000000..c32a2da --- /dev/null +++ b/src/test/java/com/t3t/authenticationapi/account/service/DefaultRefreshServiceTest.java @@ -0,0 +1,125 @@ +package com.t3t.authenticationapi.account.service; + +import com.t3t.authenticationapi.account.component.JWTUtils; +import com.t3t.authenticationapi.account.exception.TokenNotExistsException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletResponse; + +import javax.servlet.http.HttpServletRequest; + +@ExtendWith(MockitoExtension.class) +class DefaultRefreshServiceTest { + @Mock + private JWTUtils jwtUtils; + @Mock + private TokenService tokenService; + @InjectMocks + private DefaultRefreshService defaultRefreshService; + + @Test + public void TestRefreshSuccess_accessExpired(){ + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + MockHttpServletResponse response = new MockHttpServletResponse(); + + String access = "Bearer access"; + String newAccess = "newAccess"; + String refresh = "refresh"; + String uuid = "uuid"; + + Mockito.when(request.getHeader("Authority")).thenReturn(access); + Mockito.when(tokenService.findRefreshByUUID(Mockito.any())).thenAnswer(new Answer() { + private int count = 0; + + public String answer(InvocationOnMock invocation) { + if (count++ == 0) { + Claims claims = Jwts.claims(); + claims.put("uuid", uuid); + throw new ExpiredJwtException(null, claims, "Token expired"); + } + return refresh; + } + }); + Mockito.when(jwtUtils.isExpired(Mockito.any())).thenReturn(false); + + Mockito.when(jwtUtils.getUserName(refresh)).thenReturn("1"); + Mockito.when(jwtUtils.getRole(refresh)).thenReturn("ROLE_USER"); + Mockito.when(jwtUtils.createJwt("access", jwtUtils.getUserName(refresh), + jwtUtils.getRole(refresh), jwtUtils.getUUID(refresh), 900000l)).thenReturn(newAccess); + + ResponseEntity result = defaultRefreshService.refresh(request, response); + + Assertions.assertAll( + () -> Assertions.assertEquals(HttpStatus.OK, result.getStatusCode()), + () -> Assertions.assertEquals("Bearer newAccess", response.getHeader("Authority")) + ); + } + + @Test + public void TestRefreshSuccess_accessNotExpired(){ + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + MockHttpServletResponse response = new MockHttpServletResponse(); + + String access = "Bearer access"; + String newAccess = "newAccess"; + String refresh = "refresh"; + + Mockito.when(request.getHeader("Authority")).thenReturn(access); + Mockito.when(tokenService.findRefreshByUUID(Mockito.any())).thenReturn(refresh); + + Mockito.when(jwtUtils.getUserName(Mockito.any())).thenReturn("1"); + Mockito.when(jwtUtils.getRole(Mockito.any())).thenReturn("ROLE_USER"); + Mockito.when(jwtUtils.getUUID(Mockito.any())).thenReturn("uuid"); + + Mockito.when(jwtUtils.checkReIssue(Mockito.any())).thenReturn(true); + Mockito.when(tokenService.refreshTokenExists(Mockito.any())).thenReturn(true); + + Mockito.when(jwtUtils.createJwt("access", jwtUtils.getUserName(refresh), + jwtUtils.getRole(refresh), jwtUtils.getUUID(refresh), 900000l)).thenReturn(newAccess); + + ResponseEntity result = defaultRefreshService.refresh(request, response); + + Assertions.assertAll( + () -> Assertions.assertEquals(HttpStatus.OK, result.getStatusCode()), + () -> Assertions.assertEquals("Bearer newAccess", response.getHeader("Authority")) + ); + } + + @Test + public void TestRefreshFailed_AuthorityNotFound(){ + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + MockHttpServletResponse response = new MockHttpServletResponse(); + + String header = request.getHeader("Authority"); + Mockito.when(header == null).thenReturn(null); + Exception exception = Assertions.assertThrows(TokenNotExistsException.class, () -> { + defaultRefreshService.refresh(request, response); + }); + + Assertions.assertEquals("Access Token Not Exists", exception.getMessage()); + } + + @Test + public void TestRefreshFailed_BothTokenNotFound() { + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + MockHttpServletResponse response = new MockHttpServletResponse(); + + Mockito.when(request.getHeader("Authority")).thenReturn("Bearer validAccessToken"); + + Mockito.when(jwtUtils.checkReIssue(Mockito.anyString())).thenReturn(false); + ResponseEntity responseEntity = defaultRefreshService.refresh(request, response); + Assertions.assertEquals(HttpStatus.UNAUTHORIZED, responseEntity.getStatusCode()); + } +} \ No newline at end of file diff --git a/src/test/java/com/t3t/authenticationapi/account/service/DefaultUserDetailsServiceTest.java b/src/test/java/com/t3t/authenticationapi/account/service/DefaultUserDetailsServiceTest.java new file mode 100644 index 0000000..00b27ae --- /dev/null +++ b/src/test/java/com/t3t/authenticationapi/account/service/DefaultUserDetailsServiceTest.java @@ -0,0 +1,61 @@ +package com.t3t.authenticationapi.account.service; + +/*import com.t3t.authenticationapi.account.auth.CustomUserDetails; +import com.t3t.authenticationapi.account.dto.UserEntityDto; +import com.t3t.authenticationapi.account.repository.AccountRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;*/ + +/* +@ExtendWith(MockitoExtension.class) +class DefaultUserDetailsServiceTest { + @Mock + private BCryptPasswordEncoder bCryptPasswordEncoder; + + @Mock + private AccountRepository accountRepository; + + @InjectMocks + private DefaultUserDetailsService defaultUserDetailsService; + + @Test + public void TestLoadUserByUserNameSuccess(){ + String username = "user"; + UserEntityDto userEntityDto = Mockito.mock(UserEntityDto.class); + + Mockito.when(userEntityDto.getUserId()).thenReturn(String.valueOf(1l)); + Mockito.when(userEntityDto.getPassword()).thenReturn("password"); + Mockito.when(userEntityDto.getRole()).thenReturn("USER"); + Mockito.when(userEntityDto.getUsername()).thenReturn(username); + + Mockito.when(accountRepository.loadUserEntity(Mockito.any())).thenReturn(userEntityDto); +// Mockito.when(bCryptPasswordEncoder.encode(Mockito.any())).thenReturn("pwEncoded"); + + CustomUserDetails userDetails = (CustomUserDetails) defaultUserDetailsService.loadUserByUsername(username); + + Assertions.assertAll( + () -> Assertions.assertEquals(String.valueOf(1l), userDetails.getUserId()), + () -> Assertions.assertEquals("password", userDetails.getPassword()) + ); + } + + @Test + public void TestLoadUserByUserNameFailed(){ + String username = "user"; + + Mockito.when(accountRepository.loadUserEntity(Mockito.any())).thenReturn(null); + + Exception exception = Assertions.assertThrows(UsernameNotFoundException.class, + () -> {defaultUserDetailsService.loadUserByUsername(username); + }); + + Assertions.assertEquals("User Not Found", exception.getMessage()); + } +}*/ diff --git a/src/test/java/com/t3t/authenticationapi/account/service/TokenServiceTest.java b/src/test/java/com/t3t/authenticationapi/account/service/TokenServiceTest.java new file mode 100644 index 0000000..81c77ef --- /dev/null +++ b/src/test/java/com/t3t/authenticationapi/account/service/TokenServiceTest.java @@ -0,0 +1,151 @@ +package com.t3t.authenticationapi.account.service; + +import com.t3t.authenticationapi.account.entity.Refresh; +import com.t3t.authenticationapi.account.exception.TokenAlreadyExistsException; +import com.t3t.authenticationapi.account.exception.TokenNotExistsException; +import com.t3t.authenticationapi.account.repository.BlackListRepository; +import com.t3t.authenticationapi.account.repository.RefreshRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +class TokenServiceTest { + @Mock + private RefreshRepository refreshRepository; + @Mock + private BlackListRepository blackListRepository; + + @InjectMocks + private TokenService tokenService; + + + @Test + public void testSaveRefreshTokenSuccess(){ + Refresh refresh = Refresh.builder().token("token").uuid("1t2").build(); + Mockito.when(refreshRepository.existsById(Mockito.any())).thenReturn(false); + tokenService.saveRefreshToken(refresh); + Mockito.verify(refreshRepository, Mockito.times(1)).save(refresh); + } + + @Test + public void testSaveRefreshTokenFail(){ + Refresh refresh = Refresh.builder().token("token").uuid("1t2").build(); + Mockito.when(refreshRepository.existsById(Mockito.any())).thenReturn(true); + Assertions.assertThatThrownBy(()-> tokenService.saveRefreshToken(refresh)).isInstanceOf(TokenAlreadyExistsException.class).hasMessage("Token Already Exists"); + } + + @Test + public void testRemoveRefreshTokenSuccess(){ + Refresh refresh = Refresh.builder().token("token").uuid("1t2").build(); + Mockito.when(refreshRepository.existsById(Mockito.any())).thenReturn(true); + Mockito.when(refreshRepository.findById(Mockito.any())).thenReturn(Optional.of(refresh)); + + tokenService.removeRefreshToken(refresh.getToken()); + + Mockito.verify(refreshRepository,Mockito.times(1)).delete(Mockito.any()); + } + + @Test + public void testRemoveRefreshTokenFailed(){ + Refresh refresh = Refresh.builder().token("token").uuid("1t2").build(); + Mockito.when(refreshRepository.existsById(Mockito.any())).thenReturn(false); + Assertions.assertThatThrownBy(()->tokenService.removeRefreshToken(refresh.getToken())).isInstanceOf(TokenNotExistsException.class); + } + + @Test + public void testRemoveRefreshTokenByUUIDSuccess(){ + Refresh refresh = Refresh.builder().token("token").uuid("1t2").build(); + Mockito.when(refreshRepository.findByUuid(Mockito.any())).thenReturn(Optional.of(refresh)); + + tokenService.removeRefreshTokenByUUID(refresh.getUuid()); + + Mockito.verify(refreshRepository, Mockito.times(1)).delete(Mockito.any()); + } + + @Test + public void testRemoveRefreshTokenByUUISFailed(){ + Refresh refresh = Refresh.builder().token("token").uuid("1t2").build(); + Mockito.when(refreshRepository.findByUuid(Mockito.any())).thenReturn(Optional.empty()); + + Assertions.assertThatThrownBy(() -> tokenService.removeRefreshTokenByUUID(refresh.getUuid())).isInstanceOf(TokenNotExistsException.class); + } + + @Test + public void saveBlackListTokenSuccess(){ + String blackList = "black"; + + Mockito.when(blackListRepository.existsById(Mockito.any())).thenReturn(false); + tokenService.saveBlackListToken(blackList); + + Mockito.verify(blackListRepository, Mockito.times(1)).save(Mockito.any()); + } + + @Test + public void saveBlackListTokenFailed(){ + String blackList = "black"; + + Mockito.when(blackListRepository.existsById(Mockito.any())).thenReturn(true); + Assertions.assertThatThrownBy(() -> tokenService.saveBlackListToken(Mockito.any())).isInstanceOf(TokenAlreadyExistsException.class); + } + + @Test + public void refreshTokenExistsSuccess(){ + String refresh = "refresh"; + + Mockito.when(refreshRepository.existsById(Mockito.any())).thenReturn(true); + + tokenService.refreshTokenExists(refresh); + + Mockito.verify(refreshRepository, Mockito.times(1)).existsById(Mockito.any()); + } + @Test + public void refreshTokenExistsFailed(){ + String refresh = "refresh"; + + Mockito.when(refreshRepository.existsById(Mockito.any())).thenReturn(false); + tokenService.refreshTokenExists(refresh); + Mockito.verify(refreshRepository, Mockito.times(1)).existsById(Mockito.any()); + } + + @Test + public void findRefreshByUUIDSuccess(){ + String uuid = "uuid"; + Refresh refresh = Refresh.builder().token("token").uuid(uuid).build(); + + Mockito.when(refreshRepository.findByUuid(Mockito.anyString())).thenReturn(Optional.of(refresh)); + String answer = tokenService.findRefreshByUUID(uuid); + Mockito.verify(refreshRepository, Mockito.times(2)).findByUuid(Mockito.any()); + } + + @Test + public void findRefreshByUUIDFailed(){ + String uuid = "uuid"; + + Mockito.when(refreshRepository.findByUuid(Mockito.any())).thenReturn(Optional.empty()); + Assertions.assertThatThrownBy(() -> tokenService.findRefreshByUUID(uuid)).isInstanceOf(TokenNotExistsException.class); + } + + @Test + public void refreshTokenExistsByUUIDSuccess(){ + String uuid = "uuid"; + + Mockito.when(refreshRepository.existsByUuid(Mockito.any())).thenReturn(true); + tokenService.refreshTokenExistsByUUID(uuid); + Mockito.verify(refreshRepository, Mockito.times(1)).existsByUuid(uuid); + } + @Test + public void refreshTokenExistsByUUIDFailed(){ + String uuid = "uuid"; + + Mockito.when(refreshRepository.existsByUuid(Mockito.any())).thenReturn(false); + tokenService.refreshTokenExistsByUUID(uuid); + Mockito.verify(refreshRepository, Mockito.times(1)).existsByUuid(uuid); + } +} \ No newline at end of file diff --git a/src/test/java/com/t3t/authenticationapi/filter/CustomLogoutFilterTest.java b/src/test/java/com/t3t/authenticationapi/filter/CustomLogoutFilterTest.java new file mode 100644 index 0000000..a3948cf --- /dev/null +++ b/src/test/java/com/t3t/authenticationapi/filter/CustomLogoutFilterTest.java @@ -0,0 +1,4 @@ +package com.t3t.authenticationapi.filter; + +public class CustomLogoutFilterTest { +} diff --git a/src/test/java/com/t3t/authenticationapi/filter/LoginFilterTest.java b/src/test/java/com/t3t/authenticationapi/filter/LoginFilterTest.java new file mode 100644 index 0000000..18ea7e3 --- /dev/null +++ b/src/test/java/com/t3t/authenticationapi/filter/LoginFilterTest.java @@ -0,0 +1,113 @@ +package com.t3t.authenticationapi.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.t3t.authenticationapi.account.auth.CustomUserDetails; +import com.t3t.authenticationapi.account.component.JWTUtils; +import com.t3t.authenticationapi.account.dto.LoginDto; +import com.t3t.authenticationapi.account.dto.UserEntity; +import com.t3t.authenticationapi.account.exception.JsonFieldNotMatchException; +import com.t3t.authenticationapi.account.service.DefaultUserDetailsService; +import com.t3t.authenticationapi.account.service.TokenService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; + +import javax.servlet.http.Cookie; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) +@AutoConfigureMockMvc +@Transactional +public class LoginFilterTest { + @Autowired + private MockMvc mockMvc; + @MockBean + private AuthenticationManager authenticationManager; + @MockBean + private JWTUtils jwtUtils; + @MockBean + private TokenService tokenService; + @MockBean + private DefaultUserDetailsService userDetailsService; + + @Test + public void TestLoginSuccess() throws Exception { + Map testLogin = new LinkedHashMap<>(); + testLogin.put("username","user"); + testLogin.put("password","1234"); + + ObjectMapper objectMapper = new ObjectMapper(); + String messageBody = objectMapper.writeValueAsString(testLogin); + LoginDto loginDto = objectMapper.readValue(messageBody, LoginDto.class); + + + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("user", "1234", null); + Mockito.when(authenticationManager.authenticate(any())).thenReturn(auth); + + UserEntity userEntity = new UserEntity(); + userEntity.setUserId("1"); + userEntity.setUsername("user"); + userEntity.setPassword("1234"); + userEntity.setRole("ROLE_USER"); + + Mockito.when(userDetailsService.loadUserByUsername(any())).thenReturn(new CustomUserDetails(userEntity)); + + CustomUserDetails customUserDetails = (CustomUserDetails) userDetailsService.loadUserByUsername("user"); + String userId = customUserDetails.getUserId(); + + String uuid = UUID.randomUUID().toString(); + String access = jwtUtils.createJwt("access", userId, "ROLE_USER", uuid, 900000l); + String refresh = jwtUtils.createJwt("refresh", userId, "ROLE_USER", uuid,604800000l); + + Mockito.when(jwtUtils.createJwt(any(), any(), any(), any(), any())).thenReturn(access, refresh); + + MvcResult mvcResult = mockMvc.perform(post("/login") + .contentType(MediaType.APPLICATION_JSON) + .content(messageBody)) + .andExpect(status().isOk()) + .andReturn(); + + Cookie accessCookie = mvcResult.getResponse().getCookie("access"); + Assertions.assertNotNull(accessCookie); + Assertions.assertEquals("Bearer+" + access, accessCookie.getValue()); + + Cookie refreshCookie = mvcResult.getResponse().getCookie("refresh"); + Assertions.assertNotNull(refreshCookie); + Assertions.assertEquals(refresh, refreshCookie.getValue()); + } + + @Test + public void TestAttemptAuthenticationFailed() throws Exception{ + Map testLogin = new LinkedHashMap<>(); + testLogin.put("user","user"); + testLogin.put("pw","1234"); + + ObjectMapper objectMapper = new ObjectMapper(); + String messageBody = objectMapper.writeValueAsString(testLogin); + + mockMvc.perform(post("/login") + .contentType(MediaType.APPLICATION_JSON) + .content(messageBody)) + .andExpect(result -> + Assertions.assertThrows(JsonFieldNotMatchException.class, () -> + authenticationManager.authenticate(any()) + )); + } +} + diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..5e12484 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,33 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb + username: sa + password: + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + format_sql: true + show_sql: true + redis: + host: 133.186.223.228 + password: "*N2vya7H@muDTwdNMR!" + port: 6379 + database: 29 + security: + key: sakdjA24HSdflasbdglag2yhsdrg342TASGASd58aw4t3AWEIGzsoigbaWEIGHP3tug0ajw4s23a8th24tgaw2854yq3p48ghaa294 + application: + name: eureka-client + +eureka: + instance: + prefer-ip-address: true + client: + register-with-eureka: true + fetch-registry: true + service-url: + defaultZone : http://127.0.0.1:8761/eureka + diff --git a/target/authentication-api-0.0.1-SNAPSHOT.jar b/target/authentication-api-0.0.1-SNAPSHOT.jar deleted file mode 100644 index 8f829b6..0000000 Binary files a/target/authentication-api-0.0.1-SNAPSHOT.jar and /dev/null differ diff --git a/target/authentication-api-0.0.1-SNAPSHOT.jar.original b/target/authentication-api-0.0.1-SNAPSHOT.jar.original deleted file mode 100644 index 213e813..0000000 Binary files a/target/authentication-api-0.0.1-SNAPSHOT.jar.original and /dev/null differ diff --git a/target/classes/application.properties b/target/classes/application.properties deleted file mode 100644 index e69de29..0000000 diff --git a/target/classes/com/t3t/authenticationapi/AuthenticationApiApplication.class b/target/classes/com/t3t/authenticationapi/AuthenticationApiApplication.class deleted file mode 100644 index a24489b..0000000 Binary files a/target/classes/com/t3t/authenticationapi/AuthenticationApiApplication.class and /dev/null differ diff --git a/target/classes/com/t3t/authenticationapi/index/IndexController.class b/target/classes/com/t3t/authenticationapi/index/IndexController.class deleted file mode 100644 index 8f1867f..0000000 Binary files a/target/classes/com/t3t/authenticationapi/index/IndexController.class and /dev/null differ diff --git a/target/classes/templates/index.html b/target/classes/templates/index.html deleted file mode 100644 index 84707d5..0000000 --- a/target/classes/templates/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - welcome page - - -

hello spring12!!

- - \ No newline at end of file diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties deleted file mode 100644 index cfe6bf6..0000000 --- a/target/maven-archiver/pom.properties +++ /dev/null @@ -1,3 +0,0 @@ -artifactId=authentication-api -groupId=com.t3t -version=0.0.1-SNAPSHOT diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst deleted file mode 100644 index e69de29..0000000 diff --git a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst deleted file mode 100644 index 1b2c5d8..0000000 --- a/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst +++ /dev/null @@ -1,2 +0,0 @@ -E:\authentication-api\src\main\java\com\t3t\authenticationapi\index\IndexController.java -E:\authentication-api\src\main\java\com\t3t\authenticationapi\AuthenticationApiApplication.java diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/createdFiles.lst deleted file mode 100644 index e69de29..0000000 diff --git a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst b/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst deleted file mode 100644 index e19bae5..0000000 --- a/target/maven-status/maven-compiler-plugin/testCompile/default-testCompile/inputFiles.lst +++ /dev/null @@ -1,2 +0,0 @@ -E:\authentication-api\src\test\java\com\t3t\authenticationapi\AuthenticationApiApplicationTests.java -E:\authentication-api\src\test\java\com\t3t\authenticationapi\controller\IndexController.java diff --git a/target/test-classes/com/t3t/authenticationapi/AuthenticationApiApplicationTests.class b/target/test-classes/com/t3t/authenticationapi/AuthenticationApiApplicationTests.class deleted file mode 100644 index 2a0cf9a..0000000 Binary files a/target/test-classes/com/t3t/authenticationapi/AuthenticationApiApplicationTests.class and /dev/null differ