Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: 대피소, 로그인 api 수정 및 스웨거 헤더 추가 #31

Merged
merged 16 commits into from
Oct 17, 2023
Merged
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
17 changes: 14 additions & 3 deletions src/main/java/com/numberone/backend/LoginTestController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
package com.numberone.backend;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "login_test", description = "토큰 인증 테스트 관련 API")
@RestController
public class LoginTestController {
@RequestMapping("/logintest")
public String test(Authentication authentication){
return "Hello "+authentication.getName();
@Operation(summary = "토큰 인증 테스트하기", description =
"""
서버에서 발급받은 액세스 토큰을 "Bearer [발급받은 액세스토큰]" 형태로 http 헤더의 Authorization에 넣어서 요청해주세요.

유효한 토큰이라면 사용자의 이메일을 반환합니다.

앞으로 서버에 api 요청을 날릴때 이렇게 Authorization 헤더에 발급받은 액세스 토큰을 담아 전달해주세요.
""")
@GetMapping("/api/logintest")
public String test(Authentication authentication) {
return "Hello " + authentication.getName();
}
}
18 changes: 1 addition & 17 deletions src/main/java/com/numberone/backend/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,37 +1,22 @@
package com.numberone.backend.config;

import com.numberone.backend.config.auth.JwtFilter;
import com.numberone.backend.properties.JwtProperties;
import lombok.RequiredArgsConstructor;
import com.numberone.backend.config.auth.JwtFilter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

@Slf4j
@Configuration
@EnableWebSecurity
Expand Down Expand Up @@ -67,8 +52,7 @@ public WebSecurityCustomizer webSecurityCustomizer() {
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/token/**",
"/shelters/**");
"/token/**");
//.requestMatchers("/**"); // 인증 처리 하지 않을 케이스
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/numberone/backend/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.servers.Server;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;

@OpenAPIDefinition(
servers = @Server(url = "/", description = "${host.url}"),
Expand All @@ -22,4 +27,18 @@
)
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.components(new Components().addSecuritySchemes("JWT", bearerAuth()));
}

public SecurityScheme bearerAuth() {
return new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name(HttpHeaders.AUTHORIZATION);
}
}
58 changes: 46 additions & 12 deletions src/main/java/com/numberone/backend/config/auth/JwtFilter.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.numberone.backend.config.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.numberone.backend.domain.member.entity.Member;
import com.numberone.backend.domain.member.service.MemberService;
import com.numberone.backend.domain.token.util.JwtUtil;
import com.numberone.backend.exception.context.ExceptionContext;
import com.numberone.backend.exception.dto.ErrorResponse;
import com.numberone.backend.exception.notfound.NotFoundMemberException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
Expand All @@ -18,37 +23,66 @@
import java.io.IOException;
import java.util.Collections;

import static com.numberone.backend.exception.context.CustomExceptionContext.*;

@RequiredArgsConstructor
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final MemberService memberService;

//토큰이 유효하지 않다면 setAuthentication이 진행되지 않아 UsernamePasswordAuthenticationFilter에서 인증이 되지 않음
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String path = request.getRequestURI();
return !path.startsWith("/api/");
// /api로 시작하는 경로에 대해서만 jwt 인증을 진행합니다. 이렇게 안하면 security에서 무시한 경로라도 모든 경로에 대해서 이 필터를 거치네요..
// jwt인증이 필요한 api에 대해서는 /api/apple 처럼 /api로 시작하게 만들어야 될것같아요!
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
setErrorResponse(response, WRONG_ACCESS_TOKEN);
return;
}

String token = authorizationHeader.split(" ")[1];
if (!jwtUtil.isValid(token)) {
setErrorResponse(response, WRONG_ACCESS_TOKEN);
return;
}
if (jwtUtil.isExpired(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Token has expired");
filterChain.doFilter(request, response);
setErrorResponse(response, EXPIRED_ACCESS_TOKEN);
return;
}

String email = jwtUtil.getEmail(token);
Member member = memberService.findByEmail(email);
try {
Member member = memberService.findByEmail(email);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
member.getEmail(), null, Collections.emptyList());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
member.getEmail(), null, Collections.emptyList());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
} catch (NotFoundMemberException e) {
setErrorResponse(response, NOT_FOUND_MEMBER);
return;
}
}

SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
private void setErrorResponse(
HttpServletResponse response,
ExceptionContext context
) {
ObjectMapper objectMapper = new ObjectMapper();
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json;charset=UTF-8");
ErrorResponse errorResponse = new ErrorResponse(context.getCode(), context.getMessage());
try {
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
} catch (IOException e) {
e.printStackTrace();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public Member findByEmail(String email) {
.orElseThrow(NotFoundMemberException::new);
}

@Transactional
public void create(String email) {
memberRepository.save(Member.of(email));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
@Tag(name = "shelters", description = "대피소 관련 API")
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/shelters")
@RequestMapping("/api/shelters")
@RestController
public class ShelterController {
private final ShelterService shelterService;
Expand All @@ -32,6 +32,8 @@ public class ShelterController {
distance 는 미터(m) 단위이며 1500 m 이내 대피소만 검색합니다.

검색 결과가 0 개인 경우, NotFound 예외를 터뜨립니다.

access token 을 헤더에 담아서 요청해주세요.

""")
@PostMapping
Expand All @@ -54,6 +56,7 @@ public ResponseEntity<NearestShelterResponse> getNearestShelter(

검색 결과가 0 개인 경우, NotFound 예외를 터뜨립니다.

access token 을 헤더에 담아서 요청해주세요.
""")
@PostMapping("/list")
public ResponseEntity<NearbyShelterListResponse> getNearbyShelterList(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.numberone.backend.domain.token.controller;

import com.numberone.backend.domain.token.dto.request.TokenRequest;
import com.numberone.backend.domain.token.dto.response.TokenResponse;
import com.numberone.backend.domain.token.dto.request.GetTokenRequest;
import com.numberone.backend.domain.token.dto.request.RefreshTokenRequest;
import com.numberone.backend.domain.token.dto.response.GetTokenResponse;
import com.numberone.backend.domain.token.dto.response.RefreshTokenResponse;
import com.numberone.backend.domain.token.service.TokenService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -27,7 +30,7 @@ public class TokenController {
이후 서버에 API 요청시 이 JWT 토큰을 같이 담아서 요청해야 정상적으로 API가 호출 됩니다.
""")
@PostMapping("/kakao")
public TokenResponse loginKakao(@RequestBody TokenRequest tokenRequest) {
public GetTokenResponse loginKakao(@RequestBody @Valid GetTokenRequest tokenRequest) {
return tokenService.loginKakao(tokenRequest);
}

Expand All @@ -40,7 +43,7 @@ public TokenResponse loginKakao(@RequestBody TokenRequest tokenRequest) {
이후 서버에 API 요청시 이 JWT 토큰을 같이 담아서 요청해야 정상적으로 API가 호출 됩니다.
""")
@PostMapping("/naver")
public TokenResponse loginNaver(@RequestBody TokenRequest tokenRequest) {
public GetTokenResponse loginNaver(@RequestBody @Valid GetTokenRequest tokenRequest) {
return tokenService.loginNaver(tokenRequest);
}

Expand All @@ -51,7 +54,7 @@ public TokenResponse loginNaver(@RequestBody TokenRequest tokenRequest) {
새로 사용할 수 있는 JWT 토큰이 발급됩니다.
""")
@PostMapping("/refresh")
public TokenResponse refresh(@RequestBody TokenRequest tokenRequest) {
public RefreshTokenResponse refresh(@RequestBody @Valid RefreshTokenRequest tokenRequest) {
return tokenService.refresh(tokenRequest);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.numberone.backend.domain.token.dto.request;

import jakarta.validation.constraints.NotNull;
import lombok.*;
import org.hibernate.annotations.Comment;

@ToString
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class GetTokenRequest {
@NotNull(message = "토큰이 NULL 입니다.")
@Comment("카카오 또는 네이버에서 발급된 Access 토큰")
private String token;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.numberone.backend.domain.token.dto.request;

import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshTokenRequest {
@NotNull(message = "토큰이 NULL 입니다.")
@Comment("서버에서 발급받은 Refresh 토큰")
private String token;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.numberone.backend.domain.token.dto.response;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class GetTokenResponse {
private String accessToken;
private String refreshToken;

@Builder
public GetTokenResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}

public static GetTokenResponse of(String accessToken, String refreshToken) {
return GetTokenResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.numberone.backend.domain.token.dto.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -8,6 +9,7 @@
@ToString
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@JsonIgnoreProperties(ignoreUnknown = true)
public class KakaoInfoResponse {
private Long id;
private String connected_at;
Expand All @@ -17,6 +19,7 @@ public class KakaoInfoResponse {
@ToString
@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Properties {
private String nickname;
private String profile_image;
Expand All @@ -26,6 +29,7 @@ public class Properties {
@ToString
@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class KakaoAccount {
static class profile {
private String nickname;
Expand Down
Loading
Loading