Skip to content

Commit

Permalink
Merge pull request #7 from GDSC-KNU/feat/auth
Browse files Browse the repository at this point in the history
인증/인가 구현 #5
  • Loading branch information
fanta4715 authored Apr 27, 2024
2 parents 78770ff + 5d4a677 commit deb5328
Show file tree
Hide file tree
Showing 26 changed files with 874 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ out/

### VS Code ###
.vscode/

### env ###
.env
24 changes: 24 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,30 @@ dependencies {
// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

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

//jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.0'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

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

//JSON
implementation 'com.google.code.gson:gson'

//WebClient
implementation 'org.springframework.boot:spring-boot-starter-webflux'

//Postgre SQL
implementation 'org.postgresql:postgresql'

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

// Websocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/gdsc/comunity/annotation/UserId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package gdsc.comunity.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserId {
}
33 changes: 33 additions & 0 deletions src/main/java/gdsc/comunity/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package gdsc.comunity.controller;

import gdsc.comunity.dto.JwtTokensDto;
import gdsc.comunity.dto.RefreshTokenDto;
import gdsc.comunity.service.AuthService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
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;

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

@PostMapping("/refresh")
public ResponseEntity<JwtTokensDto> refreshNewTokens(
@RequestBody RefreshTokenDto refreshTokenDto
) {
//1. refresh token 유효성 검증
authService.validateRefreshToken(refreshTokenDto.refreshToken());

JwtTokensDto newTokens = authService.refreshNewTokens(refreshTokenDto.refreshToken());
return ResponseEntity.ok(newTokens);
}
}
37 changes: 37 additions & 0 deletions src/main/java/gdsc/comunity/controller/OAuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gdsc.comunity.controller;

import gdsc.comunity.dto.GoogleUserInfo;
import gdsc.comunity.dto.JwtTokensDto;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import gdsc.comunity.service.OAuthGoogleService;

@Slf4j
@RestController
@RequestMapping("/oauth")
@RequiredArgsConstructor
public class OAuthController {
private final OAuthGoogleService oAuthGoogleService;

@GetMapping("/login/google/callback")
public ResponseEntity<?> loginByGoogleCallback(
@RequestParam("code") String code
) {
//1. 해당 코드로 구글에게 토큰을 요청
String tokenByGoogle = oAuthGoogleService.getAccessToken(code);

//2. 토큰을 받아서 유저 정보를 가져옴
Map<String, String> userInfo = oAuthGoogleService.getUserInfoByAccessToken(tokenByGoogle);

//3. 유저 정보를 토대로 회원가입 or 로그인
JwtTokensDto jwtDto = oAuthGoogleService.loginOrRegister(userInfo);
return new ResponseEntity<>(jwtDto, HttpStatus.OK);
}
}
10 changes: 10 additions & 0 deletions src/main/java/gdsc/comunity/dto/JwtTokensDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package gdsc.comunity.dto;

import lombok.Builder;

@Builder
public record JwtTokensDto(
String accessToken,
String refreshToken
) {
}
9 changes: 9 additions & 0 deletions src/main/java/gdsc/comunity/dto/RefreshTokenDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gdsc.comunity.dto;

import lombok.Builder;

@Builder
public record RefreshTokenDto(
String refreshToken
) {
}
9 changes: 9 additions & 0 deletions src/main/java/gdsc/comunity/exception/CustomException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gdsc.comunity.exception;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class CustomException extends RuntimeException{
private final ErrorCode errorCode;
}
23 changes: 23 additions & 0 deletions src/main/java/gdsc/comunity/exception/ErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package gdsc.comunity.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum ErrorCode {
//401
LOGIN_REQUIRED(40100, HttpStatus.UNAUTHORIZED, "로그인이 필요합니다."),
INVALID_REFRESH_TOKEN_ERROR(40101, HttpStatus.UNAUTHORIZED, "유효하지 않은 리프레시 토큰입니다."),
INVALID_ACCESS_TOKEN_ERROR(40102, HttpStatus.UNAUTHORIZED, "유효하지 않은 엑세스 토큰입니다."),
EMPTY_REFRESH_TOKEN_ERROR(40103, HttpStatus.UNAUTHORIZED, "해당 유저의 리프레시 토큰이 존재하지 않습니다."),
INVALID_TOKEN_PREFIX_ERROR(40104, HttpStatus.UNAUTHORIZED, "토큰의 prefix가 올바르지 않습니다"),

//403
OAUTH_SERVER_ERROR(40300, HttpStatus.BAD_GATEWAY, "OAuth 서버 에러입니다.");

private final int code;
private final HttpStatus status;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package gdsc.comunity.interceptor;

import gdsc.comunity.annotation.UserId;
import gdsc.comunity.exception.CustomException;
import gdsc.comunity.exception.ErrorCode;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@Component
public class UserIdArgumentResolver implements HandlerMethodArgumentResolver {

//parameter객체의 타입을 확인하는 메소드
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Long.class)
&& parameter.hasParameterAnnotation(UserId.class);
}

@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {
Object userIdObj = webRequest.getAttribute("userId", NativeWebRequest.SCOPE_REQUEST);
if (userIdObj == null) {
throw new CustomException(ErrorCode.LOGIN_REQUIRED);
}
return Long.parseLong((String) userIdObj);
}
}
27 changes: 27 additions & 0 deletions src/main/java/gdsc/comunity/interceptor/UserIdInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package gdsc.comunity.interceptor;

import gdsc.comunity.security.info.UserPrincipal;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Slf4j
@Component
public class UserIdInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) throws Exception {
UserPrincipal userPrincipal = (UserPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

//request객체에 UserId 값 추가
request.setAttribute("userId", userPrincipal.getId());

return HandlerInterceptor.super.preHandle(request, response, handler);
}
}
36 changes: 36 additions & 0 deletions src/main/java/gdsc/comunity/interceptor/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package gdsc.comunity.interceptor.config;

import gdsc.comunity.interceptor.UserIdArgumentResolver;
import gdsc.comunity.interceptor.UserIdInterceptor;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final UserIdArgumentResolver userIdArgumentResolver;
private final UserIdInterceptor userIdInterceptor;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
WebMvcConfigurer.super.addArgumentResolvers(resolvers);
resolvers.add(this.userIdArgumentResolver);
}

@Override
public void addInterceptors(final InterceptorRegistry registry) {
registry.addInterceptor(userIdInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/api/login",
"/api/auth/register",
"/oauth/**",
"/swagger-ui/**",
"/v3/api-docs/**");
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package gdsc.comunity.repository.user;

import gdsc.comunity.entity.user.User;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
92 changes: 92 additions & 0 deletions src/main/java/gdsc/comunity/security/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package gdsc.comunity.security.config;

import gdsc.comunity.exception.CustomException;
import gdsc.comunity.exception.ErrorCode;
import gdsc.comunity.security.filter.JwtAuthenticationFilter;
import gdsc.comunity.security.filter.JwtExceptionFilter;
import gdsc.comunity.security.info.UserPrincipal;
import gdsc.comunity.security.jwt.JwtProvider;
import gdsc.comunity.security.jwt.RefreshToken;
import gdsc.comunity.security.jwt.RefreshTokenRepository;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.logout.LogoutFilter;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtProvider jwtProvider;
private final RefreshTokenRepository refreshTokenRepository;

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(sm ->
sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)

.authorizeHttpRequests(registry ->
registry
.requestMatchers("/api/login", "/api/auth/register").permitAll()
.requestMatchers("/oauth/**").permitAll()
.requestMatchers("/api/auth/refresh").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(AbstractHttpConfigurer::disable)

.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.addLogoutHandler((request, response, authentication) -> {
//1. authentication에서 userId 추출
Long userId = ((UserPrincipal) authentication.getPrincipal()).getId();

//2. 해당 userId로 redis에서 refresh token 조회 후 삭제
Optional<RefreshToken> refreshTokenOP = refreshTokenRepository.findByUserId(userId);
if (refreshTokenOP.isEmpty()) {
throw new CustomException(ErrorCode.EMPTY_REFRESH_TOKEN_ERROR);
}
refreshTokenRepository.delete(refreshTokenOP.get());
})
.logoutSuccessHandler((request, response, authentication) -> {
response.setStatus(HttpStatus.OK.value());
response.addHeader("message", "logout success");
}
)
)

.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint((request, response, authException) -> response.setStatus(HttpServletResponse.SC_UNAUTHORIZED))
.accessDeniedHandler((request, response, accessDeniedException) -> response.setStatus(HttpServletResponse.SC_FORBIDDEN))
)

.addFilterBefore(
new JwtAuthenticationFilter(jwtProvider),
LogoutFilter.class
)
.addFilterBefore(
new JwtExceptionFilter(),
JwtAuthenticationFilter.class
);

return http.build();
}
}
Loading

0 comments on commit deb5328

Please sign in to comment.