Skip to content
Open
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: 10 additions & 0 deletions 10주차 키워드 155328105aa680d18254c22e7537c8a7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# 10주차 키워드

**Spring Security**

- Spring Security는 애플리케이션 보안을 처리하는 강력한 프레임워크로, 인증(Authentication)과 인가(Authorization)을 쉽게 구현할 수 있도록 다양한 기능과 확장성을 제공

**인증(Authentication)과 인가(Authorization)**

- **인증**은 사용자가 누구인지 확인하는 과정입니다. 사용자 이름(Username)과 비밀번호(Password) 등을 기반으로 사용자가 시스템에 접근할 수 있는지 검증
- **인가**는 인증된 사용자가 특정 리소스나 기능에 접근할 권한이 있는지 확인하는 과정
11 changes: 11 additions & 0 deletions chap05/mission/umc7/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ dependencies {
// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'

// security
implementation 'org.springframework.boot:spring-boot-starter-security'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public enum ErrorStatus implements BaseErrorCode {
// Member Error
MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),
NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수 입니다."),
MEMBER_ALREADY_LOGOUT(HttpStatus.BAD_REQUEST, "MEMBER4003", "이미 로그아웃한 사용자입니다."),


// Article Error
ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다."),
Expand All @@ -44,6 +46,10 @@ public enum ErrorStatus implements BaseErrorCode {
PAGE_NUM_NOT_VALID(HttpStatus.BAD_REQUEST, "PAGENUM4001", "page가 0이하일 수 없습니다."),


TOKEN_MISMATCH(HttpStatus.BAD_REQUEST, "TOKEN4001", "토큰 종류가 올바르지 않습니다."),

MEMBER_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "TOKEN4002", "사용자의 토큰을 찾을 수 없습니다."),

;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.umc7.domain.auth;

import com.example.umc7.domain.member.Member;
import java.util.ArrayList;
import java.util.Collection;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@Getter
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {

private Member member;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return member.getRole().name();
}
});

return collection;
}

@Override
public String getPassword() {
return null;
}

@Override
public String getUsername() {
return member.getClientId();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.example.umc7.domain.auth.controller;

import com.example.umc7.apipayload.ApiResponse;
import com.example.umc7.domain.auth.dto.TokenResponseDto;
import com.example.umc7.domain.auth.service.AuthService;
import com.example.umc7.domain.member.Member;
import com.example.umc7.domain.member.dto.LoginSuccessDto;
import com.example.umc7.domain.member.service.MemberService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
@Tag(name = "Auth", description = "인증 인가 관련 API")
public class AuthController {

private final AuthService authService;
private final MemberService memberService;

private final static String LOGIN_SUCCESS_REDIRECT_URL_LOCAL = "http://localhost:5173";

@Operation(
summary = "카카오 로그인 페이지로 리다이렉트 API",
description = "카카오 로그인 페이지로 리다이렉트하는 API입니다."
)
@GetMapping("/oauth2/kakao")
public void getLoginUrl(HttpServletResponse response) throws IOException {
String redirectUrl = authService.getRedirectUrl();
log.info("{}", redirectUrl);
response.sendRedirect(redirectUrl);
}

@Operation(
summary = "인가 코드를 받아 처리하는 API",
description = "인가 코드 받아 처리하는 API입니다."
)
@GetMapping("/oauth2/kakao/code")
public void callBack(
@RequestParam(required = false) String code, HttpServletResponse response)
throws IOException {
Map<String, String> memberInfo = authService.getMemberInfo(code);
LoginSuccessDto loginSuccessDto = memberService.login(memberInfo);

String redirectUrl =
LOGIN_SUCCESS_REDIRECT_URL_LOCAL + "?isFirstLogin=" + loginSuccessDto.getIsFirstLogin()
+ "&accessToken=" + loginSuccessDto.getTokenResponseDto().getAccessToken()
+ "&refreshToken=" + loginSuccessDto.getTokenResponseDto().getRefreshToken();

response.sendRedirect(redirectUrl);
}

@Operation(
summary = "access token 재발급 API",
description = "access token 재발급하는 API입니다. 해당 API는 헤더에 refreshToken과 함께 요청해야합니다.",
responses = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "COMMON200",
description = "재발급된 acessToken과 기존의 refreshToken을 응답",
content = @Content(schema = @Schema(implementation = TokenResponseDto.class))
)
}
)
@GetMapping("/reissue")
public ResponseEntity<ApiResponse<TokenResponseDto>> reissue(
@RequestAttribute("refresh") String refreshToken) {
return ResponseEntity.ok(ApiResponse.onSuccess(authService.reissue(refreshToken)));
}

@Operation(
summary = "로그아웃 API",
description = "로그아웃 API입니다. 해당 API는 사용자 인증이 요구됩니다.",
responses = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "COMMON200",
description = "로그아웃 결과 반환",
content = @Content(schema = @Schema(implementation = String.class))
)
}
)
@PostMapping("/logout")
public ResponseEntity<ApiResponse<String>> logout(@AuthenticationPrincipal Member member) {
authService.deleteRefreshToken(member);
return ResponseEntity.ok(ApiResponse.onSuccess("로그아웃 성공"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.umc7.domain.auth.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
@Schema(description = "토큰 재발급 응답에 대한 DTO")
public class TokenResponseDto {

@Schema(description = "재발급된 accessToken", example = "")
private String accessToken;
@Schema(description = "기존 refreshToken", example = "")
private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.umc7.domain.auth.enums;

public enum TokenType {

ACCESS,
REFRESH;

@Override
public String toString() {
return super.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.umc7.domain.auth.handler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;

/**
* 필터를 지나 인증 과정을 거친 후 SecurityContext에 인증 객체는 존재하지만,
* 해당 인증 객체의 권한이 접근하려는 리소스에 접근할 수 있는 권한과 맞지 않으면 AccessDeninedHandler가 호출된다.
*
* 정리 : 인증은 되었지만, 인가(권한)이 맞지 않는 경우
*/

@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

private final HandlerExceptionResolver handlerExceptionResolver;

public CustomAccessDeniedHandler(
@Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver) {
this.handlerExceptionResolver = handlerExceptionResolver;
}

@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
log.info("CustomAccessDeniendHandler 동작");

handlerExceptionResolver.resolveException(request, response, null, accessDeniedException);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.example.umc7.domain.auth.handler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;

/**
* 필터를 지나 인증 과정을 거친 후 SecurityContext에 인증 객체가 없는 경우,
* 리소스에 접근하려할 때 AuthenticationEntryPoint를 호출한다.
*/

@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final HandlerExceptionResolver handlerExceptionResolver;

public CustomAuthenticationEntryPoint(
@Qualifier("handlerExceptionResolver") HandlerExceptionResolver handlerExceptionResolver) {
this.handlerExceptionResolver = handlerExceptionResolver;
}

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.info("CustomAuthenticationEntryPoint 동작");

// 토큰에 문제 있거나, 토큰에 담긴 clientId의 유저가 우리 db에 없는 경우
if (request.getAttribute("exception") != null) {
handlerExceptionResolver.resolveException(request, response, null,
(Exception) request.getAttribute("exception"));
} else { // 토큰을 담지 않아서 jwtFilter에서 토큰 검증 로직 없이 다음 필터로 넘어간 경우
handlerExceptionResolver.resolveException(request, response, null, authException);
}
}
}
Loading