Skip to content

Commit

Permalink
Merge pull request #11 from team-REDDI/feat/#8/security-auth
Browse files Browse the repository at this point in the history
feat : 시큐리티 환경설정 및 Auth
  • Loading branch information
YoungGyo-00 authored Jan 10, 2024
2 parents e7d1df8 + f96e80d commit 6f86e67
Show file tree
Hide file tree
Showing 22 changed files with 686 additions and 4 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ dependencies {

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

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

// Swagger 3.0.0
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.example.reddiserver.dto.security;

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);
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/example/reddiserver/dto/security/TokenDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example.reddiserver.dto.security;

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
35 changes: 35 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,35 @@
package com.example.reddiserver.entity;

import com.example.reddiserver.entity.base.BaseTimeEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshToken extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

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

@Column(nullable = false)
private String refreshToken;

@Builder
public RefreshToken(String email, String refreshToken) {
this.email = email;
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,10 @@
package com.example.reddiserver.repository;

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

import java.util.Optional;

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByEmail(String email);
}
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.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 6f86e67

Please sign in to comment.