diff --git a/backend/.gitignore b/backend/.gitignore index c2065bc2..a02a3af4 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -4,6 +4,8 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +.env +backend/src/main/resources/application-oauth.yml ### STS ### .apt_generated diff --git a/backend/build.gradle b/backend/build.gradle index bb7a2421..924bd97f 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -33,6 +33,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' annotationProcessor 'org.projectlombok:lombok' diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 00000000..6e569023 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3" + +services: + db: + image: mariadb:10 + ports: + - 3306:3306 + env_file: .env + environment: + TZ: Asia/Seoul + networks: + - backend + restart: always +networks: + backend: diff --git a/backend/src/main/java/com/twtw/backend/BackendApplication.java b/backend/src/main/java/com/twtw/backend/BackendApplication.java index 471009fa..d8a09665 100644 --- a/backend/src/main/java/com/twtw/backend/BackendApplication.java +++ b/backend/src/main/java/com/twtw/backend/BackendApplication.java @@ -5,8 +5,8 @@ @SpringBootApplication public class BackendApplication { - public static void main(String[] args) { + SpringApplication.run(BackendApplication.class, args); } diff --git a/backend/src/main/java/com/twtw/backend/config/security/SecurityConfig.java b/backend/src/main/java/com/twtw/backend/config/security/SecurityConfig.java new file mode 100644 index 00000000..22949c67 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/config/security/SecurityConfig.java @@ -0,0 +1,51 @@ +package com.twtw.backend.config.security; + +import com.twtw.backend.config.security.jwt.JwtAccessDeniedHandler; +import com.twtw.backend.config.security.jwt.JwtAuthenticationEntryPoint; +import com.twtw.backend.config.security.jwt.JwtFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig{ + private final JwtFilter jwtFilter; + private final JwtAccessDeniedHandler jwtAccessDeniedHandler; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Bean + public SecurityFilterChain configure(HttpSecurity http) throws Exception{ + return http.cors(cors -> cors.disable()).csrf(csrf -> csrf.disable()) + .httpBasic(h -> h.disable()) + .formLogin(f -> f.disable()) + .authorizeHttpRequests( + x->x.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html","auth/**") + .permitAll() + ) + .authorizeHttpRequests( + x->x.requestMatchers("/test/**") + .permitAll() + .anyRequest() + .authenticated() + ) + .sessionManagement(x -> x.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling( + x -> { + x.authenticationEntryPoint(jwtAuthenticationEntryPoint); + x.accessDeniedHandler(jwtAccessDeniedHandler); + } + ).build(); + } + +} diff --git a/backend/src/main/java/com/twtw/backend/config/security/jwt/JwtAccessDeniedHandler.java b/backend/src/main/java/com/twtw/backend/config/security/jwt/JwtAccessDeniedHandler.java new file mode 100644 index 00000000..e3ee61af --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/config/security/jwt/JwtAccessDeniedHandler.java @@ -0,0 +1,18 @@ +package com.twtw.backend.config.security.jwt; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class JwtAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + } +} diff --git a/backend/src/main/java/com/twtw/backend/config/security/jwt/JwtAuthenticationEntryPoint.java b/backend/src/main/java/com/twtw/backend/config/security/jwt/JwtAuthenticationEntryPoint.java new file mode 100644 index 00000000..b346f2a5 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/config/security/jwt/JwtAuthenticationEntryPoint.java @@ -0,0 +1,19 @@ +package com.twtw.backend.config.security.jwt; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED); + } +} diff --git a/backend/src/main/java/com/twtw/backend/config/security/jwt/JwtFilter.java b/backend/src/main/java/com/twtw/backend/config/security/jwt/JwtFilter.java new file mode 100644 index 00000000..3a3c1fba --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/config/security/jwt/JwtFilter.java @@ -0,0 +1,46 @@ +package com.twtw.backend.config.security.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtFilter extends OncePerRequestFilter { + + private final TokenProvider tokenProvider; + private final String AUTHORIZATION_HEADER = "Authorization"; + private final String BEARER_PREFIX = "Bearer "; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String jwt = resolveToken(request); + + if(StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) + { + Authentication authentication = tokenProvider.getAuthentication(jwt); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(request,response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + + if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) + { + return bearerToken.substring(7); + } + + return null; + } +} diff --git a/backend/src/main/java/com/twtw/backend/config/security/jwt/TokenProvider.java b/backend/src/main/java/com/twtw/backend/config/security/jwt/TokenProvider.java new file mode 100644 index 00000000..3a27ad6a --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/config/security/jwt/TokenProvider.java @@ -0,0 +1,120 @@ +package com.twtw.backend.config.security.jwt; +import com.twtw.backend.domain.member.dto.response.TokenDto; +import com.twtw.backend.domain.member.entity.Member; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Component +public class TokenProvider implements InitializingBean { + + private Key key; + private final String secretKey; + private static final String AUTHORITIES_KEY = "auth"; + private static final Long ACCESS_TOKEN_EXPIRE_LENGTH = 60L * 60 * 24 * 1000; // 1 Day + private static final Long REFRESH_TOKEN_EXPIRE_LENGTH = 60L * 60 * 24 * 14 * 1000; // 14 Days + + public TokenProvider( + @Value("${jwt.secret}") String secretKey + ){ + this.secretKey = secretKey; + } + + @Override + public void afterPropertiesSet() throws Exception { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + + public TokenDto createToken(Authentication authentication) { + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + Long now = (new Date()).getTime(); + Date validAccessDate = new Date(now + ACCESS_TOKEN_EXPIRE_LENGTH); + Date validRefreshDate = new Date(now + REFRESH_TOKEN_EXPIRE_LENGTH); + + String accessToken = Jwts.builder() + .setSubject(authentication.getName()) + .claim(AUTHORITIES_KEY,authorities) + .signWith(key, SignatureAlgorithm.HS512) + .setExpiration(validAccessDate) + .compact(); + + String refreshToken = Jwts.builder() + .setExpiration(validRefreshDate) + .signWith(key,SignatureAlgorithm.HS512) + .compact(); + + return new TokenDto(accessToken,refreshToken); + } + + public Authentication getAuthentication(String accessToken) { + Claims claims = parseClaims(accessToken); + + if(claims.get(AUTHORITIES_KEY) == null) + { + throw new RuntimeException("권한 정보가 없는 토큰입니다."); + } + + Collection authorities = new ArrayList<>(); + String role = claims.get(AUTHORITIES_KEY).toString(); + + if(role.equals("ROLE_ADMIN")) { + authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); + } + + else if(role.equals("ROLE_USER")){ + authorities.add(new SimpleGrantedAuthority("ROLE_USER")); + } + + return new UsernamePasswordAuthenticationToken(claims.getSubject(),"",authorities); + + } + + public boolean validateToken(String token) { + try{ + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + }catch (ExpiredJwtException | UnsupportedJwtException | IllegalStateException e) { + return false; + } + } + + private Claims parseClaims(String accessToken) { + try{ + Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); + return claims; + }catch (ExpiredJwtException e) + { + return e.getClaims(); + } + } + + public UsernamePasswordAuthenticationToken makeCredit(Member member) + { + List role = new ArrayList<>(); + role.add(new SimpleGrantedAuthority(member.getRole().toString())); + UsernamePasswordAuthenticationToken credit = new UsernamePasswordAuthenticationToken(member.getId().toString(),"",role); + + return credit; + } + +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/SwaggerConfiguration.java b/backend/src/main/java/com/twtw/backend/domain/member/SwaggerConfiguration.java new file mode 100644 index 00000000..51baaf7c --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/SwaggerConfiguration.java @@ -0,0 +1,31 @@ +package com.twtw.backend.domain.member; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import java.util.List; + +@Configuration +public class SwaggerConfiguration { + @Bean + public OpenAPI getOpenAPI() { + return new OpenAPI() + .info( + new Info().title("TWTW") + .description("Hong Dam Jin") + .version("v0.0.1") + ) + .components( + new Components().addSecuritySchemes( + "bearerAuth", new SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT") + .in(SecurityScheme.In.HEADER).name("Authorization") + ) + ).security( + List.of(new SecurityRequirement().addList("bearerAuth")) + ); + } +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/controller/AuthController.java b/backend/src/main/java/com/twtw/backend/domain/member/controller/AuthController.java new file mode 100644 index 00000000..7e5f8798 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/controller/AuthController.java @@ -0,0 +1,45 @@ +package com.twtw.backend.domain.member.controller; + +import com.twtw.backend.domain.member.dto.request.MemberSaveRequest; +import com.twtw.backend.domain.member.dto.response.TokenDto; +import com.twtw.backend.domain.member.service.AuthService; +import com.twtw.backend.domain.member.dto.request.OAuthRequest; +import com.twtw.backend.domain.member.dto.request.TokenRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth") +public class AuthController { + private final AuthService authService; + + public AuthController(AuthService authService) { + this.authService = authService; + } + + @PostMapping("/refresh") + public ResponseEntity authorize(@RequestBody TokenRequest tokenRequest) { + return ResponseEntity.ok(authService.refreshToken(tokenRequest.getAccessToken(),tokenRequest.getRefreshToken())); + } + + @PostMapping("/save") + public ResponseEntity saveMember(@RequestBody MemberSaveRequest memberSaveRequest) { + TokenDto tokenDto = authService.saveMember(memberSaveRequest); + + return ResponseEntity.status(HttpStatus.OK).body(tokenDto); + } + + @PostMapping("/login") + public ResponseEntity afterSocialLogin(@RequestBody OAuthRequest request){ + TokenDto tokenDto = authService.getTokenByOAuth(request); + + if(tokenDto == null){ + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(tokenDto); + } + + else{ + return ResponseEntity.status(HttpStatus.OK).body(tokenDto); + } + } +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/controller/MemberController.java b/backend/src/main/java/com/twtw/backend/domain/member/controller/MemberController.java new file mode 100644 index 00000000..f02684b1 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/controller/MemberController.java @@ -0,0 +1,17 @@ +package com.twtw.backend.domain.member.controller; + +import com.twtw.backend.domain.member.service.MemberService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/member") +public class MemberController { + private final MemberService memberService; + public MemberController(MemberService memberService) + { + this.memberService = memberService; + } + + +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/dto/client/KakaoResponse.java b/backend/src/main/java/com/twtw/backend/domain/member/dto/client/KakaoResponse.java new file mode 100644 index 00000000..8a2fc40d --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/dto/client/KakaoResponse.java @@ -0,0 +1,10 @@ +package com.twtw.backend.domain.member.dto.client; + +import lombok.*; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class KakaoResponse { + public long id; +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/dto/request/MemberSaveRequest.java b/backend/src/main/java/com/twtw/backend/domain/member/dto/request/MemberSaveRequest.java new file mode 100644 index 00000000..3b8462a4 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/dto/request/MemberSaveRequest.java @@ -0,0 +1,24 @@ +package com.twtw.backend.domain.member.dto.request; + +import com.twtw.backend.domain.member.entity.Role; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MemberSaveRequest { + private String nickname; + + private String profileImage; + + private String phoneNumber; + + @Enumerated(EnumType.STRING) + private Role role; + + private OAuthRequest oauthRequest; +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/dto/request/OAuthRequest.java b/backend/src/main/java/com/twtw/backend/domain/member/dto/request/OAuthRequest.java new file mode 100644 index 00000000..8e0f2edd --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/dto/request/OAuthRequest.java @@ -0,0 +1,16 @@ +package com.twtw.backend.domain.member.dto.request; + +import com.twtw.backend.domain.member.entity.AuthType; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +public class OAuthRequest { + private String token; + + @Enumerated(EnumType.STRING) + private AuthType authType; +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/dto/request/TokenRequest.java b/backend/src/main/java/com/twtw/backend/domain/member/dto/request/TokenRequest.java new file mode 100644 index 00000000..1b714497 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/dto/request/TokenRequest.java @@ -0,0 +1,11 @@ +package com.twtw.backend.domain.member.dto.request; + +import lombok.*; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class TokenRequest { + private String accessToken; + private String refreshToken; +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/dto/response/TokenDto.java b/backend/src/main/java/com/twtw/backend/domain/member/dto/response/TokenDto.java new file mode 100644 index 00000000..b923e6f8 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/dto/response/TokenDto.java @@ -0,0 +1,11 @@ +package com.twtw.backend.domain.member.dto.response; + +import lombok.*; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class TokenDto { + private String accessToken; + private String refreshToken; +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/entity/AuthType.java b/backend/src/main/java/com/twtw/backend/domain/member/entity/AuthType.java new file mode 100644 index 00000000..ec50c355 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/entity/AuthType.java @@ -0,0 +1,6 @@ +package com.twtw.backend.domain.member.entity; + +public enum AuthType { + APPLE, + KAKAO +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/entity/Member.java b/backend/src/main/java/com/twtw/backend/domain/member/entity/Member.java new file mode 100644 index 00000000..a5b6e04e --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/entity/Member.java @@ -0,0 +1,42 @@ +package com.twtw.backend.domain.member.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Member { + @Id + @GeneratedValue(generator = "uuid2") + @Column(name = "id",columnDefinition = "BINARY(16)") + private UUID id; + + private String nickname; + + private String profileImage; + + private String phoneNumber; + + @Enumerated(EnumType.STRING) + private Role role; + + @Embedded + private OAuth2Info oAuth2Info; + + + public Member(String nickname,String profileImage,String phoneNumber,Role role) { + this.nickname = nickname; + this.profileImage = profileImage; + this.phoneNumber = phoneNumber; + this.role = role; + } + + public void updateOAuth(OAuth2Info oAuth2Info){ + this.oAuth2Info = oAuth2Info; + } +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/entity/OAuth2Info.java b/backend/src/main/java/com/twtw/backend/domain/member/entity/OAuth2Info.java new file mode 100644 index 00000000..df94e06a --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/entity/OAuth2Info.java @@ -0,0 +1,39 @@ +package com.twtw.backend.domain.member.entity; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +@Getter +@Embeddable +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class OAuth2Info { + + @Lob + @Column(nullable = false) + private String clientId; + + @Enumerated(value = EnumType.STRING) + private AuthType authType; + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + OAuth2Info tmp = (OAuth2Info) o; + + return Objects.equals(clientId,tmp.clientId) && authType == tmp.authType; + } + + @Override + public int hashCode() { + return Objects.hash(clientId,authType); + } +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/entity/RefreshToken.java b/backend/src/main/java/com/twtw/backend/domain/member/entity/RefreshToken.java new file mode 100644 index 00000000..c7f03216 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/entity/RefreshToken.java @@ -0,0 +1,30 @@ +package com.twtw.backend.domain.member.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RefreshToken { + @Id + @GeneratedValue(generator = "uuid2") + @Column(name = "id",columnDefinition = "BINARY(16)") + private UUID id; + private String tokenKey; + + private String tokenValue; + + public RefreshToken(String tokenKey,String tokenValue) + { + this.tokenKey = tokenKey; + this.tokenValue = tokenValue; + } +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/entity/Role.java b/backend/src/main/java/com/twtw/backend/domain/member/entity/Role.java new file mode 100644 index 00000000..1a1f3e2e --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/entity/Role.java @@ -0,0 +1,6 @@ +package com.twtw.backend.domain.member.entity; + +public enum Role { + ROLE_ADMIN, + ROLE_USER +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/mapper/MemberMapper.java b/backend/src/main/java/com/twtw/backend/domain/member/mapper/MemberMapper.java new file mode 100644 index 00000000..566ffcdd --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/mapper/MemberMapper.java @@ -0,0 +1,24 @@ +package com.twtw.backend.domain.member.mapper; + +import com.twtw.backend.domain.member.dto.request.MemberSaveRequest; +import com.twtw.backend.domain.member.entity.AuthType; +import com.twtw.backend.domain.member.entity.Member; +import com.twtw.backend.domain.member.entity.OAuth2Info; +import org.springframework.stereotype.Component; + +@Component +public class MemberMapper { + + public Member toMemberEntity(MemberSaveRequest request) + { + Member member = new Member(request.getNickname(), request.getProfileImage(), request.getPhoneNumber(), request.getRole()); + return member; + } + + public OAuth2Info toOAuthInfo(String clientId, AuthType type) + { + OAuth2Info info = new OAuth2Info(clientId,type); + + return info; + } +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/repository/MemberRepository.java b/backend/src/main/java/com/twtw/backend/domain/member/repository/MemberRepository.java new file mode 100644 index 00000000..76f8346c --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/repository/MemberRepository.java @@ -0,0 +1,17 @@ +package com.twtw.backend.domain.member.repository; + + +import com.twtw.backend.domain.member.entity.AuthType; +import com.twtw.backend.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; +import java.util.UUID; + +public interface MemberRepository extends JpaRepository { + Optional findById(UUID uuid); + + @Query("SELECT m FROM Member m WHERE m.oAuth2Info.clientId = :oAuthId AND m.oAuth2Info.authType = :authType") + Optional findByOAuthIdAndAuthType(String oAuthId, AuthType authType); +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/repository/RefreshTokenRepository.java b/backend/src/main/java/com/twtw/backend/domain/member/repository/RefreshTokenRepository.java new file mode 100644 index 00000000..451e539c --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/repository/RefreshTokenRepository.java @@ -0,0 +1,7 @@ +package com.twtw.backend.domain.member.repository; + +import com.twtw.backend.domain.member.entity.RefreshToken; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RefreshTokenRepository extends JpaRepository { +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/service/AuthService.java b/backend/src/main/java/com/twtw/backend/domain/member/service/AuthService.java new file mode 100644 index 00000000..b540c2f8 --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/service/AuthService.java @@ -0,0 +1,111 @@ +package com.twtw.backend.domain.member.service; + +import com.twtw.backend.domain.member.dto.request.MemberSaveRequest; +import com.twtw.backend.domain.member.dto.response.TokenDto; +import com.twtw.backend.domain.member.entity.Member; +import com.twtw.backend.domain.member.entity.RefreshToken; +import com.twtw.backend.config.security.jwt.TokenProvider; +import com.twtw.backend.domain.member.mapper.MemberMapper; +import com.twtw.backend.domain.member.repository.RefreshTokenRepository; +import com.twtw.backend.domain.member.dto.request.OAuthRequest; +import com.twtw.backend.domain.member.repository.MemberRepository; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +public class AuthService { + private final MemberRepository memberRepository; + private final RefreshTokenRepository refreshTokenRepository; + private final TokenProvider tokenProvider; + private final MemberMapper memberMapper; + + public AuthService(MemberRepository memberRepository,RefreshTokenRepository refreshTokenRepository,TokenProvider tokenProvider,MemberMapper memberMapper) { + this.memberRepository = memberRepository; + this.refreshTokenRepository = refreshTokenRepository; + this.tokenProvider = tokenProvider; + this.memberMapper = memberMapper; + } + + /* + * 1. after social SignUp + * 2. 기본 정보 기입 + * 3. OAuth Info 저장 -> kakao , apple enum 구분 + * 4. 저장 + * 5. 토큰(jwt) 발급 + * */ + + @Transactional + public TokenDto saveMember(MemberSaveRequest request){ + Member member = memberMapper.toMemberEntity(request); + + String clientId = request.getOauthRequest().getToken(); + + member.updateOAuth(memberMapper.toOAuthInfo(clientId,request.getOauthRequest().getAuthType())); + memberRepository.save(member); + + UsernamePasswordAuthenticationToken credit = tokenProvider.makeCredit(member); + TokenDto tokenDto = saveRefreshToken(credit,member.getId().toString()); + + return tokenDto; + } + /* + * 1.로그인(Social) 후의 토큰 발급 + * 2.JWT 토큰 발급 -> OAuth 정보 (clientId , AuthType)으로 진행 + * + * */ + public TokenDto getTokenByOAuth(OAuthRequest request) { + String clientId = request.getToken(); + + Optional member = memberRepository.findByOAuthIdAndAuthType(clientId,request.getAuthType()); + + if(member.isPresent()) { + Member curMember = member.get(); + UsernamePasswordAuthenticationToken credit = tokenProvider.makeCredit(curMember); + TokenDto tokenDto = saveRefreshToken(credit,curMember.getId().toString()); + return tokenDto; + } + + return null; + } + + + /* + * Token 재발급 + * 1. refreshToken validate 확인 + * 2. refreshToken DB 정보 확인 + * 3. 토큰 만들어서 반환 + * */ + + public TokenDto refreshToken(String accessToken, String refreshToken){ + if(!tokenProvider.validateToken(refreshToken)) { + throw new RuntimeException("Refresh Token이 유효하지 않습니다."); + } + + Authentication authentication = tokenProvider.getAuthentication(accessToken); + + String userName = authentication.getName(); + + if(!getRefreshTokenValue(userName).equals(refreshToken)) { + throw new RuntimeException("Refresh Token 정보가 일치하지 않습니다."); + } + + return saveRefreshToken(authentication,userName); + } + + public String getRefreshTokenValue(String tokenKey) { + return refreshTokenRepository.getReferenceById(tokenKey).getTokenValue(); + } + + public TokenDto saveRefreshToken(Authentication authentication,String userName){ + TokenDto token = tokenProvider.createToken(authentication); + refreshTokenRepository.save(new RefreshToken(userName,token.getRefreshToken())); + + return token; + } + +} diff --git a/backend/src/main/java/com/twtw/backend/domain/member/service/MemberService.java b/backend/src/main/java/com/twtw/backend/domain/member/service/MemberService.java new file mode 100644 index 00000000..c2554b1e --- /dev/null +++ b/backend/src/main/java/com/twtw/backend/domain/member/service/MemberService.java @@ -0,0 +1,13 @@ +package com.twtw.backend.domain.member.service; + +import com.twtw.backend.domain.member.repository.MemberRepository; +import org.springframework.stereotype.Service; + +@Service +public class MemberService { + private final MemberRepository memberRepository; + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties deleted file mode 100644 index 8b137891..00000000 --- a/backend/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ - diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 00000000..d9820dc6 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,15 @@ +spring: + profiles: + include: env + datasource: + driver-class-name: org.mariadb.jdbc.Driver + url: jdbc:mariadb://localhost:3306/TWTW + username: root + password: root! + jpa: + database: mysql + generate-ddl: true + show-sql: true + hibernate: + ddl-auto: create +