Skip to content

Commit

Permalink
[LINKER-89] oAuth2 로그인 (#27)
Browse files Browse the repository at this point in the history
* feat: Security 기본 설정

* feat: oAuth구성요소 기본설정

* feat: oAuth Login

* feat: Jwt Generator

* feat: Token 발행

* fix: 불필요한 코드 삭제

* fix: 불필요한 코드 삭제

* fix: 프론트 기본 URL로 변경

* fix: 불필요한 코드 삭제

* fix: yml 설정분리
  • Loading branch information
ktj1997 authored Jan 22, 2024
1 parent bb0e093 commit 8ca7c6e
Show file tree
Hide file tree
Showing 31 changed files with 743 additions and 13 deletions.
5 changes: 3 additions & 2 deletions common/error/src/main/java/com/imlinker/error/ErrorType.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ public enum ErrorType {
UNAUTHENTICATED(401, "UNAUTHENTICATED", "인증되지 않은 사용자입니다."),
UNAUTHORIZED(403, "UNAUTHORIZED", "권한이 없는 사용자입니다."),
INVALID_REQUEST_PARAMETER(400, "INVALID_REQUEST_PARAMETER", "잘못된 요청 파라미터 입니다."),
INTERNAL_PROCESSING_ERROR(500, "INTERNAL_PROCESSING_ERROR", "내부 시스템 에러가 발생했습니다."),

USER_NOT_FOUND(404, "USER_NOT_FOUND", "사용자를 찾을 수 없습니다."),
NEWS_NOT_FOUND(404, "NEWS_NOT_FOUND", "뉴스를 찾을 수 없습니다."),
TAG_NOT_FOUND(404, "TAG_NOT_FOUND", "태그를 찾을 수 없습니다.");
TAG_NOT_FOUND(404, "TAG_NOT_FOUND", "태그를 찾을 수 없습니다."),

INTERNAL_PROCESSING_ERROR(500, "INTERNAL_PROCESSING_ERROR", "내부 시스템 에러가 발생했습니다.");

private final int status;
private final String code;
Expand Down
6 changes: 6 additions & 0 deletions core-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ dependencies {
runtimeOnly(project(":storage"))
runtimeOnly(project(":crawler"))

runtimeOnly("io.jsonwebtoken:jjwt-impl:$jsonWebTokenVersion")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jsonWebTokenVersion")

implementation(project(":domain"))
implementation(project(":common:error"))
implementation(project(":common:enums"))
Expand All @@ -10,7 +13,10 @@ dependencies {
implementation(project(":modules:monitoring"))
implementation(project(":modules:local-cache"))

implementation("io.jsonwebtoken:jjwt-api:$jjwtApiVersion")
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.openApiVersion}"

testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.imlinker.coreapi.configuration;

import com.imlinker.coreapi.core.auth.jwt.JwtTokenProperties;
import com.imlinker.coreapi.core.auth.jwt.TokenProperties;
import com.imlinker.coreapi.core.auth.oauth2.*;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableConfigurationProperties({JwtTokenProperties.class, TokenProperties.class})
public class SecurityConfiguration {
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
private final CustomAuthorizationRequestResolver customOAuth2AuthorizationRequestResolver;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable);

http.oauth2Login(
loginHandler ->
loginHandler
.successHandler(customAuthenticationSuccessHandler)
.userInfoEndpoint(
userInfoEndpoint -> userInfoEndpoint.userService(customOAuth2UserService))
.authorizationEndpoint(
authorizationEndpoint ->
authorizationEndpoint.authorizationRequestResolver(
customOAuth2AuthorizationRequestResolver)));

http.exceptionHandling(
exceptionHandling ->
exceptionHandling
.accessDeniedHandler(customAccessDeniedHandler)
.authenticationEntryPoint(customAuthenticationEntryPoint));

http.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().permitAll());

return http.build();
}

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) ->
web.ignoring()
.requestMatchers(
"/v3/api-docs/**",
"/swagger-resources/**",
"/swagger-ui/**",
"/webjars/**",
"/swagger/**",
"/favicon.ico");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.imlinker.coreapi.core.auth.jwt;

import com.imlinker.coreapi.support.exception.FilterExceptionHandler;
import com.imlinker.error.ErrorType;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.AllArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

@AllArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {

private final JwtTokenProvider jwtTokenProvider;
private final FilterExceptionHandler filterExceptionHandler;

@Override
public void doFilter(
ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
try {
String token = ((HttpServletRequest) servletRequest).getHeader("Authorization");
if (token != null) {
Claims claims =
jwtTokenProvider.parseClaims(token.replace("Bearer ", ""), TokenType.ACCESS_TOKEN);
if (claims != null) {
SecurityContextHolder.getContext()
.setAuthentication(jwtTokenProvider.generateAuthentication(claims));
}
}

filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception e) {
filterExceptionHandler.sendErrorMessage(
(HttpServletResponse) servletResponse, ErrorType.INTERNAL_PROCESSING_ERROR, e.getCause());
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.imlinker.coreapi.core.auth.jwt;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "jwt")
public class JwtTokenProperties {
private final TokenProperties access;
private final TokenProperties refresh;

@Getter
@AllArgsConstructor
public static class TokenProperties {
private final Long expire;
private final String secret;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.imlinker.coreapi.core.auth.jwt;

import com.imlinker.coreapi.support.exception.FilterExceptionHandler;
import com.imlinker.domain.common.Email;
import com.imlinker.error.ApplicationException;
import com.imlinker.error.ErrorType;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.io.DecodingException;
import io.jsonwebtoken.security.Keys;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import javax.crypto.SecretKey;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

private final JwtTokenProperties jwtTokenProperties;
private final FilterExceptionHandler filterExceptionHandler;

public String generateToken(Email email, TokenType tokenType) {
JwtTokenProperties.TokenProperties properties =
tokenType == TokenType.ACCESS_TOKEN
? jwtTokenProperties.getAccess()
: jwtTokenProperties.getRefresh();

Instant now = Instant.now();
Instant expire = now.plus(properties.getExpire(), ChronoUnit.MILLIS);

return Jwts.builder()
.claim("email", email.getValue())
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(expire))
.signWith(Keys.hmacShaKeyFor(properties.getSecret().getBytes()))
.compact();
}

public Claims parseClaims(String token, TokenType tokenType) {
try {
SecretKey key =
Keys.hmacShaKeyFor(
(tokenType == TokenType.ACCESS_TOKEN
? jwtTokenProperties.getAccess()
: jwtTokenProperties.getRefresh())
.getSecret()
.getBytes());

return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();

} catch (SecurityException e) {
throw new ApplicationException(ErrorType.UNAUTHORIZED, "유효하지 않은 토큰입니다.", e.getCause());
} catch (ExpiredJwtException e) {
throw new ApplicationException(ErrorType.UNAUTHORIZED, "만료된 토큰입니다.", e.getCause());
} catch (DecodingException e) {
throw new ApplicationException(ErrorType.UNAUTHORIZED, "잘못된 인증입니다.", e.getCause());
} catch (MalformedJwtException e) {
throw new ApplicationException(ErrorType.UNAUTHORIZED, "손상된 토큰입니다.", e.getCause());
}
}

public Authentication generateAuthentication(Claims claims) {
Email email = Email.of(claims.get("email").toString());
return new UsernamePasswordAuthenticationToken(email, null, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.imlinker.coreapi.core.auth.jwt;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "token")
public class TokenProperties {
private final Long expire;
private final String secret;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.imlinker.coreapi.core.auth.jwt;

public enum TokenType {
ACCESS_TOKEN,
REFRESH_TOKEN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.imlinker.coreapi.core.auth.oauth2;

import com.imlinker.coreapi.support.exception.FilterExceptionHandler;
import com.imlinker.error.ErrorType;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

private final FilterExceptionHandler filterExceptionHandler;

@Override
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException {
filterExceptionHandler.sendErrorMessage(
response, ErrorType.UNAUTHORIZED, accessDeniedException);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.imlinker.coreapi.core.auth.oauth2;

import com.imlinker.coreapi.support.exception.FilterExceptionHandler;
import com.imlinker.error.ErrorType;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final FilterExceptionHandler filterExceptionHandler;

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException {
filterExceptionHandler.sendErrorMessage(response, ErrorType.UNAUTHENTICATED, authException);
}
}
Loading

0 comments on commit 8ca7c6e

Please sign in to comment.