-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
28 changed files
with
745 additions
and
90 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.