Skip to content

Commit

Permalink
Merge 4de5d45 into 1ae7458
Browse files Browse the repository at this point in the history
  • Loading branch information
wjdwnsdnjs13 authored Jul 18, 2024
2 parents 1ae7458 + 4de5d45 commit bdee869
Show file tree
Hide file tree
Showing 28 changed files with 745 additions and 90 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -382,4 +382,6 @@ gradle-app.setting
# End of https://www.toptal.com/developers/gitignore/api/macos,windows,intellij,intellij+iml,intellij+all,visualstudiocode,java,gradle,kotlin
/db/

.env
.env

*.application-jwt.yml
4 changes: 3 additions & 1 deletion application/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ bin/
.vscode/

### Mac OS ###
.DS_Store
.DS_Store

*.application-jwt.yml
10 changes: 10 additions & 0 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,18 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework:spring-aspects")

// security
implementation("org.springframework.boot:spring-boot-starter-security")

// swagger
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:_")

// jwt
// implementation("io.jsonwebtoken:jjwt:_")
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
implementation("io.jsonwebtoken:jjwt-impl:0.11.5")
implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")

}

// spring boot main application이므로 실행 가능한 jar를 생성한다.
Expand Down
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();
}
}
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);
}
}
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 "";
}
}
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());
}
}
Loading

0 comments on commit bdee869

Please sign in to comment.