Skip to content
Closed
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
10 changes: 6 additions & 4 deletions src/main/java/com/juu/juulabel/admin/AdminController.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@Tag(
name = "관리자 API",
Expand All @@ -35,4 +33,8 @@ public ResponseEntity<CommonResponse<Void>> assignBadge(
return CommonResponse.success(SuccessCode.SUCCESS);
}

@GetMapping("/permission/test")
public ResponseEntity<Member> test(@AuthenticationPrincipal Member member) {
return ResponseEntity.ok(member);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@


import com.juu.juulabel.common.provider.JwtTokenProvider;
import com.juu.juulabel.member.domain.Member;
import com.juu.juulabel.member.service.MemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
Expand All @@ -18,14 +20,16 @@
public class TestAccessTokenController {

private final JwtTokenProvider jwtTokenProvider;
private final MemberService memberService;

@Operation(
summary = "JWT 테스트용 토큰 발급 API",
description = "기본 rldh11111@naver.com 이메일로 JWT 발급"
)
@GetMapping("/token")
public String testAccessToken(@RequestParam(defaultValue = "rldh11111@naver.com") String email) {
return jwtTokenProvider.createAccessToken(email);
Member member = memberService.getMemberByEmail(email);
return jwtTokenProvider.createAccessToken(member);
}

}
75 changes: 37 additions & 38 deletions src/main/java/com/juu/juulabel/common/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@

import com.juu.juulabel.common.filter.JwtAuthorizationFilter;
import com.juu.juulabel.common.filter.JwtExceptionFilter;
import com.juu.juulabel.member.domain.MemberRole;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.List;

import static org.springframework.http.HttpMethod.OPTIONS;
Expand All @@ -29,58 +29,57 @@ public class SecurityConfig {
private final JwtExceptionFilter jwtExceptionFilter;

private static final String[] PERMIT_PATHS = {
"/swagger-ui/**", "/v3/api-docs/**", "/error", "/favicon.ico", "/", "/actuator/**",
"/v1/api/alcohols/**", "/v1/api/terms/**", "/v1/api/images",
"/v1/api/members/**", "/v1/api/shared-space/tasting-notes/**", "/v1/api/notifications/**",
"/v1/api/daily-lives/**", "/v1/api/alcoholicDrinks/**", "v1/api/follow" , "/**"
"/swagger-ui/**", "/v3/api-docs/**", "/error", "/favicon.ico", "/", "/actuator/**",
"/v1/api/alcohols/**", "/v1/api/terms/**", "/v1/api/images",
"/v1/api/members/**", "/v1/api/shared-space/tasting-notes/**", "/v1/api/notifications/**",
"/v1/api/daily-lives/**", "/v1/api/alcoholicDrinks/**", "v1/api/follow",
"/**"
};

private static final String[] ALLOW_ORIGINS = {
"http://localhost:8084",
"http://localhost:8080",
"http://localhost:5173",
"http://localhost:3000",
"https://api.juulabel.com",
"https://qa.juulabel.com",
"https://juulabel.com",
"https://juulabel.shop",
"https://juulabel-front.vercel.app/",
"https://juulabel-front-seven.vercel.app/",
"https://d3jwyw9rpnxu8p.cloudfront.net"
"http://localhost:8084",
"http://localhost:8080",
"http://localhost:5173",
"http://localhost:3000",
"https://api.juulabel.com",
"https://qa.juulabel.com",
"https://juulabel.com",
"https://juulabel.shop",
"https://juulabel-front.vercel.app/",
"https://juulabel-front-seven.vercel.app/",
"https://d3jwyw9rpnxu8p.cloudfront.net"
};

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/v1/api/members/logout").authenticated()
.requestMatchers(OPTIONS, "**").permitAll()
.requestMatchers(PERMIT_PATHS).permitAll()
.anyRequest().authenticated()
)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/v1/api/members/logout").authenticated()
.requestMatchers(OPTIONS, "**").permitAll()
.requestMatchers(PERMIT_PATHS).permitAll()
.requestMatchers("/v1/api/admins/permission/test").hasAnyAuthority(MemberRole.ROLE_ADMIN.name())
.anyRequest().authenticated()
)

.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter, JwtAuthorizationFilter.class)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtExceptionFilter, JwtAuthorizationFilter.class)

.build();
.build();
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
config.setAllowedOrigins(List.of(ALLOW_ORIGINS));
config.addExposedHeader(HttpHeaders.AUTHORIZATION);
config.setAllowCredentials(true);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.juu.juulabel.common.exception.handler;

import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.juu.juulabel.common.exception.BaseException;
import com.juu.juulabel.common.exception.code.ErrorCode;
import com.juu.juulabel.common.response.CommonResponse;
Expand All @@ -8,11 +9,13 @@
import io.sentry.Sentry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.util.Arrays;
import java.util.Objects;

@Slf4j
Expand Down Expand Up @@ -60,8 +63,25 @@ public ResponseEntity<CommonResponse<String>> handle(MalformedJwtException e) {

@ExceptionHandler(NoResourceFoundException.class)
public void handle(NoResourceFoundException e) {
// 이거 키면 출력이 너무 많이 됨
//log.warn("NoResourceFoundException : {}", e.getMessage());
// 이거 키면 출력이 너무 많이 됨
//log.warn("NoResourceFoundException : {}", e.getMessage());
}

@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<CommonResponse<String>> handleValidationException(HttpMessageNotReadableException exception) {
String errorDetails = "";
log.error("HttpMessageNotReadableException :", exception);
if (exception.getCause() instanceof InvalidFormatException invalidFormatException) {
if (invalidFormatException.getTargetType() != null && invalidFormatException.getTargetType().isEnum()) {
errorDetails = String.format("'%s'. 값은 다음 중 하나여야 합니다: %s.",
invalidFormatException.getPath().getLast().getFieldName(),
Arrays.toString(invalidFormatException.getTargetType().getEnumConstants())
);
}
}
if (errorDetails.isEmpty()) {
errorDetails = exception.getMessage();
}
return CommonResponse.fail(ErrorCode.VALIDATION_ERROR, errorDetails);
}
}
Original file line number Diff line number Diff line change
@@ -1,63 +1,71 @@
package com.juu.juulabel.common.provider;

import com.juu.juulabel.common.principal.CustomUserDetailsService;
import com.juu.juulabel.common.principal.JuulabelMember;
import com.juu.juulabel.common.constants.AuthConstants;
import com.juu.juulabel.common.exception.CustomJwtException;
import com.juu.juulabel.common.exception.InvalidParamException;
import com.juu.juulabel.common.exception.code.ErrorCode;
import com.juu.juulabel.member.domain.Member;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.crypto.SecretKey;
import java.time.Duration;
import java.util.Base64;
import java.util.Date;
import java.util.Optional;
import java.util.*;

@Component
public class JwtTokenProvider {

public static final Long ACCESS_TOKEN_EXPIRE_TIME = Duration.ofHours(6).toMillis();
private static final long ACCESS_TOKEN_EXPIRE_TIME = Duration.ofDays(1).toMillis();
private static final String ISSUER = "juulabel";
private static final String ROLE_CLAIM = "role";

private final SecretKey key;
private final CustomUserDetailsService customUserDetailsService;

public JwtTokenProvider(
@Value("${spring.jwt.secret}") String key,
CustomUserDetailsService customUserDetailsService
) {
public JwtTokenProvider(@Value("${spring.jwt.secret}") String key) {
byte[] keyBytes = Base64.getDecoder().decode(key);
this.key = Keys.hmacShaKeyFor(keyBytes);
this.customUserDetailsService = customUserDetailsService;
}

public String createAccessToken(String email) {
Date now = new Date();
long accessTokenExpireTime = now.getTime() + ACCESS_TOKEN_EXPIRE_TIME;

public String createAccessToken(Member member) {
return Jwts.builder()
.subject(email)
.issuedAt(now)
.expiration(new Date(accessTokenExpireTime))
.subject(String.valueOf(member.getId()))
.claim(ROLE_CLAIM, member.getRole().name())
.issuedAt(new Date())
.issuer(ISSUER)
.expiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRE_TIME))
.signWith(key)
.compact();
}

public Authentication getAuthentication(String token) {
JuulabelMember userDetails = (JuulabelMember) customUserDetailsService.loadUserByUsername(getEmailByToken(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());

public Authentication getAuthentication(String accessToken) {
Claims claims = parseClaims(accessToken);

Collection<? extends GrantedAuthority> roles =
Collections.singletonList(new SimpleGrantedAuthority(claims.get(ROLE_CLAIM, String.class)));

Member member = Member.builder()
.id(Long.parseLong(claims.getSubject()))
.build();

return new UsernamePasswordAuthenticationToken(
member,
null,
roles
);
}

public String resolveToken(String header) {
return Optional.ofNullable(header)
.orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_AUTHENTICATION))
.replace(AuthConstants.TOKEN_PREFIX, "");
.orElseThrow(() -> new InvalidParamException(ErrorCode.INVALID_AUTHENTICATION))
.replace(AuthConstants.TOKEN_PREFIX, "");
}

public boolean isValidateToken(String token) {
Expand All @@ -68,10 +76,6 @@ public boolean isValidateToken(String token) {
return !getExpirationByToken(token).before(new Date());
}

public String getEmailByToken(String token) {
return parseClaims(token).getSubject();
}

public Date getExpirationByToken(String token) {
return parseClaims(token).getExpiration();
}
Expand Down
Loading