Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 10 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
name: Tradin_CI

on:
push:
branches:
- dev
pull_request:
types: [ opened, synchronize, reopened ]
- '**'
jobs:
build:
name: Build and analyze
build-springboot:
name: Build and analyze (SpringBoot)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand All @@ -20,22 +19,16 @@ jobs:
java-version: 17
distribution: 'temurin'

- name: Cache SonarCloud packages
uses: actions/cache@v3
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar

- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: ~/.gradle/caches
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle

- name: Build and analyze
- name: Build and analyze (SpringBoot)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./gradlew clean build sonarqube --info
run: |
./gradlew clean build
79 changes: 27 additions & 52 deletions src/main/java/com/tradin/common/config/SecurityConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
import com.tradin.common.jwt.JwtUtil;
import com.tradin.common.utils.PasswordEncoder;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import lombok.val;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
Expand All @@ -23,69 +23,44 @@
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {

private final JwtUtil jwtUtil;
private final PasswordEncoder passwordEncoder;

@Value("${secret.swagger-username}")
private String swaggerUsername;

@Value("${secret.swagger-password}")
private String swaggerPassword;

private final String[] SWAGGER_PATTERN = {"/swagger-ui", "/api-docs"};
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final JwtExceptionFilter jwtExceptionFilter;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.build();
// http
// .authorizeRequests()
// .mvcMatchers("/health-check").permitAll()
// .mvcMatchers("/v1/auth/cognito", "/v1/auth/token").permitAll()
// .mvcMatchers("/v1/strategies/future", "/v1/strategies/spot").permitAll()
// .mvcMatchers("/v1/histories").permitAll()
// .mvcMatchers(SWAGGER_PATTERN).authenticated()
// .anyRequest().authenticated()
// .and()
// .cors().configurationSource(corsConfigurationSource())
// .and()
// .httpBasic()
// .and()
// .formLogin().disable()
// .logout().disable()
// .csrf().disable()
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// .and()
// .headers()
// .frameOptions().sameOrigin()
// .httpStrictTransportSecurity().disable()
//// .and()
//// .exceptionHandling()
//// .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
// .and()
// .addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
// .addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class)
// .build();
return http.csrf(AbstractHttpConfigurer::disable)
.cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource()))
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(
session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
).authorizeHttpRequests(authorizeRequest ->
authorizeRequest
.requestMatchers("/v1/auth/test/token").permitAll()
.anyRequest().authenticated()
)
.headers(headers -> headers
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter, JwtAuthenticationFilter.class)
.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
val configuration = new CorsConfiguration();

configuration.addAllowedOriginPattern("*");
configuration.addAllowedOrigin("*");
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.setAllowCredentials(true);
configuration.setAllowCredentials(false);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
val source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);

return source;
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {

auth.inMemoryAuthentication()
.withUser(swaggerUsername).password(passwordEncoder.encode(swaggerPassword)).roles("SWAGGER");
}
}
7 changes: 5 additions & 2 deletions src/main/java/com/tradin/common/exception/ExceptionType.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ public enum ExceptionType {
WRONG_PASSWORD_EXCEPTION(UNAUTHORIZED, "비밀번호가 일치하지 않습니다."),
EMAIL_ALREADY_EXISTS_EXCEPTION(UNAUTHORIZED, "이미 존재하는 이메일입니다."),
NOT_FOUND_JWK_PARTS_EXCEPTION(UNAUTHORIZED, "존재하지 않는 kid입니다."),
INVALID_JWT_SIGNATURE_EXCEPTION(UNAUTHORIZED, "유효하지 않은 JWT 서명입니다."),
EXPIRED_JWT_TOKEN_EXCEPTION(UNAUTHORIZED, "만료된 JWT 토큰입니다."),
UNSUPPORTED_JWT_TOKEN_EXCEPTION(UNAUTHORIZED, "지원하지 않는 JWT 토큰입니다."),
NOT_FOUND_JWT_CLAIMS_EXCEPTION(UNAUTHORIZED, "JWT Claims가 존재하지 않습니다."),

//403 Forbidden

Expand All @@ -46,8 +50,7 @@ public enum ExceptionType {
DECRYPT_FAIL_EXCEPTION(INTERNAL_SERVER_ERROR, "복호화에 실패하였습니다."),
SIGNATURE_GENERATION_FAIL_EXCEPTION(INTERNAL_SERVER_ERROR, "JWT 서명 생성에 실패하였습니다."),
PUBLIC_KEY_GENERATE_FAIL_EXCEPTION(INTERNAL_SERVER_ERROR, "공개키 생성에 실패하였습니다."),
INTERNAL_SERVER_ERROR_EXCEPTION(INTERNAL_SERVER_ERROR, "서버 내부 오류입니다.");
;
INTERNAL_SERVER_ERROR_EXCEPTION(INTERNAL_SERVER_ERROR, "서버 내부 오류입니다.");;

private final HttpStatus httpStatus;
private final String message;
Expand Down
33 changes: 19 additions & 14 deletions src/main/java/com/tradin/common/filter/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,42 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String AUTHORIZATION_HEADER_PREFIX = "Authorization";

private static final List<String> ALLOW_LIST = List.of("/auth", "/swagger-ui", "/api-docs", "/health-check",
"/notifications"
);
public static final String BEARER_PREFIX = "Bearer ";
public static final String AUTHORIZATION_HEADER_PREFIX = "Authorization";

private final JwtUtil jwtUtil;
private static final List<String> ALLOW_LIST = List.of("/swagger-ui", "/api-docs", "/health-check", "/v1/auth/cognito", "/v1/auth/token", "/v1/strategies/future", "/v1/strategies/spot", "/v1/histories");

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!isAllowList(request.getRequestURI())) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER_PREFIX);
String sub = validateHeaderAndGetSub(bearerToken);
setAuthentication(sub);
Long userId = validateHeaderAndGetUserId(bearerToken);
setAuthentication(userId);
}

filterChain.doFilter(request, response);
}

private boolean isAllowList(String requestURI) {
return ALLOW_LIST.stream().anyMatch(requestURI::contains);
}

private String validateHeaderAndGetSub(String bearerToken) {

private Long validateHeaderAndGetUserId(String bearerToken) {
validateHasText(bearerToken);
validateStartWithBearer(bearerToken);
return validateAccessTokenAndGetSub(getAccessTokenFromBearer(bearerToken));
return validateAccessToken(getAccessTokenFromBearer(bearerToken));
}

private void validateHasText(String bearerToken) {
Expand All @@ -58,15 +63,15 @@ private void validateStartWithBearer(String bearerToken) {
}
}

private String validateAccessTokenAndGetSub(String accessToken) {
return jwtUtil.validateToken(accessToken).get("sub", String.class);
private Long validateAccessToken(String accessToken) {
return jwtUtil.validateAccessToken(accessToken);
}

private String getAccessTokenFromBearer(String bearerToken) {
return bearerToken.substring(BEARER_PREFIX.length());
}

private void setAuthentication(String sub) {
SecurityContextHolder.getContext().setAuthentication(jwtUtil.getAuthentication(sub));
private void setAuthentication(Long userId) {
SecurityContextHolder.getContext().setAuthentication(jwtUtil.getAuthentication(userId));
}
}
45 changes: 21 additions & 24 deletions src/main/java/com/tradin/common/filter/JwtExceptionFilter.java
Original file line number Diff line number Diff line change
@@ -1,43 +1,40 @@
package com.tradin.common.filter;


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tradin.common.exception.TradinException;
import com.tradin.common.exception.ExceptionType;
import com.tradin.common.response.TradinResponse;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@RequiredArgsConstructor
@Component
public class JwtExceptionFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain)
throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (TradinException e) {
respondException(response, e);
filterChain.doFilter(httpServletRequest, httpServletResponse);
} catch (Exception e) {
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.getWriter()
.write(toJson(TradinResponse.error(ExceptionType.INVALID_JWT_TOKEN_EXCEPTION, e.getMessage())));
}
}

private void respondException(HttpServletResponse response, TradinException e) throws IOException {
setResponseHeader(response);
writeResponse(response, e);
}

private void setResponseHeader(HttpServletResponse response) {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
}

private void writeResponse(HttpServletResponse response, TradinException e) throws IOException {
response.setStatus(e.getErrorType().getHttpStatus().value());
response.getWriter().write(toJson(e.getErrorType().getHttpStatus()));
}

private String toJson(HttpStatus exceptionResponse) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(exceptionResponse);
private String toJson(TradinResponse<?> response) throws JsonProcessingException {
return new ObjectMapper().writeValueAsString(response);
}
}
}
46 changes: 46 additions & 0 deletions src/main/java/com/tradin/common/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.tradin.common.jwt;

import com.tradin.module.auth.controller.dto.response.TokenResponseDto;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtProvider {

private final RedisTemplate<String, Object> redisTemplate;
private final JwtSecretKeyProvider jwtSecretKeyProvider;

private static final long ACCESS_TOKEN_EXPIRE_TIME = 24 * 60 * 60 * 1000L; // 1일
private static final long REFRESH_TOKEN_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L; // 7일

public TokenResponseDto createJwtToken(Long userId) {
long now = (new Date()).getTime();
Date accessTokenExpiresIn = new Date(now + ACCESS_TOKEN_EXPIRE_TIME);
Date refreshTokenExpiresIn = new Date(now + REFRESH_TOKEN_EXPIRE_TIME);

// Access Token 생성
String accessToken = Jwts.builder()
.claim("USER_ID", String.valueOf(userId))
.setExpiration(accessTokenExpiresIn)
.signWith(jwtSecretKeyProvider.getSecretKey(), SignatureAlgorithm.HS512)
.compact();

// Refresh Token 생성
String refreshToken = Jwts.builder()
.setExpiration(refreshTokenExpiresIn)
.signWith(jwtSecretKeyProvider.getSecretKey(), SignatureAlgorithm.HS512)
.compact();

redisTemplate.opsForValue().set("RT:" + userId, refreshToken, REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);

return TokenResponseDto.of(accessToken, refreshToken);
}
}
22 changes: 22 additions & 0 deletions src/main/java/com/tradin/common/jwt/JwtSecretKeyProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.tradin.common.jwt;

import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class JwtSecretKeyProvider {

private final Key secretKey;

public JwtSecretKeyProvider(@Value("${JWT_SECRET_KEY}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
}

public Key getSecretKey() {
return secretKey;
}
}
Loading