-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* build : security 의존성 추가 * feat : security config 추가 * docs : jwt 시크릿 키 추가 * build : jwt 사용을 위한 라이브러리 추가 * feat : jwtFilter 등록 * feat : jwt 구현 * feat : jwtFilter 구현 * docs : jwt 구현 * docs : 카카오 Http 요청을 위한 webflux 추가 * feat : member 도메인 구현 * feat : memberEntity 구현 * feat : 카카오 엑세스 토큰, 유저 정보 가져오기 구현 * feat : 멤버 Role 관리를 위한 enum 구현 * remove : 초기 세팅 테스트용으로 작성된 코드 제거 * feat : 닉네임 중복 확인 구현 * feat : 닉네임 중복 확인 구현 * feat : 닉네임 중복 에러 구현 * refactor : 카카오 계정 정보 가져오는 로직 변경 * feat : 회원가입, 로그인 구현 * feat : 회원가입, 로그인 구현 * feat : id 토큰을 이용한 로그인 구현 * refactor : email과 이름, 전화번호, 레벨 권한 문제로 인해 nullable 처리 * refactor : url과 clientId 키 값 분리 * feat : Authentication를 상속받은 jwt 토큰 구현 * refactor : 권한 문제로 인해 idToken값 비교로 변경 * docs : oauth를 위한 환경 변수 추가 * fix : SecretKey 타입 차이로 인한 공백 오류 해결 * refactor : 로그인, 회원가입 JWT 사용해서 구현 * feat : 빌더 패턴 추가 * refactor : Member 변경에 따른 get 메소드 변경 * fix : spotless 포맷팅 오류 해결 * fix : 닉네임 중복 Exception 수정 * fix : swagger jwtFilter 오류 해결 * fix : 기존 회원 정보가 없을 경우 발생하는 에러 수정 * refactor : JwtFilter 개발 환경에서 모든 api 허용 * fix : my_team nullable로 인한 오류 해결(추 후 해당 필드와 컬럼 삭제 예정) * feat: jwt 파일 분리 * feat: gitignore update * feat: update gitignore * Delete application/src/main/resources/application-jwt.yml * feat: add kakao yml to gitignore * fix: properties mapping 이슈 해결 --------- Co-authored-by: EunjiShin <eunji980310@gmail.com> Co-authored-by: 우디 <38103085+EunjiShin@users.noreply.github.com>
- Loading branch information
1 parent
1ae7458
commit 50dcd96
Showing
27 changed files
with
743 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,4 +39,6 @@ bin/ | |
.vscode/ | ||
|
||
### Mac OS ### | ||
.DS_Store | ||
.DS_Store | ||
|
||
*.application-jwt.yml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
application/src/main/java/org/depromeet/spot/application/common/config/SecurityConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package org.depromeet.spot.application.common.config; | ||
|
||
import org.depromeet.spot.application.common.jwt.JwtAuthenticationFilter; | ||
import org.depromeet.spot.application.common.jwt.JwtTokenUtil; | ||
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.annotation.web.configurers.AbstractHttpConfigurer; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
|
||
@Configuration | ||
@EnableWebSecurity | ||
@RequiredArgsConstructor | ||
public class SecurityConfig { | ||
|
||
private final JwtTokenUtil jwtTokenUtil; | ||
|
||
@Bean | ||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | ||
http | ||
// cross-site -> stateless라서 필요 없음. | ||
.csrf(AbstractHttpConfigurer::disable) | ||
// 초기 로그인 화면 필요 없음. | ||
.formLogin(AbstractHttpConfigurer::disable) | ||
// 토큰 방식을 사용하므로 httpBasic도 제거. | ||
.httpBasic(AbstractHttpConfigurer::disable) | ||
.authorizeHttpRequests( | ||
authorize -> | ||
authorize | ||
// 테스트, 개발 중엔 모든 경로 오픈. | ||
.requestMatchers("/**") | ||
.permitAll()) | ||
// UsernamePasswordAuthenticationFilter 필터 전에 jwt 필터가 먼저 동작하도록함. | ||
.addFilterBefore( | ||
new JwtAuthenticationFilter(jwtTokenUtil), | ||
UsernamePasswordAuthenticationFilter.class); | ||
return http.build(); | ||
} | ||
} |
69 changes: 69 additions & 0 deletions
69
...tion/src/main/java/org/depromeet/spot/application/common/jwt/JwtAuthenticationFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package org.depromeet.spot.application.common.jwt; | ||
|
||
import java.io.IOException; | ||
import java.util.List; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
|
||
import org.depromeet.spot.domain.member.enums.MemberRole; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
import org.springframework.web.server.ResponseStatusException; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
|
||
@RequiredArgsConstructor | ||
@Slf4j | ||
@Component | ||
public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||
|
||
private final JwtTokenUtil jwtTokenUtil; | ||
|
||
@Override | ||
protected void doFilterInternal( | ||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||
throws ServletException, IOException { | ||
|
||
List<String> list = | ||
List.of( | ||
// swagger-ui와 v3/api-docs는 스웨거를 제외하기 위해 등록. | ||
// 혹시나 스웨거 자원 사용 에러 발생 시 아래 두 가지 추가 필요함. | ||
// Swagger UI에서 사용하는 외부 라ㅇ이브러리 제공 엔드포인트 : "/webjars/**" | ||
// Swagger UI에서 사용하는 리소스 제공 엔드포인트 : "/swagger-resources/**" | ||
// 로그인, 회원가입은 제외 | ||
"/swagger-ui", "/v3/api-docs", "/api/v1/members", "/kakao/", "/api/v1/"); | ||
|
||
// 현재 URL 이 LIST 안에 포함되있는걸로 시작하는가? | ||
boolean flag = list.stream().anyMatch(url -> request.getRequestURI().startsWith(url)); | ||
|
||
if (flag) { | ||
filterChain.doFilter(request, response); | ||
return; | ||
} | ||
|
||
String header = request.getHeader(HttpHeaders.AUTHORIZATION); | ||
log.info("JwtAuthenticationFilter header : {}", header); | ||
|
||
// header가 null이거나 빈 문자열이면 안됨. | ||
if (header != null && !header.equalsIgnoreCase("")) { | ||
if (header.startsWith("Bearer")) { | ||
String access_token = header.split(" ")[1]; | ||
if (jwtTokenUtil.isValidateToken(access_token)) { | ||
String memberId = jwtTokenUtil.getIdFromJWT(access_token); | ||
MemberRole role = MemberRole.valueOf(jwtTokenUtil.getRoleFromJWT(access_token)); | ||
JwtToken jwtToken = new JwtToken(memberId, role); | ||
SecurityContextHolder.getContext().setAuthentication(jwtToken); | ||
filterChain.doFilter(request, response); | ||
} | ||
} | ||
// 토큰 검증 실패 -> Exception | ||
} else throw new ResponseStatusException(HttpStatus.UNAUTHORIZED); | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
application/src/main/java/org/depromeet/spot/application/common/jwt/JwtToken.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package org.depromeet.spot.application.common.jwt; | ||
|
||
import java.util.Collection; | ||
import java.util.List; | ||
|
||
import org.depromeet.spot.domain.member.enums.MemberRole; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.security.core.GrantedAuthority; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
public class JwtToken implements Authentication { | ||
// TODO : Authentication을 상속받고 UserDetail을 상속받은 커스텀 유저 정보 객체 생성해줘야함. | ||
private String memberId; | ||
private MemberRole memberRole; | ||
|
||
@Override | ||
public Collection<? extends GrantedAuthority> getAuthorities() { | ||
return List.of(); | ||
} | ||
|
||
@Override | ||
public Object getCredentials() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public Object getDetails() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public Object getPrincipal() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public boolean isAuthenticated() { | ||
return false; | ||
} | ||
|
||
@Override | ||
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {} | ||
|
||
@Override | ||
public String getName() { | ||
return ""; | ||
} | ||
} |
118 changes: 118 additions & 0 deletions
118
application/src/main/java/org/depromeet/spot/application/common/jwt/JwtTokenUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package org.depromeet.spot.application.common.jwt; | ||
|
||
import java.security.Key; | ||
import java.util.Date; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
import javax.crypto.spec.SecretKeySpec; | ||
|
||
import org.depromeet.spot.domain.member.Member; | ||
import org.depromeet.spot.domain.member.enums.MemberRole; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.http.HttpHeaders; | ||
import org.springframework.stereotype.Component; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.ExpiredJwtException; | ||
import io.jsonwebtoken.Jws; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.MalformedJwtException; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
import io.jsonwebtoken.UnsupportedJwtException; | ||
import io.jsonwebtoken.security.WeakKeyException; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
|
||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class JwtTokenUtil { | ||
// JWT를 생성하고 관리하는 클래스 | ||
|
||
// 토큰에 사용되는 시크릿 키 | ||
@Value("${spring.jwt.secret}") | ||
private String SECRETKEY; | ||
|
||
public HttpHeaders getJWTToken(Member member) { | ||
// TODO 토큰 구현하기. | ||
|
||
// jwt 토큰 생성 | ||
String token = generateToken(member.getId(), member.getRole()); | ||
|
||
HttpHeaders headers = new HttpHeaders(); | ||
headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token); | ||
return headers; | ||
} | ||
|
||
public String generateToken(Long memberId, MemberRole memberRole) { | ||
return Jwts.builder() | ||
.setHeader(createHeader()) | ||
.setClaims(createClaims(memberRole)) | ||
.setSubject(memberId.toString()) | ||
.setIssuedAt(new Date(System.currentTimeMillis())) | ||
.setExpiration( | ||
new Date( | ||
System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 30L)) // 토큰 만료 시간 | ||
.signWith(SignatureAlgorithm.HS256, SECRETKEY.getBytes()) | ||
.compact(); | ||
} | ||
|
||
public String getIdFromJWT(String token) { | ||
return Jwts.parser() | ||
.setSigningKey(SECRETKEY.getBytes()) | ||
.parseClaimsJws(token) | ||
.getBody() | ||
.get("id", String.class); | ||
} | ||
|
||
public String getRoleFromJWT(String token) { | ||
return Jwts.parser() | ||
.setSigningKey(SECRETKEY.getBytes()) | ||
.parseClaimsJws(token) | ||
.getBody() | ||
.get("role", String.class); | ||
} | ||
|
||
public Jws<Claims> getClaims(String token) { | ||
return Jwts.parserBuilder().setSigningKey(createSignature()).build().parseClaimsJws(token); | ||
} | ||
|
||
public boolean isValidateToken(String token) { | ||
try { | ||
Jws<Claims> claims = getClaims(token); | ||
return true; | ||
} catch (ExpiredJwtException exception) { | ||
log.error("Token Expired"); | ||
throw new ExpiredJwtException(exception.getHeader(), exception.getClaims(), token); | ||
} catch (UnsupportedJwtException | WeakKeyException exception) { | ||
log.error("Unsupported Token"); | ||
throw new UnsupportedJwtException("지원되지 않는 토큰입니다."); | ||
} catch (MalformedJwtException | IllegalArgumentException exception) { | ||
throw new MalformedJwtException("잘못된 형식의 토큰입니다."); | ||
} | ||
} | ||
|
||
private Map<String, Object> createHeader() { | ||
// 헤더 생성 | ||
Map<String, Object> headers = new HashMap<>(); | ||
|
||
headers.put("typ", "JWT"); | ||
headers.put("alg", "HS256"); // 서명? 생성에 사용될 알고리즘 | ||
|
||
return headers; | ||
} | ||
|
||
// Claim -> 정보를 key-value 형태로 저장함. | ||
private Map<String, Object> createClaims(MemberRole role) { | ||
Map<String, Object> claims = new HashMap<>(); | ||
|
||
claims.put("role", role); | ||
return claims; | ||
} | ||
|
||
private Key createSignature() { | ||
byte[] apiKeySecretBytes = SECRETKEY.getBytes(); | ||
return new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName()); | ||
} | ||
} |
Oops, something went wrong.