diff --git a/src/main/java/com/juu/juulabel/admin/AdminController.java b/src/main/java/com/juu/juulabel/admin/AdminController.java index d6023c29..6e8fedb7 100644 --- a/src/main/java/com/juu/juulabel/admin/AdminController.java +++ b/src/main/java/com/juu/juulabel/admin/AdminController.java @@ -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", @@ -35,4 +33,8 @@ public ResponseEntity> assignBadge( return CommonResponse.success(SuccessCode.SUCCESS); } + @GetMapping("/permission/test") + public ResponseEntity test(@AuthenticationPrincipal Member member) { + return ResponseEntity.ok(member); + } } diff --git a/src/main/java/com/juu/juulabel/admin/TestAccessTokenController.java b/src/main/java/com/juu/juulabel/admin/TestAccessTokenController.java index 79553e63..dda372b8 100644 --- a/src/main/java/com/juu/juulabel/admin/TestAccessTokenController.java +++ b/src/main/java/com/juu/juulabel/admin/TestAccessTokenController.java @@ -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; @@ -18,6 +20,7 @@ public class TestAccessTokenController { private final JwtTokenProvider jwtTokenProvider; + private final MemberService memberService; @Operation( summary = "JWT 테스트용 토큰 발급 API", @@ -25,7 +28,8 @@ public class TestAccessTokenController { ) @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); } } diff --git a/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java b/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java index c01eab2b..1b2d8dd3 100644 --- a/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java +++ b/src/main/java/com/juu/juulabel/common/config/SecurityConfig.java @@ -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; @@ -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); diff --git a/src/main/java/com/juu/juulabel/common/config/SwaggerConfig.java b/src/main/java/com/juu/juulabel/common/config/SwaggerConfig.java index 1e6a97ad..4d7d3bce 100644 --- a/src/main/java/com/juu/juulabel/common/config/SwaggerConfig.java +++ b/src/main/java/com/juu/juulabel/common/config/SwaggerConfig.java @@ -15,7 +15,7 @@ @OpenAPIDefinition( servers = { @Server(url = "https://dev.juulabel.com", description = "Server"), - @Server(url = "http://localhost:8080", description = "Local") + @Server(url = "http://localhost:8084", description = "Local") } ) @Configuration diff --git a/src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java b/src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java index 5224856a..788bea12 100644 --- a/src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java +++ b/src/main/java/com/juu/juulabel/common/provider/JwtTokenProvider.java @@ -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 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) { @@ -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(); } diff --git a/src/main/java/com/juu/juulabel/member/service/MemberService.java b/src/main/java/com/juu/juulabel/member/service/MemberService.java index 51979731..fb5b0686 100644 --- a/src/main/java/com/juu/juulabel/member/service/MemberService.java +++ b/src/main/java/com/juu/juulabel/member/service/MemberService.java @@ -65,6 +65,10 @@ public class MemberService { private final FollowReader followReader; + public Member getMemberByEmail(String email) { + return memberReader.getByEmail(email); + } + @Transactional public LoginResponse login(OAuthLoginRequest oAuthLoginRequest) { OAuthLoginInfo authLoginInfo = oAuthLoginRequest.toDto(); @@ -86,8 +90,13 @@ public LoginResponse login(OAuthLoginRequest oAuthLoginRequest) { validateNotWithdrawnMember(email); - Token token = createTokenForMember(isNewMember, email); // TODO : 카카오와 구글 이메일이 같다면 토큰 중복 사용 가능 여부 확인 - + String generatedToken = jwtTokenProvider.createAccessToken(getMemberByEmail(email)); + Token token; + if (isNewMember) { + token = new Token(null, null); + }else{ + token = new Token(generatedToken, jwtTokenProvider.getExpirationByToken(generatedToken)); + } return new LoginResponse( token, isNewMember, @@ -117,7 +126,7 @@ public SignUpMemberResponse signUp(SignUpMemberRequest signUpRequest) { // TODO memberTermsWriter.storeAll(memberTerms); } - String token = jwtTokenProvider.createAccessToken(member.getEmail()); + String token = jwtTokenProvider.createAccessToken(member); return new SignUpMemberResponse( member.getId(), @@ -125,14 +134,6 @@ public SignUpMemberResponse signUp(SignUpMemberRequest signUpRequest) { // TODO ); } - private Token createTokenForMember(boolean isNewMember, String email) { - if (isNewMember) { - return new Token(null, null); - } else { - String generatedToken = jwtTokenProvider.createAccessToken(email); - return new Token(generatedToken, jwtTokenProvider.getExpirationByToken(generatedToken)); - } - } private List getMemberAlcoholTypeList(Member member, List alcoholTypeIdList) { return alcoholTypeIdList.stream()