Skip to content

Commit

Permalink
Merge pull request #292 from twenty-three-23/feature/TT-346-filter-chain
Browse files Browse the repository at this point in the history
TT-346 filter chain
  • Loading branch information
ch8930 authored Aug 7, 2024
2 parents c3ddc4d + dac856c commit d7ebeae
Show file tree
Hide file tree
Showing 20 changed files with 513 additions and 7 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

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

//java JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.twentythree.peech.auth.controller;

import com.twentythree.peech.auth.service.ReissueAccessAndRefreshTokenService;
import com.twentythree.peech.user.dto.AccessAndRefreshToken;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1.1/auth")
public class AuthController {

private final ReissueAccessAndRefreshTokenService reissueAccessAndRefreshTokenService;

@PatchMapping("/reissue")
public AccessAndRefreshToken reissueAccessToken(String refreshToken) {
return reissueAccessAndRefreshTokenService.createNewToken(refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.twentythree.peech.auth.controller;

import com.twentythree.peech.user.dto.AccessAndRefreshToken;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.springframework.web.bind.annotation.RequestBody;

public interface SwaggerAuthController {
@ApiResponse(responseCode = "200", description = "성공", content = {@Content(schema = @Schema(implementation = AccessAndRefreshToken.class), mediaType = "application/json")})
@ApiResponse(responseCode = "401", description = "실패", content = {@Content(schema = @Schema(implementation = Error.class), mediaType = "application/json")})
AccessAndRefreshToken reissueAccessToken(@RequestBody String refreshToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.twentythree.peech.auth.service;

import com.twentythree.peech.common.utils.JWTUtils;
import com.twentythree.peech.common.utils.UserRoleConvertUtils;
import com.twentythree.peech.security.jwt.JWTAuthentication;
import com.twentythree.peech.user.dto.AccessAndRefreshToken;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.NoSuchElementException;

@Service
@RequiredArgsConstructor
public class ReissueAccessAndRefreshTokenService {

private final JWTUtils jwtUtils;

public AccessAndRefreshToken createNewToken(String refreshToken) {

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
JWTAuthentication jwtAuthentication = (JWTAuthentication) authentication.getPrincipal();

Long userId = jwtAuthentication.getUserId();
GrantedAuthority Authority = authentication.getAuthorities()
.stream().findFirst().orElseThrow(() -> new NoSuchElementException("No authorities found for the user"));

String newAccessToken = jwtUtils.createAccessToken(userId, UserRoleConvertUtils
.convertStringToUserRole(Authority.getAuthority()));
String newRefreshToken = jwtUtils.createRefreshToken(userId, UserRoleConvertUtils
.convertStringToUserRole(Authority.getAuthority()));

return new AccessAndRefreshToken(newAccessToken, newRefreshToken);
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.twentythree.peech.common.exception;

public class AccessTokenExpiredException extends RuntimeException {
public AccessTokenExpiredException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,17 @@ public ResponseEntity<ErrorDTO> baseExceptionHandler(Exception e) {
ErrorDTO errorDTO = new ErrorDTO(e.getMessage());
return new ResponseEntity<>(errorDTO, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(AccessTokenExpiredException.class)
public ResponseEntity<String> handleExpiredJWTTokenException(AccessTokenExpiredException e) {
log.error("access 토큰 만료 에러 발생", e);
// 410번 에러 코드로 응답
return new ResponseEntity<>(e.getMessage(), HttpStatus.GONE);
}

@ExceptionHandler(RefreshTokenExpiredException.class)
public ResponseEntity<String> handleExpiredJWTTokenException(RefreshTokenExpiredException e) {
log.error("refresh 토큰 만료 에러 발생", e);
return new ResponseEntity<>(e.getMessage(), HttpStatus.UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.twentythree.peech.common.exception;

import io.jsonwebtoken.ExpiredJwtException;

public class RefreshTokenExpiredException extends RuntimeException{
public RefreshTokenExpiredException(String message) {
super(message);
}
}
50 changes: 46 additions & 4 deletions src/main/java/com/twentythree/peech/common/utils/JWTUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.twentythree.peech.common.JwtProperties;
import com.twentythree.peech.common.exception.AccessTokenExpiredException;
import com.twentythree.peech.common.exception.RefreshTokenExpiredException;
import com.twentythree.peech.user.dto.IdentityToken;
import com.twentythree.peech.user.dto.IdentityTokenHeader;
import com.twentythree.peech.user.dto.IdentityTokenPayload;
import com.twentythree.peech.user.value.UserRole;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
Expand All @@ -23,6 +27,7 @@
@Component
public class JWTUtils {

private final Logger log = LoggerFactory.getLogger(this.getClass());
private final JwtProperties jwtProperties;

private String secretString;
Expand Down Expand Up @@ -124,6 +129,44 @@ public Jws<Claims> parseRefreshToken(String token) {
return claimsJws;
}

public String resolveToken(HttpServletRequest request) {
return request.getHeader("Authorization");
}

public Claims validateAccessToken(String token) {
try {
Jws<Claims> claimsJws = parseAccessToken(token);
return claimsJws.getPayload();
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT Token", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT Token", e);
throw new AccessTokenExpiredException("AccessToken이 만료되었습니다. refreshToken으로 재요청해주세요.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT Token", e);
} catch (IllegalArgumentException e) {
log.info("JWT Token is empty", e);
}
return null;
}

public Claims validateRefreshToken(String token) {
try {
Jws<Claims> claimsJws = parseRefreshToken(token);
return claimsJws.getPayload();
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT Token", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT Token", e);
throw new RefreshTokenExpiredException("RefreshToken이 만료되었습니다. 다시 로그인해 주세요.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT Token", e);
} catch (IllegalArgumentException e) {
log.info("JWT Token is empty", e);
}
return null;
}

public IdentityToken decodeIdentityToken(String token) {
try {

Expand All @@ -147,5 +190,4 @@ public IdentityToken decodeIdentityToken(String token) {
throw new IllegalArgumentException("error", e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.twentythree.peech.common.utils;

import com.twentythree.peech.user.value.UserRole;

public class UserRoleConvertUtils {

public static UserRole convertStringToUserRole(String role) {

if (role.equals("ROLE_ADMIN")) {
return UserRole.ROLE_ADMIN;
} else if (role.equals("ROLE_COMMON")) {
return UserRole.ROLE_COMMON;
} else {
throw new IllegalArgumentException("존재하지 않는 권한입니다.");
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.twentythree.peech.security.config;

import com.twentythree.peech.common.utils.JWTUtils;
import com.twentythree.peech.security.filter.JWTAuthenticationFilter;
import com.twentythree.peech.security.handler.JWTAuthAccessDeniedHandler;
import com.twentythree.peech.security.jwt.JWTAuthenticationProvider;
import com.twentythree.peech.user.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JWTSecurityConfig {

private final JWTUtils jwtUtils;
private final UserService userService;

public JWTSecurityConfig(JWTUtils jwtUtils, UserService userService) {
this.jwtUtils = jwtUtils;
this.userService = userService;
}

@Bean
public JWTAuthenticationFilter jwtAuthenticationFilter() {
return new JWTAuthenticationFilter(jwtUtils);
}

@Bean
public JWTAuthenticationProvider jwtAuthenticationProvider() {
return new JWTAuthenticationProvider(userService);
}

@Bean
public JWTAuthAccessDeniedHandler authAccessDeniedHandler() {
return new JWTAuthAccessDeniedHandler();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.twentythree.peech.security.config;

import com.twentythree.peech.security.filter.JWTAuthenticationFilter;
import com.twentythree.peech.security.filter.JWTExceptionFilter;
import com.twentythree.peech.security.handler.JWTAuthAccessDeniedHandler;
import com.twentythree.peech.security.handler.JWTAuthEntryPoint;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.access.ExceptionTranslationFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity
@EnableMethodSecurity
@Configuration
public class SecurityConfig {

private final JWTAuthenticationFilter jwtAuthenticationFilter;
private final JWTAuthEntryPoint JWTAuthEntryPoint;
private final JWTAuthAccessDeniedHandler JWTAuthAccessDeniedHandler;

public SecurityConfig(JWTAuthenticationFilter jwtAuthenticationFilter,
JWTAuthEntryPoint JWTAuthEntryPoint,
JWTAuthAccessDeniedHandler JWTAuthAccessDeniedHandler
) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.JWTAuthEntryPoint = JWTAuthEntryPoint;
this.JWTAuthAccessDeniedHandler = JWTAuthAccessDeniedHandler;
}

// 필터체인 설정
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize ->
authorize.requestMatchers("/api/v1.1/auth/reissue").permitAll()
.requestMatchers(HttpMethod.POST,"/api/v1.1/user").permitAll()
.requestMatchers("/swagger-ui/").hasAuthority("ROLE_ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtAuthenticationFilter, ExceptionTranslationFilter.class)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(e -> e.authenticationEntryPoint(JWTAuthEntryPoint)
.accessDeniedHandler(JWTAuthAccessDeniedHandler))

.httpBasic(AbstractHttpConfigurer::disable)
.anonymous(AbstractHttpConfigurer::disable);

return http.build();
}
}
Loading

0 comments on commit d7ebeae

Please sign in to comment.