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 step2 #6

Open
wants to merge 49 commits into
base: becooq81
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 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
944fbdf
feat: friendship
becooq81 Oct 10, 2023
3d072ca
refactor
becooq81 Oct 10, 2023
586f77c
feat: friendship repository
becooq81 Oct 10, 2023
c47ae0f
fix: friendship repository
becooq81 Oct 10, 2023
ec6a5b8
feat: friendship service
becooq81 Oct 10, 2023
375be84
feat: friendship service
becooq81 Oct 10, 2023
22da1a3
fix: entity name
becooq81 Oct 10, 2023
a0bc06c
feat: friendship request
becooq81 Oct 10, 2023
357ae96
feat: friendship response
becooq81 Oct 10, 2023
6fbbc4a
feat(MemberService): find by username
becooq81 Oct 10, 2023
33bd72c
feat: friendship controller
becooq81 Oct 10, 2023
9c8f18b
fix: friendship request
becooq81 Oct 10, 2023
3872955
feat: converter
becooq81 Oct 10, 2023
cb77fc4
feat: friendship service
becooq81 Oct 10, 2023
9e39d39
fix: error
becooq81 Oct 10, 2023
9c1df6b
fix: friendship
becooq81 Oct 10, 2023
ef9a13b
fix
becooq81 Oct 10, 2023
e03b676
fix
becooq81 Oct 10, 2023
f6ae741
fix
becooq81 Oct 10, 2023
649d7bf
fixed untracked files
becooq81 Oct 10, 2023
037cc91
fix
becooq81 Oct 10, 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
45 changes: 45 additions & 0 deletions .gitignore
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/
52 changes: 52 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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"

}

tasks.named('test') {
useJUnitPlatform()
}
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
7 changes: 7 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
13 changes: 13 additions & 0 deletions src/main/java/com/gdscys/cokepoke/CokePokeApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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,53 @@
package com.gdscys.cokepoke.auth;

import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.web.context.HttpRequestResponseHolder;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;

public class DelegatingSecurityContextRepository implements SecurityContextRepository {
private final List<SecurityContextRepository> delegates;

public DelegatingSecurityContextRepository(SecurityContextRepository... delegates) {
this(Arrays.asList(delegates));
}

public DelegatingSecurityContextRepository(List<SecurityContextRepository> delegates) {
Assert.notEmpty(delegates, "delegates cannot be empty");
this.delegates = delegates;
}

@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
SecurityContext result = null;
for (SecurityContextRepository delegate : this.delegates) {
SecurityContext delegateResult = delegate.loadContext(requestResponseHolder);
if (result == null || delegate.containsContext(requestResponseHolder.getRequest())) {
result = delegateResult;
}
}
return result;
}

@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
for (SecurityContextRepository delegate : this.delegates) {
delegate.saveContext(context, request, response);
}
}

@Override
public boolean containsContext(HttpServletRequest request) {
for (SecurityContextRepository delegate : this.delegates) {
if (delegate.containsContext(request)) {
return true;
}
}
return false;
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/gdscys/cokepoke/auth/SecurityUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.gdscys.cokepoke.auth;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

public class SecurityUtil {
public static String getLoginUsername(){
Authentication loggedInUser = SecurityContextHolder.getContext().getAuthentication();
String username = loggedInUser.getName();
return username;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.gdscys.cokepoke.auth.controller;

import com.gdscys.cokepoke.auth.domain.TokenInfo;
import com.gdscys.cokepoke.auth.dto.LoginRequest;
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;

@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 = memberService.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