Skip to content

Commit

Permalink
[#89] add: Spring Security 세팅
Browse files Browse the repository at this point in the history
  • Loading branch information
yeseul106 authored and rdd9223 committed Dec 9, 2023
1 parent 627e74a commit 24593af
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 12 deletions.
6 changes: 6 additions & 0 deletions main/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux:3.1.5'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation group: 'org.postgresql', name: 'postgresql', version: '42.6.0'

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'
implementation 'com.auth0:java-jwt:4.4.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.sopt.makers.crew.main.common.config;

import lombok.RequiredArgsConstructor;
import org.sopt.makers.crew.main.common.config.jwt.JwtAuthenticationEntryPoint;
import org.sopt.makers.crew.main.common.config.jwt.JwtAuthenticationFilter;
import org.sopt.makers.crew.main.common.config.jwt.JwtTokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {

private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

private static final String[] SWAGGER_URL = {
"/swagger-resources/**",
"/favicon.ico",
"/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html",
"/swagger-ui/index.html",
"/docs/swagger-ui/index.html",
"/swagger-ui/swagger-ui.css",
};

private static final String[] AUTH_WHITELIST = {
"/health"
};

@Bean
@Profile("dev")
SecurityFilterChain prodSecurityFilterChain(HttpSecurity http) throws Exception {
return http.csrf((csrfConfig) ->
csrfConfig.disable()
)
.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(
authorize -> authorize.requestMatchers(AUTH_WHITELIST).permitAll()
.requestMatchers(SWAGGER_URL).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, jwtAuthenticationEntryPoint),
UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exceptionHandling ->
exceptionHandling.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.sopt.makers.crew.main.common.config.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.sopt.makers.crew.main.common.response.CommonResponseDto;
import org.sopt.makers.crew.main.common.response.ErrorStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper mapper = new ObjectMapper();

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


public void setResponse(HttpServletResponse response, ErrorStatus status) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

CommonResponseDto apiResponse = CommonResponseDto.fail(status.getErrorCode());
response.getWriter().println(mapper.writeValueAsString(apiResponse));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.sopt.makers.crew.main.common.config.jwt;

import static org.sopt.makers.crew.main.common.config.jwt.JwtExceptionType.VALID_JWT_TOKEN;

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.sopt.makers.crew.main.common.response.ErrorStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String accessToken = jwtTokenProvider.resolveToken(request);

if (accessToken != null) {
// 토큰 검증
if (jwtTokenProvider.validateToken(accessToken)
== VALID_JWT_TOKEN) { // 토큰이 존재하고 유효한 토큰일 때만
Integer userId = jwtTokenProvider.getAccessTokenPayload(accessToken);
UserAuthentication authentication = new UserAuthentication(userId, null,
null); //사용자 객체 생성
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request)); // request 정보로 사용자 객체 디테일 설정
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
jwtAuthenticationEntryPoint.commence(request, response,
new AuthenticationException(ErrorStatus.UNAUTHORIZED_TOKEN.getErrorCode()) {
});
return;
}
}
chain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.sopt.makers.crew.main.common.config.jwt;

public enum JwtExceptionType {
VALID_JWT_TOKEN, // 유효한 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
@@ -0,0 +1,98 @@
package org.sopt.makers.crew.main.common.config.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import jakarta.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import javax.crypto.spec.SecretKeySpec;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sopt.makers.crew.main.entity.user.UserRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

private final UserRepository userRepository;

@Value("${JWT_SECRET}")
private String secretKey;

@Value("${ACCESS_TOKEN_EXPIRED_TIME}")
private Long accessTokenExpireLength;

private static final String AUTHORIZATION_HEADER = "Authorization";

public String generateAccessToken(Authentication authentication) {
Date now = new Date();
Date expiration = new Date(now.getTime() + accessTokenExpireLength);

final Claims claims = Jwts.claims()
.setIssuedAt(now)
.setExpiration(expiration);

claims.put("id", authentication.getPrincipal());

return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setClaims(claims)
.signWith(getSignKey(), SignatureAlgorithm.HS256)
.compact();
}

public Integer getAccessTokenPayload(String token) {
return Integer.parseInt(
Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(token)
.getBody().get("id").toString());
}

public String resolveToken(HttpServletRequest request) {

String header = request.getHeader(AUTHORIZATION_HEADER);

if (header == null || !header.startsWith("Bearer ")) {
return null;
} else {
return header.split(" ")[1];
}
}

public JwtExceptionType validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(token)
.getBody();
return JwtExceptionType.VALID_JWT_TOKEN;
} catch (io.jsonwebtoken.security.SignatureException exception) {
log.error("잘못된 JWT 서명을 가진 토큰입니다.");
return JwtExceptionType.INVALID_JWT_SIGNATURE;
} catch (MalformedJwtException exception) {
log.error("잘못된 JWT 토큰입니다.");
return JwtExceptionType.INVALID_JWT_TOKEN;
} catch (ExpiredJwtException exception) {
log.error("만료된 JWT 토큰입니다.");
return JwtExceptionType.EXPIRED_JWT_TOKEN;
} catch (UnsupportedJwtException exception) {
log.error("지원하지 않는 JWT 토큰입니다.");
return JwtExceptionType.UNSUPPORTED_JWT_TOKEN;
} catch (IllegalArgumentException exception) {
log.error("JWT Claims가 비어있습니다.");
return JwtExceptionType.EMPTY_JWT;
}
}

private Key getSignKey() {
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
return new SecretKeySpec(keyBytes, "HmacSHA256");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.sopt.makers.crew.main.common.config.jwt;


import java.util.Collection;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

// UsernamePasswordAuthenticationToken: 사용자의 인증 정보 저장하고 전달
public class UserAuthentication extends UsernamePasswordAuthenticationToken {

// 사용자 인증 객체 생성
public UserAuthentication(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,22 @@
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)

public enum ErrorStatus {
/**
* 400 BAD_REQUEST
*/
VALIDATION_EXCEPTION("CR-001"), // errorCode는 예시, 추후 변경 예정 -> 잘못된 요청입니다.
VALIDATION_REQUEST_MISSING_EXCEPTION("요청값이 입력되지 않았습니다."),
/**
* 400 BAD_REQUEST
*/
VALIDATION_EXCEPTION("CR-001"), // errorCode는 예시, 추후 변경 예정 -> 잘못된 요청입니다.
VALIDATION_REQUEST_MISSING_EXCEPTION("요청값이 입력되지 않았습니다."),

/**
* 500 SERVER_ERROR
*/
INTERNAL_SERVER_ERROR("예상치 못한 서버 에러가 발생했습니다.");
/**
* 401 UNAUTHORIZED
*/
UNAUTHORIZED_TOKEN("유효하지 않은 토큰입니다."),

private final String errorCode;
/**
* 500 SERVER_ERROR
*/
INTERNAL_SERVER_ERROR("예상치 못한 서버 에러가 발생했습니다.");

private final String errorCode;

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@

import lombok.RequiredArgsConstructor;
import org.sopt.makers.crew.main.health.v1.dtos.service.get_health.response.HealthServiceGetHealthResponseDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/health")
public class HealthController {

private final HealthService healthService;

@GetMapping("/")
@GetMapping("")
public ResponseEntity<HealthServiceGetHealthResponseDto> getHealth() {
return this.healthService.getHealth();
}
Expand Down

0 comments on commit 24593af

Please sign in to comment.