Skip to content

Commit

Permalink
Prefix [#5] jwt 구현 진행중 (#12)
Browse files Browse the repository at this point in the history
## 🍀 작업한 내용에 대해 설명해주세요
<!-- 설명하고 싶은 코드가 있다면 첨부해주세요 -->
- 진행중이라 마지막에 설명 쓰겠슴니다....

<br>

## 🍀 어떤 것을 중점으로 리뷰 해주길 바라시나요?
- 

<br>

## 🍀 공통 작업 부분에 대한 수정 사항이 있다면 적어주세요
- 

<br>

## 🍀 PR 유형
어떤 변경 사항인가요?

- [ ] 새로운 기능 추가
- [ ] 버그 수정
- [ ] 코드에 영향을 주지 않는 변경사항 (오타 수정, 탭 사이즈 변경, 변수명 변경)
- [ ] 코드 리팩토링
- [ ] 주석 추가 및 수정
- [ ] 문서 수정
- [ ] 파일 혹은 패키지명 수정
- [ ] 파일 혹은 패키지 삭제

<br>

## 🍀 Checklist
- [ ] 코드 컨벤션을 지켰나요?
- [ ] git 컨벤션을 지켰나요?
- [ ] PR 날리기 전에 검토하셨나요?
<!-- 스스로 QA를 진행해봤는지 (기기 대응, 앱 터지지 않는지 등) -->
- [ ] 코드리뷰를 반영했나요?


### 🍀 Issue
<!-- 생성한 관련 이슈가 있다면 Resolved #이슈번호로 닫아주세요! -->
Resolved #이슈번호
  • Loading branch information
eunseo5343 authored Jun 24, 2024
2 parents 069c16d + f44fcd0 commit cd0261b
Show file tree
Hide file tree
Showing 21 changed files with 487 additions and 5 deletions.
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/Jaksim-Server.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions jaksim/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//JWT
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'


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

tasks.named('test') {
Expand Down
26 changes: 26 additions & 0 deletions jaksim/src/main/java/org/sopt/jaksim/auth/PrincipalHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.sopt.jaksim.auth;

import org.sopt.jaksim.global.exception.UnauthorizedException;
import org.sopt.jaksim.global.message.ErrorMessage;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component
public class PrincipalHandler {

private static final String ANONYMOUS_USER = "anonymousUser";

public Long getUserIdFromPrincipal() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
isPrincipalNull(principal);
return Long.valueOf(principal.toString());
}

public void isPrincipalNull(
final Object principal
) {
if (principal.toString().equals(ANONYMOUS_USER)) {
throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION);
}
}
}
48 changes: 48 additions & 0 deletions jaksim/src/main/java/org/sopt/jaksim/auth/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.sopt.jaksim.auth;

import lombok.RequiredArgsConstructor;
import org.sopt.jaksim.auth.filter.CustomAccessDeniedHandler;
import org.sopt.jaksim.auth.filter.CustomJwtAuthenticationEntryPoint;
import org.sopt.jaksim.auth.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.annotation.web.configurers.RequestCacheConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity //web Security를 사용할 수 있게
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;


private static final String[] AUTH_WHITE_LIST = {"/api/v1/member"};

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.requestCache(RequestCacheConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.exceptionHandling(exception ->
{
exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint);
exception.accessDeniedHandler(customAccessDeniedHandler);
});


http.authorizeHttpRequests(auth -> {
auth.requestMatchers(AUTH_WHITE_LIST).permitAll();
auth.anyRequest().authenticated();
})
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
17 changes: 17 additions & 0 deletions jaksim/src/main/java/org/sopt/jaksim/auth/UserAuthentication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.sopt.jaksim.auth;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class UserAuthentication extends UsernamePasswordAuthenticationToken {

public UserAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}

public static UserAuthentication createUserAuthentication(Long userId) {
return new UserAuthentication(userId, null, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.sopt.jaksim.auth.filter;


import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
setResponse(response);
}

private void setResponse(HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.sopt.jaksim.auth.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.sopt.jaksim.global.message.ErrorMessage;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
setResponse(response);
}

private void setResponse(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter()
.write(objectMapper.writeValueAsString(
ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.sopt.jaksim.auth.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.sopt.jaksim.auth.UserAuthentication;
import org.sopt.jaksim.global.common.jwt.JwtTokenProvider;
import org.sopt.jaksim.global.exception.UnauthorizedException;
import org.sopt.jaksim.global.message.ErrorMessage;
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;

import java.io.IOException;

import static org.sopt.jaksim.global.common.jwt.JwtValidationType.VALID_JWT;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
try {
final String token = getJwtFromRequest(request);
if (jwtTokenProvider.validateToken(token) == VALID_JWT) {
Long memberId = jwtTokenProvider.getUserFromJwt(token);
UserAuthentication authentication = UserAuthentication.createUserAuthentication(memberId);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception exception) {
throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION);
}
filterChain.doFilter(request, response);
}

private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring("Bearer ".length());
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package org.sopt.jaksim.global.common.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Base64;
import java.util.Date;

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

private static final String USER_ID = "userId";

private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14;
private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14;

@Value("${jwt.secret}")
private String JWT_SECRET;


public String issueAccessToken(final Authentication authentication) {
return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME);
}

public String issueRefreshToken(final Authentication authentication) {
return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME);
}

public String generateToken(Authentication authentication, Long tokenExpirationTime) {
final Date now = new Date();
final Claims claims = Jwts.claims()
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간

claims.put(USER_ID, authentication.getPrincipal());

return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header
.setClaims(claims) // Claim
.signWith(getSigningKey()) // Signature
.compact();
}

private SecretKey getSigningKey() {
String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); //SecretKey 통해 서명 생성
return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용
}

public JwtValidationType validateToken(String token) {
try {
final Claims claims = getBody(token);
return JwtValidationType.VALID_JWT;
} catch (MalformedJwtException ex) {
return JwtValidationType.INVALID_JWT_TOKEN;
} catch (ExpiredJwtException ex) {
return JwtValidationType.EXPIRED_JWT_TOKEN;
} catch (UnsupportedJwtException ex) {
return JwtValidationType.UNSUPPORTED_JWT_TOKEN;
} catch (IllegalArgumentException ex) {
return JwtValidationType.EMPTY_JWT;
}
}

private Claims getBody(final String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}

public Long getUserFromJwt(String token) {
Claims claims = getBody(token);
return Long.valueOf(claims.get(USER_ID).toString());
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sopt.jaksim.global.common.jwt;

public enum JwtValidationType {
VALID_JWT, // 유효한 JWT
INVALID_JWT_SIGNATURE, // 유효하지 않은 서명
INVALID_JWT_TOKEN, // 유효하지 않은 토큰
EXPIRED_JWT_TOKEN, // 만료된 토큰
UNSUPPORTED_JWT_TOKEN, // 지원하지 않는 형식의 토큰
EMPTY_JWT // 빈 JWT
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,6 @@ public enum ErrorMessage {
private final HttpStatus httpStatus;
private final String code;
private final String message;


}
Loading

0 comments on commit cd0261b

Please sign in to comment.