Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/becooq81 step1 #5

Open
wants to merge 60 commits into
base: becooq81
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
4747c16
feat: auth controller
becooq81 Oct 10, 2023
7e65b88
feat: jwt code
becooq81 Oct 10, 2023
dfbcbc2
feat: token info
becooq81 Oct 10, 2023
2e4aae7
feat: login request
becooq81 Oct 10, 2023
63295b7
feat: jwt auth filter
becooq81 Oct 10, 2023
8d3bed6
feat: jwt token provider
becooq81 Oct 10, 2023
8578d10
feat: query dsl config
becooq81 Oct 10, 2023
9340f26
feat: security config
becooq81 Oct 10, 2023
50032c1
feat: swagger config
becooq81 Oct 10, 2023
91786bd
feat: error response
becooq81 Oct 10, 2023
0ac1b03
feat: global exception handler
becooq81 Oct 10, 2023
4e15829
feat: member controller
becooq81 Oct 10, 2023
c6d5666
feat: member domain
becooq81 Oct 10, 2023
b4d9902
feat: refresh token
becooq81 Oct 10, 2023
a57a047
feat: member response
becooq81 Oct 10, 2023
aa97e43
feat: signup request
becooq81 Oct 10, 2023
3fa0df6
feat: update member request
becooq81 Oct 10, 2023
8d2d636
feat: member repository
becooq81 Oct 10, 2023
d7e2aec
feat: refresh token repository
becooq81 Oct 10, 2023
840f8ff
feat: custom user details service
becooq81 Oct 10, 2023
b76e369
feat: member service interface
becooq81 Oct 10, 2023
2b3173a
feat: member service
becooq81 Oct 10, 2023
21cdbd7
feat: password matches validator
becooq81 Oct 10, 2023
e206db6
feat: validate email
becooq81 Oct 10, 2023
fc6fc9f
feat: constraints added
becooq81 Oct 10, 2023
9ddfaaf
fix: filter chain
becooq81 Oct 10, 2023
22cfdc7
fix: imports
becooq81 Oct 10, 2023
3709231
fix: delete redundant code
becooq81 Oct 10, 2023
fb904f0
fix
becooq81 Oct 10, 2023
76517ee
fix: user info
becooq81 Oct 11, 2023
4a154e8
fix
becooq81 Oct 11, 2023
9f8f91c
changes
becooq81 Oct 11, 2023
41d775c
feat: friendship
becooq81 Oct 10, 2023
e102f1e
refactor
becooq81 Oct 10, 2023
0650ab4
feat: friendship repository
becooq81 Oct 10, 2023
6a283db
fix: friendship repository
becooq81 Oct 10, 2023
eda53db
feat: friendship service
becooq81 Oct 10, 2023
f1a2c3b
feat: friendship service
becooq81 Oct 10, 2023
f8e1bf7
fix: entity name
becooq81 Oct 10, 2023
bcba278
feat: friendship request
becooq81 Oct 10, 2023
9ad2177
feat: friendship response
becooq81 Oct 10, 2023
0b96a47
feat(MemberService): find by username
becooq81 Oct 10, 2023
3bd27da
feat: friendship controller
becooq81 Oct 10, 2023
72e0314
fix: friendship request
becooq81 Oct 10, 2023
8e0d070
feat: converter
becooq81 Oct 10, 2023
7e112d1
feat: friendship service
becooq81 Oct 10, 2023
14a2723
fix: error
becooq81 Oct 10, 2023
804e0d5
fix: friendship
becooq81 Oct 10, 2023
3981db1
fix
becooq81 Oct 10, 2023
5bf6ec0
fix
becooq81 Oct 10, 2023
91c7447
fix
becooq81 Oct 11, 2023
c4a5e30
fix: git ignore
becooq81 Oct 11, 2023
114c83d
fixed untracked files
becooq81 Oct 10, 2023
f9047d2
fix
becooq81 Oct 10, 2023
6d218b2
fix: rebased due to errors in this branch
becooq81 Oct 11, 2023
53c4e50
feat: added email as index for query performance
becooq81 Oct 11, 2023
cdb5b0a
refactor: reorganized files
becooq81 Oct 11, 2023
340532d
fix: separated auth from member logic
becooq81 Oct 11, 2023
971b37f
fix: removed unused methods
becooq81 Oct 11, 2023
7bfbd56
fix: remove unused methods
becooq81 Oct 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
settings.gradle
/build/**
/.gradle/**
gradlew
gradlew.bat




### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/

*.exe
*.dll
61 changes: 61 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
plugins {
id 'java'
id 'org.springframework.boot' version '2.7.16'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.gdsc-ys'
version = '0.0.1-SNAPSHOT'

java {
sourceCompatibility = '11'
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'

implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.12'
implementation 'net.minidev:json-smart:2.4.9'

compileOnly 'org.projectlombok:lombok:1.18.30'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

// jwt
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'


implementation "com.querydsl:querydsl-jpa:5.0.0"
annotationProcessor "com.querydsl:querydsl-apt:5.0.0"
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

tasks.named('test') {
useJUnitPlatform()
}
45 changes: 45 additions & 0 deletions git.ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
settings.gradle
/build/**
/.gradle/**
gradlew
gradlew.bat




### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
11 changes: 11 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
<<<<<<< HEAD
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
=======
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
>>>>>>> f6ae741 (fix)
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
12 changes: 12 additions & 0 deletions src/main/java/com/gdscys/cokepoke/CokePokeApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.gdscys.cokepoke;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CokePokeApplication {
public static void main(String[] args) {
SpringApplication.run(CokePokeApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.gdscys.cokepoke.auth.controller;

import com.gdscys.cokepoke.auth.domain.TokenInfo;
import com.gdscys.cokepoke.auth.dto.LoginRequest;
import com.gdscys.cokepoke.auth.service.CustomUserDetailsService;
import com.gdscys.cokepoke.member.domain.Member;
import com.gdscys.cokepoke.member.dto.SignupRequest;
import com.gdscys.cokepoke.member.dto.MemberResponse;
import javax.validation.Valid;

import com.gdscys.cokepoke.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import static org.springframework.http.HttpHeaders.SET_COOKIE;

@Controller
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final MemberService memberService;
private final CustomUserDetailsService userDetailsService;

@PostMapping("/signup")
public ResponseEntity<MemberResponse> signup(@RequestBody @Valid SignupRequest request) {
Member member = memberService.saveMember(request.getEmail(), request.getUsername(), request.getPassword());
return ResponseEntity.status(HttpStatus.CREATED)
.body(MemberResponse.of(member));
}

@PostMapping(value = "/login")
public ResponseEntity<TokenInfo> login(@RequestBody @Valid LoginRequest loginRequest) {
TokenInfo tokenInfo = userDetailsService.login(loginRequest.getEmail(), loginRequest.getPassword());
return ResponseEntity.ok()
.header(SET_COOKIE, generateCookie("accessToken", tokenInfo.getAccessToken()).toString())
.header(SET_COOKIE, generateCookie("refreshToken", tokenInfo.getRefreshToken()).toString())
.body(tokenInfo);
}

private ResponseCookie generateCookie(String from, String token) {
return ResponseCookie.from(from, token)
.httpOnly(true) // false로 하면 클라이언트도 쿠키로 접근할 수 있기 때문에 보안상 조치
.path("/")
.build();
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/gdscys/cokepoke/auth/domain/JwtCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.gdscys.cokepoke.auth.domain;

public enum JwtCode {
ACCESS,EXPIRED,DENIED
}
16 changes: 16 additions & 0 deletions src/main/java/com/gdscys/cokepoke/auth/domain/TokenInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.gdscys.cokepoke.auth.domain;

import lombok.Getter;

@Getter
public class TokenInfo {
private String accessToken;
private String refreshToken;

protected TokenInfo() {}

public TokenInfo(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
23 changes: 23 additions & 0 deletions src/main/java/com/gdscys/cokepoke/auth/dto/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.gdscys.cokepoke.auth.dto;

import com.gdscys.cokepoke.validation.declaration.ValidEmail;
import lombok.Getter;

import javax.validation.constraints.NotBlank;

@Getter
public class LoginRequest {

@ValidEmail
private String email;

@NotBlank
private String password;

protected LoginRequest() {}

public LoginRequest(String email, String password) {
this.email = email;
this.password = password;
}
}
104 changes: 104 additions & 0 deletions src/main/java/com/gdscys/cokepoke/auth/jwt/JwtAuthFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package com.gdscys.cokepoke.auth.jwt;


import com.gdscys.cokepoke.auth.domain.JwtCode;
import com.gdscys.cokepoke.auth.domain.TokenInfo;
import com.gdscys.cokepoke.member.domain.RefreshToken;
import com.gdscys.cokepoke.member.repository.RefreshTokenRepository;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenRepository refreshTokenRepository;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Optional<String> accessToken = extractTokenFromCookie(request, "accessToken");
Optional<String> refreshToken = extractTokenFromCookie(request, "refreshToken");

// 유효한 토큰인지 확인합니다.
if (accessToken.isPresent() && jwtTokenProvider.validateToken(accessToken.get()) == JwtCode.ACCESS) {
// 토큰이 유효하면 토큰으로부터 유저 정보를 받아옵니다.
Authentication authentication = jwtTokenProvider.getAuthentication(accessToken.get());
// SecurityContext 에 Authentication 객체를 저장합니다.
SecurityContextHolder.getContext().setAuthentication(authentication);
} else if (accessToken.isPresent() && jwtTokenProvider.validateToken(accessToken.get()) == JwtCode.EXPIRED) {
log.info("Access token expired");

// refresh token 검증
if (refreshToken.isPresent() && jwtTokenProvider.validateToken(refreshToken.get()) == JwtCode.ACCESS) {

Optional<RefreshToken> savedToken = refreshTokenRepository.findByRefreshToken(refreshToken.get());

Claims claims = jwtTokenProvider.parseClaims(accessToken.get());

if (savedToken.isPresent() && claims.get("email").equals(savedToken.get().getMember().getEmail())) {
// accessToken 으로 부터 Authentication 객체 추출
Authentication authentication = jwtTokenProvider.getAuthentication(accessToken.get());

// email 을 추출하여 accessToken, refreshToken 생성
TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication, savedToken.get().getMember().getEmail());

// 인증 객체 설정
SecurityContextHolder.getContext().setAuthentication(authentication);

// refreshToken 업데이트
savedToken.get().setRefreshToken(tokenInfo.getRefreshToken());
refreshTokenRepository.save(savedToken.get());

response.addCookie(jwtTokenProvider.generateCookie("refreshToken", tokenInfo.getRefreshToken()));
response.addCookie(jwtTokenProvider.generateCookie("accessToken", tokenInfo.getAccessToken()));

log.info("Reissue access token");
}
}
}

filterChain.doFilter(request, response);
}

private Optional<String> extractTokenFromCookie(HttpServletRequest request, String cookieName) {

if (request.getCookies() == null || request.getCookies().length == 0) return Optional.empty();

return Arrays.stream(request.getCookies())
.sequential()
.filter(cookie -> cookie.getName().equals(cookieName))
.map(Cookie::getValue)
.findFirst();
}

@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
// request 에서 요청 path 추출
String path = request.getServletPath();

// filter 에서 제외한 url 목록
String[] excludedPaths = { "/auth/login", "/auth/signup", "/h2-console"};

for (String excludedPath : excludedPaths) {
if (path.startsWith(excludedPath)) {
return true;
}
}

return false;
}
}
Loading