Skip to content

Commit

Permalink
Merge pull request #13 from local-mood/chore/12-env
Browse files Browse the repository at this point in the history
Feat: ํšŒ์› API ์ƒ์„ฑ
  • Loading branch information
yeni-choi authored Dec 13, 2023
2 parents b83f1c2 + ec3eb8b commit 908515e
Show file tree
Hide file tree
Showing 25 changed files with 1,007 additions and 18 deletions.
14 changes: 14 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security:3.0.4'
testImplementation 'org.springframework.security:spring-security-test'

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

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation('it.ozimov:embedded-redis:0.7.3') { exclude group: "org.slf4j", module: "slf4j-simple" }


}

tasks.named('test') {
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/ceos/vote/auth/CurrentUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.ceos.vote.auth;

import org.springframework.security.core.annotation.AuthenticationPrincipal;

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : user")
public @interface CurrentUser {
}
109 changes: 109 additions & 0 deletions src/main/java/com/ceos/vote/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.ceos.vote.auth.controller;

import com.ceos.vote.auth.service.AuthService;
import com.ceos.vote.common.dto.NormalResponseDto;
import com.ceos.vote.auth.jwt.entity.TokenDto;
import com.ceos.vote.auth.jwt.entity.LoginRequestDto;
import com.ceos.vote.domain.member.dto.MemberRequestDto;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.server.Cookie;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/app/auth")
@RequiredArgsConstructor
public class AuthController {

private final long COOKIE_EXPIRATION = 7776000; // 90์ผ

private final AuthService authService;

// ํšŒ์›๊ฐ€์ž…
@PostMapping("/signup")
public ResponseEntity<NormalResponseDto> join(@RequestBody @Valid MemberRequestDto requestDto) {
authService.joinMember(requestDto);
return ResponseEntity.ok(NormalResponseDto.success());
}

// ๋กœ๊ทธ์ธ
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequestDto loginRequest) {

TokenDto tokenDto = authService.login(loginRequest);

HttpCookie httpCookie = ResponseCookie.from("refresh-token", tokenDto.getRefreshToken())
.maxAge(COOKIE_EXPIRATION)
.httpOnly(true)
.secure(true)
.sameSite(Cookie.SameSite.NONE.attributeValue()) //์„œ๋“œํŒŒํ‹ฐ ์ฟ ํ‚ค ์‚ฌ์šฉ ํ—ˆ์šฉ
.build();
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, httpCookie.toString())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + tokenDto.getAccessToken())
.build();
}

// ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
@PostMapping("/validate")
public ResponseEntity<?> validate(@RequestHeader("Authorization") String requestAccessToken) {
if (!authService.validate(requestAccessToken)) {
return ResponseEntity.ok().build();
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}

// ํ† ํฐ ์žฌ๋ฐœ๊ธ‰
@PostMapping("/reissue")
public ResponseEntity<?> reissue(@CookieValue(name = "refresh-token") String requestRefreshToken,
@RequestHeader("Authorization") String requestAccessToken) {

TokenDto newAuthToken = authService.reissue(requestAccessToken, requestRefreshToken);

if (newAuthToken != null) {
// ์ƒˆ๋กœ์šด ํ† ํฐ ๋ฐœ๊ธ‰, ๋ฐ˜ํ™˜
ResponseCookie responseCookie = ResponseCookie.from("refresh-token", newAuthToken.getRefreshToken())
.maxAge(COOKIE_EXPIRATION)
.httpOnly(true)
.secure(true)
.sameSite(Cookie.SameSite.NONE.attributeValue())
.build();
return ResponseEntity.status(HttpStatus.OK)
.header(HttpHeaders.SET_COOKIE, responseCookie.toString())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + newAuthToken.getAccessToken())
.build();
} else {
// Refresh Token์ด ํƒˆ์ทจ ๊ฐ€๋Šฅํ•  ๋•Œ ์ฟ ํ‚ค ์‚ญ์ œํ•˜๊ณ  ์žฌ๋กœ๊ทธ์ธ
ResponseCookie responseCookie = ResponseCookie.from("refresh-token", "")
.maxAge(0)
.path("/")
.build();
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.header(HttpHeaders.SET_COOKIE, responseCookie.toString())
.build();
}
}

// ๋กœ๊ทธ์•„์›ƒ
@PostMapping("/logout")
public ResponseEntity<NormalResponseDto> logout(@RequestHeader("Authorization") String requestAccessToken) {

// Access Token์„ ๋ฌดํšจํ™”ํ•˜์—ฌ ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ
authService.logout(requestAccessToken);

ResponseCookie responseCookie = ResponseCookie.from("refresh-token", "")
.maxAge(0)
.path("/")
.build();


return ResponseEntity.status(HttpStatus.OK)
.header(HttpHeaders.SET_COOKIE, responseCookie.toString())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.ceos.vote.auth.exception;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {

// ํ•„์š”ํ•œ ๊ถŒํ•œ ์—†์ด ์ ‘๊ทผ์‹œ 403 error
response.sendError(HttpServletResponse.SC_FORBIDDEN);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ceos.vote.auth.exception;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authenticationException) throws IOException {
// ์œ ํšจํ•˜์ง€ ์•Š์€ ์ž๊ฒฉ์ฆ๋ช…์ผ ๋•Œ 401 error
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/ceos/vote/auth/jwt/entity/LoginRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.ceos.vote.auth.jwt.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LoginRequestDto {
private String email;

private String password;
}
20 changes: 20 additions & 0 deletions src/main/java/com/ceos/vote/auth/jwt/entity/TokenDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ceos.vote.auth.jwt.entity;

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

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class TokenDto {

private String accessToken;
private String refreshToken;

public TokenDto(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.ceos.vote.auth.jwt.filter;

import com.ceos.vote.auth.jwt.provider.JwtTokenProvider;
import com.ceos.vote.exception.ErrorCode;
import com.ceos.vote.exception.CeosException;
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.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;


@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
// AT ์ถ”์ถœ
String token = resolveToken(request);

try {
// ์œ ํšจ๊ธฐ๊ฐ„๋งŒ ์ œ์™ธํ•˜๊ณ  ์ •์ƒํ† ํฐ์ธ์ง€ ๊ฒ€์‚ฌ
if (token != null && jwtTokenProvider.validateToken(token)) {
Authentication authentication = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (UsernameNotFoundException e) { // ํšŒ์› ๋ชป ์ฐพ์„ ๋•Œ
throw new CeosException(ErrorCode.MEMBER_NOT_FOUND);
}

// ๋‹ค์Œ ํ•„ํ„ฐ๋กœ ์ด๋™
filterChain.doFilter(request, response);
}

// request header์—์„œ ํ† ํฐ ์ถ”์ถœ
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
Loading

0 comments on commit 908515e

Please sign in to comment.