Skip to content

Commit

Permalink
[feat] JWT 토큰 생성
Browse files Browse the repository at this point in the history
[feat] JWT 토큰 생성
  • Loading branch information
nykoh2001 authored Feb 9, 2024
2 parents d1a28d7 + 71799fa commit 7398fa1
Show file tree
Hide file tree
Showing 14 changed files with 408 additions and 6 deletions.
12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,23 @@ dependencies {

//Test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.projectlombok:lombok:1.18.26'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// MySQL
runtimeOnly 'com.mysql:mysql-connector-j'

}

tasks.named('test') {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/org/harang/server/annotation/MemberId.java
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 {
}
22 changes: 22 additions & 0 deletions src/main/java/org/harang/server/config/WebMVCConfig.java
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);
}
}
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 ";
}
4 changes: 2 additions & 2 deletions src/main/java/org/harang/server/domain/MemberInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ public class MemberInfo {
private String refreshToken;

/* Relation Parent Mapping */
@OneToMany(mappedBy = "member_info")
@OneToMany(mappedBy = "memberInfo")
private List<MemberHelp> memberHelpList = new ArrayList<>();

@OneToMany(mappedBy = "member_info")
@OneToMany(mappedBy = "memberInfo")
private List<Certification> certificationList = new ArrayList<>();

@Builder
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/harang/server/dto/response/JwtTokenResponse.java
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();
}
}
16 changes: 13 additions & 3 deletions src/main/java/org/harang/server/dto/type/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,21 @@ public enum ErrorMessage {
// bad request - 400
BAD_REQUEST("40001", HttpStatus.BAD_REQUEST, "올바르지 않은 요청입니다."),

// unauthorized - 403
UNAUTHORIZED("40301", HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."),
// unauthorized - 401
UNAUTHORIZED("40101", HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."),

// JWT Error - 401
INVALID_JWT("40102", HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."),
EXPIRED_JWT("40103", HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."),
UNSUPPORTED_JWT("40104", HttpStatus.UNAUTHORIZED, "지원하지 않는 토큰입니다."),
JWT_IS_EMPTY("40105", HttpStatus.UNAUTHORIZED, "토큰이 비어있습니다."),
INVALID_TOKEN_TYPE("40106", HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 타입입니다."),

// Principal error - 401
PRINCIPAL_IS_EMPTY("40107", HttpStatus.UNAUTHORIZED, "컨텍스트로부터 유저 정보를 가져올 수 없습니다."),

// forbidden - 403
FORBIDDEN("40302", HttpStatus.FORBIDDEN, "권한이 없습니다."),
FORBIDDEN("40301", HttpStatus.FORBIDDEN, "권한이 없습니다."),

// not found - 404
NOT_FOUND("40401", HttpStatus.NOT_FOUND, "리소스가 존재하지 않습니다."),
Expand Down
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());
}
}
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();
}
}
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;
}
}
111 changes: 111 additions & 0 deletions src/main/java/org/harang/server/util/JwtUtil.java
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();
}
}
1 change: 0 additions & 1 deletion src/main/resources/application.properties

This file was deleted.

Loading

0 comments on commit 7398fa1

Please sign in to comment.