Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/team-REDDI/reddi-server int…
Browse files Browse the repository at this point in the history
…o feat/#10/infra-setup
  • Loading branch information
itsme-shawn committed Jan 13, 2024
2 parents 4f29796 + 787531f commit 75e4a70
Show file tree
Hide file tree
Showing 27 changed files with 786 additions and 4 deletions.
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,14 @@ dependencies {

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '3.0.5'

// Swagger 3.0.0
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'

implementation 'org.springframework.boot:spring-boot-starter-actuator'

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis'

}

tasks.named('test') {
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/example/reddiserver/common/ApiResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public static <T> ApiResponse<T> successResponse(T data) {
return new ApiResponse<>(SUCCESS_STATUS, data, null);
}

public static <T> ApiResponse<T> successResponse(T data, String message) {
return new ApiResponse<>(SUCCESS_STATUS, data, message);
}

public static ApiResponse<?> successWithNoContent() {
return new ApiResponse<>(SUCCESS_STATUS, null, null);
}
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/com/example/reddiserver/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.reddiserver.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String redisHost;

@Value("${spring.data.redis.port}")
private int redisPort;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}

@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();

redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());

return redisTemplate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.reddiserver.controller.auth;

import com.example.reddiserver.common.ApiResponse;
import com.example.reddiserver.dto.auth.request.ReissueRequestDto;
import com.example.reddiserver.dto.security.response.TokenDto;
import com.example.reddiserver.service.auth.AuthService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthController {
private final AuthService authService;

@PostMapping("/reissue")
public ApiResponse<TokenDto> reissue(@RequestBody ReissueRequestDto reissueRequestDto) {
return ApiResponse.successResponse(authService.reissue(reissueRequestDto), "Access Token 재발급 성공");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.reddiserver.dto.auth.request;

import lombok.Getter;

@Getter
public class ReissueRequestDto {
private String accessToken;
private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.reddiserver.dto.security.response;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Builder;
import lombok.Getter;

@Getter
public class JwtErrorResponse {
private static final ObjectMapper objectMapper = new ObjectMapper();

private int status;
private String message;

@Builder
public JwtErrorResponse(int status, String message) {
this.status = status;
this.message = message;
}
public static JwtErrorResponse of(int status, String message) {
return JwtErrorResponse.builder()
.status(status)
.message(message)
.build();
}

public String convertToJson() throws JsonProcessingException {
return objectMapper.writeValueAsString(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.reddiserver.dto.security.response;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class TokenDto {
private String accessToken;
private String refreshToken;
}
6 changes: 3 additions & 3 deletions src/main/java/com/example/reddiserver/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class Member extends BaseTimeEntity {
private List<Bookmark> bookmarks = new ArrayList<>();

@Column(nullable = false, unique = true)
private String email;
private String providerId;

@Column(nullable = false)
private String name;
Expand All @@ -42,8 +42,8 @@ public class Member extends BaseTimeEntity {
private Authority authority;

@Builder
public Member(String email, String name, String imageUrl, ProviderType providerType, Authority authority) {
this.email = email;
public Member(String providerId, String name, String imageUrl, ProviderType providerType, Authority authority) {
this.providerId = providerId;
this.name = name;
this.imageUrl = imageUrl;
this.providerType = providerType;
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/com/example/reddiserver/entity/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.reddiserver.entity;

import lombok.Builder;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;

@Getter
@RedisHash(value = "refreshToken", timeToLive = 60*60*24*3)
public class RefreshToken {

@Id
private String refreshToken;

@Indexed
private String providerId;


@Builder
public RefreshToken(String providerId, String refreshToken) {
this.providerId = providerId;
this.refreshToken = refreshToken;
}

public RefreshToken updateToken(String token) {
this.refreshToken = token;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.reddiserver.repository;

import com.example.reddiserver.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByProviderId(String providerId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.reddiserver.repository;

import com.example.reddiserver.entity.RefreshToken;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
Optional<RefreshToken> findByProviderId(String providerId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.reddiserver.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();

config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");

source.registerCorsConfiguration("/api/**", config);

return source;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.example.reddiserver.security.config;

import com.example.reddiserver.security.jwt.JwtAuthenticationFilter;
import com.example.reddiserver.security.jwt.JwtExceptionHandlerFilter;
import com.example.reddiserver.security.jwt.exception.JwtAccessDeniedHandler;
import com.example.reddiserver.security.jwt.exception.JwtAuthenticationEntryPoint;
import com.example.reddiserver.security.oauth.handler.OAuthFailureHandler;
import com.example.reddiserver.security.oauth.handler.OAuthSuccessHandler;
import com.example.reddiserver.security.oauth.PrincipalOAuth2DetailsService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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;
import org.springframework.web.cors.CorsConfigurationSource;

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtExceptionHandlerFilter jwtExceptionHandlerFilter;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CorsConfigurationSource corsConfigurationSource;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final PrincipalOAuth2DetailsService customOAuth2UserService;
private final OAuthSuccessHandler oAuthSuccessHandler;
private final OAuthFailureHandler oAuthFailureHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors((cors) -> cors.configurationSource(corsConfigurationSource))
.csrf(csrf -> csrf.disable())
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.httpBasic(httpBasic -> httpBasic.disable())
.formLogin(formLogin -> formLogin.disable())
.exceptionHandling(authenticationManager -> authenticationManager
.accessDeniedHandler(jwtAccessDeniedHandler)
.authenticationEntryPoint(jwtAuthenticationEntryPoint))
.oauth2Login(oauth2LoginConfigurer -> oauth2LoginConfigurer
.failureHandler(oAuthFailureHandler)
.successHandler(oAuthSuccessHandler)
.userInfoEndpoint()
.userService(customOAuth2UserService))
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/**").permitAll()
.requestMatchers(HttpMethod.GET, "/swagger-ui/**").permitAll()
.requestMatchers(HttpMethod.GET, "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
);

http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(jwtExceptionHandlerFilter, JwtAuthenticationFilter.class);

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.example.reddiserver.security.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer";
private final TokenProvider tokenProvider;

private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);

if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}

return null;
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwt = resolveToken(request);

if (StringUtils.hasText(jwt) && tokenProvider.validateAccessToken(jwt)) {
Authentication authentication = tokenProvider.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.reddiserver.security.jwt;

import com.example.reddiserver.dto.security.response.JwtErrorResponse;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtExceptionHandlerFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (io.jsonwebtoken.security.SignatureException | MalformedJwtException e) {
throw new JwtException("잘못된 JWT 서명입니다.", e);
} catch (ExpiredJwtException e) {
throw new JwtException("만료된 JWT 토큰입니다.", e);
} catch (UnsupportedJwtException e) {
throw new JwtException("지원되지 않는 JWT 토큰입니다.", e);
} catch (IllegalArgumentException e) {
throw new JwtException("Jwt 토큰이 잘못되었습니다.", e);
}
}

public void setErrorResponse(HttpStatus status, HttpServletResponse response, Throwable e) throws IOException {
response.setStatus(status.value());
response.setContentType("application/json; charset=UTF-8");

response.getWriter().write(
JwtErrorResponse.of(
HttpServletResponse.SC_UNAUTHORIZED,
e.getMessage()
).convertToJson()
);
}
}
Loading

0 comments on commit 75e4a70

Please sign in to comment.