-
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.
[feat] JWT 토큰 생성
- Loading branch information
Showing
14 changed files
with
408 additions
and
6 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 |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package org.harang.server.annotation; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Target(ElementType.PARAMETER) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface MemberId { | ||
} |
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,22 @@ | ||
package org.harang.server.config; | ||
|
||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import org.harang.server.interceptor.pre.MemberIdArgumentResolver; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.servlet.config.annotation.EnableWebMvc; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
|
||
@Configuration | ||
@EnableWebMvc | ||
@RequiredArgsConstructor | ||
public class WebMVCConfig implements WebMvcConfigurer { | ||
private final MemberIdArgumentResolver memberIdArgumentResolver; | ||
|
||
@Override | ||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { | ||
WebMvcConfigurer.super.addArgumentResolvers(resolvers); | ||
resolvers.add(memberIdArgumentResolver); | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
src/main/java/org/harang/server/constants/jwt/JwtProperties.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,7 @@ | ||
package org.harang.server.constants.jwt; | ||
|
||
public class JwtProperties { | ||
public static final String MEMBER_ID_CLAIM_NAME = "mid"; | ||
public static final String MEMBER_ROLE_CLAIM_NAME = "mtype"; | ||
public static final String BEARER = "Bearer "; | ||
} |
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
15 changes: 15 additions & 0 deletions
15
src/main/java/org/harang/server/dto/response/JwtTokenResponse.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,15 @@ | ||
package org.harang.server.dto.response; | ||
|
||
import jakarta.validation.constraints.NotNull; | ||
import lombok.Builder; | ||
|
||
@Builder | ||
public record JwtTokenResponse(@NotNull String accessToken, | ||
@NotNull String refreshToken) { | ||
public static JwtTokenResponse of(String accessToken, String refreshToken) { | ||
return JwtTokenResponse.builder() | ||
.accessToken(accessToken) | ||
.refreshToken(refreshToken) | ||
.build(); | ||
} | ||
} |
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
31 changes: 31 additions & 0 deletions
31
src/main/java/org/harang/server/interceptor/pre/MemberIdArgumentResolver.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,31 @@ | ||
package org.harang.server.interceptor.pre; | ||
|
||
import java.security.Principal; | ||
import org.harang.server.annotation.MemberId; | ||
import org.harang.server.dto.type.ErrorMessage; | ||
import org.harang.server.exception.CustomException; | ||
import org.springframework.core.MethodParameter; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.bind.support.WebDataBinderFactory; | ||
import org.springframework.web.context.request.NativeWebRequest; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.method.support.ModelAndViewContainer; | ||
|
||
@Component | ||
public class MemberIdArgumentResolver implements HandlerMethodArgumentResolver { | ||
@Override | ||
public boolean supportsParameter(MethodParameter parameter) { | ||
return parameter.getParameterType().equals(Long.class) | ||
&& parameter.hasParameterAnnotation(MemberId.class); | ||
} | ||
|
||
@Override | ||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, | ||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { | ||
final Principal principal = webRequest.getUserPrincipal(); | ||
if (principal == null) { | ||
throw new CustomException(ErrorMessage.PRINCIPAL_IS_EMPTY); | ||
} | ||
return Long.valueOf(principal.getName()); | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
src/main/java/org/harang/server/security/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,36 @@ | ||
package org.harang.server.security.config; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.harang.server.security.filter.JwtAuthenticationFilter; | ||
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.config.http.SessionCreationPolicy; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | ||
|
||
@Configuration | ||
@EnableWebSecurity | ||
@RequiredArgsConstructor | ||
public class SecurityConfig { | ||
private final JwtAuthenticationFilter jwtAuthenticationFilter; | ||
|
||
@Bean | ||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | ||
return http.httpBasic(AbstractHttpConfigurer::disable).cors(AbstractHttpConfigurer::disable) | ||
.csrf(AbstractHttpConfigurer::disable).formLogin(AbstractHttpConfigurer::disable) | ||
|
||
.sessionManagement(sessionManagementConfigurer -> sessionManagementConfigurer.sessionCreationPolicy( | ||
SessionCreationPolicy.STATELESS)) | ||
|
||
.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> | ||
authorizationManagerRequestMatcherRegistry | ||
// TODO: 이후에 구현되는 로그인 api 엔드포인트랑 동일한지 확인 | ||
.requestMatchers("/v1/login").permitAll() | ||
.anyRequest().authenticated()) | ||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) | ||
.build(); | ||
} | ||
} |
73 changes: 73 additions & 0 deletions
73
src/main/java/org/harang/server/security/filter/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,73 @@ | ||
package org.harang.server.security.filter; | ||
|
||
import io.jsonwebtoken.Claims; | ||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import jakarta.validation.constraints.NotNull; | ||
import java.io.IOException; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.harang.server.constants.jwt.JwtProperties; | ||
import org.harang.server.domain.enums.Type; | ||
import org.harang.server.dto.type.ErrorMessage; | ||
import org.harang.server.exception.CustomException; | ||
import org.harang.server.util.JwtUtil; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.util.StringUtils; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||
|
||
private static final String AUTH_HEADER = "Authorization"; | ||
|
||
private final JwtUtil jwtUtil; | ||
|
||
@Override | ||
protected void doFilterInternal(@NotNull HttpServletRequest request, | ||
@NotNull HttpServletResponse response, | ||
@NotNull FilterChain filterChain) | ||
throws ServletException, IOException { | ||
final String token = getTokenFromHeader(request); | ||
|
||
log.debug(String.valueOf(request)); | ||
|
||
if (StringUtils.hasText(token) && jwtUtil.verifyToken(token)) { | ||
Claims claims = jwtUtil.getClaim(token); | ||
Long memberId = claims.get(JwtProperties.MEMBER_ID_CLAIM_NAME, Long.class); | ||
Type role = claims.get(JwtProperties.MEMBER_ROLE_CLAIM_NAME, Type.class); | ||
if (role == null) { | ||
throw new CustomException(ErrorMessage.INVALID_TOKEN_TYPE); | ||
} | ||
// type 값을 GrantedAuthority 객체 콜렉션으로 변환 | ||
List<GrantedAuthority> roles = Collections.singletonList(new SimpleGrantedAuthority(role.getValue())); | ||
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(memberId, null, roles); | ||
|
||
// TODO: 왜 쓰는지 알아보기 | ||
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); | ||
|
||
// security context에 authentication 객체 저장 | ||
SecurityContextHolder.getContext().setAuthentication(auth); | ||
} | ||
} | ||
|
||
private String getTokenFromHeader(HttpServletRequest request) { | ||
String bearerToken = request.getHeader(AUTH_HEADER); | ||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(JwtProperties.BEARER)) { | ||
return bearerToken.substring(JwtProperties.BEARER.length()); | ||
} | ||
log.debug("요청 헤더에 토큰이 존재하지 않습니다."); | ||
return null; | ||
} | ||
} |
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,111 @@ | ||
package org.harang.server.util; | ||
|
||
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.io.Decoders; | ||
import io.jsonwebtoken.security.Keys; | ||
import java.security.Key; | ||
import java.util.Date; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.harang.server.constants.jwt.JwtProperties; | ||
import org.harang.server.domain.enums.Type; | ||
import org.harang.server.dto.response.JwtTokenResponse; | ||
import org.harang.server.dto.type.ErrorMessage; | ||
import org.harang.server.exception.CustomException; | ||
import org.springframework.beans.factory.InitializingBean; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
@Slf4j | ||
public class JwtUtil implements InitializingBean { | ||
|
||
@Value("${jwt.secret-key}") | ||
private String secretKey; | ||
|
||
@Value("${jwt.access-token-expire-period}") | ||
private Integer accessTokenExpirePeriod; | ||
|
||
@Value("${jwt.refresh-token-expire-period}") | ||
private Integer refreshTokenExpirePeriod; | ||
|
||
private Key key; | ||
|
||
|
||
@Override | ||
public void afterPropertiesSet() throws Exception { | ||
byte[] keyBytes = Decoders.BASE64.decode(secretKey); | ||
this.key = Keys.hmacShaKeyFor(keyBytes); | ||
} | ||
|
||
public JwtTokenResponse generateTokens(Long id, Type type) { | ||
return JwtTokenResponse.of( // Bearer prefix 추가 | ||
JwtProperties.BEARER + generateAccessToken(id, type), | ||
JwtProperties.BEARER + generateRefreshToken(id) | ||
); | ||
} | ||
|
||
public boolean verifyToken(String token) { | ||
try { | ||
Jws<Claims> claims = Jwts.parserBuilder() | ||
.setSigningKey(key) | ||
.build() | ||
.parseClaimsJws(token); | ||
return claims.getBody() | ||
.getExpiration() | ||
.after(new Date(System.currentTimeMillis())); | ||
}catch (Exception e) { | ||
return false; | ||
} | ||
} | ||
|
||
public Claims getClaim(String token) { | ||
try { | ||
return Jwts.parserBuilder() | ||
.setSigningKey(key) | ||
.build() | ||
// JWS - 서버의 PK로 인증정보를 서명한 것 | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
} | ||
catch (MalformedJwtException e) { | ||
throw new CustomException(ErrorMessage.INVALID_JWT); | ||
}catch (ExpiredJwtException e) { | ||
throw new CustomException(ErrorMessage.EXPIRED_JWT); | ||
}catch (UnsupportedJwtException e) { | ||
throw new CustomException(ErrorMessage.UNSUPPORTED_JWT); | ||
}catch (IllegalArgumentException e) { | ||
throw new CustomException(ErrorMessage.JWT_IS_EMPTY); | ||
} | ||
} | ||
|
||
private String generateAccessToken(Long id, Type type) { | ||
Claims claims = Jwts.claims(); | ||
claims.put(JwtProperties.MEMBER_ID_CLAIM_NAME, id); | ||
claims.put(JwtProperties.MEMBER_ROLE_CLAIM_NAME, type); | ||
|
||
return Jwts.builder() | ||
.setClaims(claims) | ||
.setIssuedAt(new Date(System.currentTimeMillis())) | ||
.setExpiration(new Date(System.currentTimeMillis() + accessTokenExpirePeriod)) | ||
.signWith(key, SignatureAlgorithm.HS512) | ||
.compact(); | ||
} | ||
|
||
private String generateRefreshToken(Long id) { | ||
Claims claims = Jwts.claims(); | ||
claims.put(JwtProperties.MEMBER_ID_CLAIM_NAME, id); | ||
|
||
return Jwts.builder() | ||
.setClaims(claims) | ||
.setIssuedAt(new Date(System.currentTimeMillis())) | ||
.setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpirePeriod)) | ||
.signWith(key, SignatureAlgorithm.HS512) | ||
.compact(); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.