Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8d0f940
feat: 페이크 회원가입 구현
Cho-SangHyun Jan 29, 2023
ef1e197
feat: 이메일로 찾기 구현
Cho-SangHyun Jan 29, 2023
aba2dcd
test: 페이크 회원가입 테스트 구현
Cho-SangHyun Jan 29, 2023
f7762a1
feat: 토큰 발급 기능 구현
Cho-SangHyun Jan 29, 2023
06d0263
feat: 토큰 검증 기능 구현
Cho-SangHyun Jan 29, 2023
5861ff5
test: 토큰 검증 테스트 구현
Cho-SangHyun Jan 29, 2023
aad0351
feat: 헤더로부터 토큰 가져오기 및 이메일 빼내기 구현
Cho-SangHyun Jan 29, 2023
a006779
test: 토큰에서 이메일 가져오는 테스트 구현
Cho-SangHyun Jan 29, 2023
c0f9401
feat: 로그인시 비밀번호 인코딩 기능 추가
Cho-SangHyun Feb 6, 2023
574213b
feat: 회원가입 시 이메일 중복검사 구현
Cho-SangHyun Feb 6, 2023
73a64b3
test: 회원가입시 이메일 중복테스트 추가
Cho-SangHyun Feb 6, 2023
47e26e7
refactor: 토큰관련기능 TokenProvider로 합침
Cho-SangHyun Feb 6, 2023
b6659c7
feat: 토큰관련테스트 리팩토링
Cho-SangHyun Feb 6, 2023
9b25dc3
rename: MemberForm에서 SignupDTO로 이름변경
Cho-SangHyun Feb 6, 2023
a4c0963
feat: 페이크 로그인 구현
Cho-SangHyun Feb 6, 2023
fe7a0dd
test: 회원가입테스트 리팩토링
Cho-SangHyun Feb 6, 2023
417e71f
test: 로그인 관련 테스트 추가
Cho-SangHyun Feb 6, 2023
2e8d047
fix: 비밀번호판별기능 수정
Cho-SangHyun Feb 6, 2023
459f0d6
feat: 시큐리티 적용
Cho-SangHyun Feb 6, 2023
96e695c
feat: 로그인 성공시 ResponseEntity를 반환하도록 구현
Cho-SangHyun Feb 6, 2023
a09faf6
feat: 인터셉터로 응답값 통일
Cho-SangHyun Feb 6, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ out/

### VS Code ###
.vscode/

### etc
**/application.properties
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ 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-web'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Empty file modified gradlew
100755 → 100644
Empty file.
182 changes: 91 additions & 91 deletions gradlew.bat
Original file line number Diff line number Diff line change
@@ -1,91 +1,91 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.dku.springstudy.config;

import com.dku.springstudy.jwt.JwtTokenProvider;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
@Value("${jwt.secret}")
private String jwtSecretKey;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 파라미터로 받은 request, response 객체들이 한 번 읽으면 없어지는 애들이라 한 번 감싼 애들을 만들어준다. 일종의 복제
ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper wrappingResponse = new ContentCachingResponseWrapper(response);

String token = tokenProvider.getTokenFromHeader(request);
if (token != null && tokenProvider.validateToken(token, jwtSecretKey)) {
Authentication authentication = tokenProvider.getAuthentication(token, jwtSecretKey);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(wrappingRequest, wrappingResponse);

// 필터엔 복제한 애들 넘기기. 인터셉터도 래핑된 애들을 파라미터로 받음
filterChain.doFilter(request, wrappingResponse);
// 마지막에 클라이언트에게 response나갈 때 wrapping한 내용을 써줌(복제본 내용을 원본에 쓴다!)
wrappingResponse.copyBodyToResponse();
}
}
43 changes: 43 additions & 0 deletions src/main/java/com/dku/springstudy/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.dku.springstudy.config;

import com.dku.springstudy.jwt.JwtTokenProvider;
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.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity // 기본적인 웹 보안을 활성화하는 어노테이션
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider tokenProvider;

@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf().disable() // CSRF 공격에 대한 방어를 해제
.cors().and() // cors 허용
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 서버를 stateless하게 유지 즉 세션 X
.and()
.formLogin().disable() // 시큐리티가 기본 제공하는 로그인 화면 없앰. JWT는 로그인과정을 수동으로 클래스로 만들어야 하니까
.httpBasic().disable() // 토큰 방식을 이용할 것이므로 보안에 취약한 HttpBasic은 꺼두기
.authorizeRequests() // HttpServletRequest를 사용하는 요청들에 대한 접근제한을 설정하겠다
.requestMatchers("/account/signup", "/account/login", "/").permitAll() // 요 세 놈에 대한 요청은 인증없이 접근 허용
.anyRequest().authenticated() // 나머지에 대해선 인증을 받아야 한다.
.and()
// 여러 필터들 중 UsernamePassword필터 앞에 내가 만든 필터를 둔다. 이렇게 하면 커스텀 필터로 인가인증을 다룰 수 있음
.addFilterBefore(new JwtAuthenticationFilter(tokenProvider),
UsernamePasswordAuthenticationFilter.class)
.build();
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/dku/springstudy/config/SpringConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.dku.springstudy.config;

import com.dku.springstudy.repository.JpaMemberRepository;
import com.dku.springstudy.repository.MemberRepository;
import com.dku.springstudy.service.MemberService;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {
private final EntityManager em;

@Autowired
public SpringConfig(EntityManager em) {
this.em = em;
}

@Bean
public MemberRepository memberRepository() {
return new JpaMemberRepository(em);
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/dku/springstudy/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dku.springstudy.config;

import com.dku.springstudy.interceptor.MyInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final MyInterceptor myInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor)
.addPathPatterns("/**");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.dku.springstudy.controller;

import com.dku.springstudy.dto.LoginDTO;
import com.dku.springstudy.dto.LoginResponseDTO;
import com.dku.springstudy.jwt.JwtTokenProvider;
import com.dku.springstudy.model.Member;
import com.dku.springstudy.dto.SignupDTO;
import com.dku.springstudy.model.Role;
import com.dku.springstudy.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value = "/account")
@RequiredArgsConstructor
public class AccountController {
private final JwtTokenProvider tokenProvider;
private final PasswordEncoder passwordEncoder;
private final MemberService memberService;

@Value("${jwt.secret}")
private String jwtSecretKey;
private final long EXPIRED_MS = 30 * 60 * 1000L;

@PostMapping("/signup")
public String signup(SignupDTO memberForm) {
String rawPassword = memberForm.getPassword();
String encodedPassword = passwordEncoder.encode(rawPassword);

Member newMember = new Member();
newMember.setEmail(memberForm.getEmail());
newMember.setPassword(encodedPassword);
newMember.setNickname(memberForm.getNickname());
newMember.setRole(Role.USER);
memberService.join(newMember);

return "success";
}

@PostMapping("/login")
public LoginResponseDTO login(@RequestBody LoginDTO loginInfo) {
String loginEmail = loginInfo.getEmail();
String loginRawPassword = loginInfo.getPassword();
if (!memberService.login(loginEmail, loginRawPassword)) {
throw new IllegalStateException("로그인에 실패했습니다.");
}

String accessToken = tokenProvider.createToken(loginEmail, jwtSecretKey, EXPIRED_MS);
LoginResponseDTO loginResponseDTO = new LoginResponseDTO(accessToken);

return loginResponseDTO;
}
}
Loading