Skip to content

Commit

Permalink
feat: application.yml profile 별 분리 및 인증 로직 기본 구현, JWT 관련 로직 구현 (#30)
Browse files Browse the repository at this point in the history
* feat: spring security 관련 의존성 추가

* feat: spring oauth 2.0 관련 의존성 추가

* refactor: User 엔티티 관련 리팩토링 및 관련 클래스 리팩토링

* feat: UserAuthentication (인증 객체) 구현

* feat: SecurityUtil 구현

* feat: redis 의존성 추가

* feat: profile 분리 ( 개발, 운영, 테스트, 인증, 데이터소스, 레디스 )

* feat: io-netty-dns-native-macos 관련 의존성 추가

* feat: JWT 관련 설정값 & Redis 관련 설정값 주입 구현

* feat: application-redis.yml 구현

* feat: jwt 의존성 추가 (#17)

* feat: jwtAuthenticationFilter 구현 및 관련 Service 구현 (#17)

* feat: 응답을 위한 TokenResponse 구현 및 기타 구현 (#17)

* fix: IntegrationTest 수정 (#17)

* refactor: JwtAuthenticationFilter 불필요한 주석 제거 (#17)

* fix: ci 에러 수정 (#17)

* fix: ci 에러 수정 (#17)

* fix: ci 에러 수정 (#17)

* fix: PropertyTest 제거 (#17)

* fix: ci 에러 수정 (#17)

* fix: ci 에러 수정 (#17)

* fix: ci 에러 수정 (#17)

* fix: ci 에러 수정 (#17)

* fix: ci 에러 수정 (#17)

* fix: ci 에러 수정 (#17)

* feat: securityConfig JwtAuthenticationFilter 적용 (#17)

* fix: ci 에러 수정 (#17)

* fix: ci 에러 수정 (#17)
  • Loading branch information
choidongkuen authored Jan 6, 2024
1 parent 09acdf6 commit 836166d
Show file tree
Hide file tree
Showing 47 changed files with 896 additions and 269 deletions.
12 changes: 12 additions & 0 deletions gradle/spring.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,24 @@ allprojects {
dependencies {
implementation "org.springframework.boot:spring-boot-starter"
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-configuration-processor'

runtimeOnly 'io.netty:netty-resolver-dns-native-macos:4.1.104.Final:osx-aarch_64'
implementation "org.springframework.boot:spring-boot-starter-webflux"

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

implementation "org.springframework.boot:spring-boot-starter-actuator"

implementation "org.springframework.boot:spring-boot-starter-security"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
implementation 'io.jsonwebtoken:jjwt:0.9.1'


runtimeOnly 'io.micrometer:micrometer-registry-prometheus'

testImplementation "org.springframework.boot:spring-boot-starter-test"
testImplementation 'org.springframework.security:spring-security-test'
}
}
4 changes: 1 addition & 3 deletions src/main/java/net/teumteum/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/net/teumteum/core/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.teumteum.core.config;

import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
@ConfigurationPropertiesScan("net.teumteum.core.property")
public class AppConfig {
}
4 changes: 2 additions & 2 deletions src/main/java/net/teumteum/core/context/LoginContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

public interface LoginContext {

void setUserId(Long userId);

Long getUserId();

void setUserId(Long userId);

}
11 changes: 5 additions & 6 deletions src/main/java/net/teumteum/core/context/LoginContextImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@ public class LoginContextImpl implements LoginContext {

private Long userId;

@Override
public void setUserId(Long userId) {
this.userId = userId;
}

@Override
public Long getUserId() {
return userId;
}

}
@Override
public void setUserId(Long userId) {
this.userId = userId;
}
}
38 changes: 16 additions & 22 deletions src/main/java/net/teumteum/core/entity/TimeBaseEntity.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,29 @@
package net.teumteum.core.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import java.time.Instant;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.Instant;

@Getter
@SuperBuilder
@NoArgsConstructor
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeBaseEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;

@Column(name = "created_at", columnDefinition = "TIMESTAMP(6)", nullable = false, updatable = false)
protected Instant createdAt;

@Column(name = "updated_at", columnDefinition = "TIMESTAMP(6)", nullable = false)
protected Instant updatedAt;

@PrePersist
void prePersist() {
var now = Instant.now();

createdAt = createdAt != null ? createdAt : now;
updatedAt = updatedAt != null ? updatedAt : now;
}

@PreUpdate
void preUpdate() {
updatedAt = updatedAt != null ? updatedAt : Instant.now();
}

@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
}

32 changes: 32 additions & 0 deletions src/main/java/net/teumteum/core/property/JwtProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package net.teumteum.core.property;

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

@Getter
@Setter
@ConfigurationProperties(prefix = "jwt")
public class JwtProperty {

private String bearer;
private String secret;
private Access access;
private Refresh refresh;


@Getter
@Setter
public static class Access{
private long expiration;
private String header;

}

@Getter
@Setter
public static class Refresh {
private long expiration;
private String header;
}
}
13 changes: 13 additions & 0 deletions src/main/java/net/teumteum/core/property/RedisProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package net.teumteum.core.property;

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

@Getter
@Setter
@ConfigurationProperties(prefix = "data.redis")
public class RedisProperty {
private String host;
private int port;
}
6 changes: 6 additions & 0 deletions src/main/java/net/teumteum/core/security/Authenticated.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package net.teumteum.core.security;

/* 소셜 OAuth 로그인 타입 */
public enum Authenticated {
카카오,네이버;
}
60 changes: 60 additions & 0 deletions src/main/java/net/teumteum/core/security/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package net.teumteum.core.security;


import lombok.RequiredArgsConstructor;
import net.teumteum.core.security.filter.JwtAuthenticationFilter;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(request
-> request.requestMatchers("/users").permitAll()
.requestMatchers(PathRequest.toH2Console()).permitAll()
.anyRequest().authenticated())
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement(sessionManagement
-> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.cors(cors -> cors.configurationSource(this.corsConfigurationSource()));

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*");
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
configuration.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
48 changes: 48 additions & 0 deletions src/main/java/net/teumteum/core/security/UserAuthentication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package net.teumteum.core.security;

import lombok.Getter;
import net.teumteum.user.domain.User;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.ArrayList;
import java.util.List;

@Getter
public class UserAuthentication extends AbstractAuthenticationToken {

private final String oauthId;
private Long id;

public UserAuthentication(User user) {
super(authorities(user));
this.id = user.getId();
this.oauthId = user.getOauth().getOauthId();
}

private static List<GrantedAuthority> authorities(User User) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(User.getRoleType().name()));
return authorities;
}

@Override
public Object getCredentials() {
return null;
}

@Override
public Object getPrincipal() {
return id;
}

@Override
public boolean isAuthenticated() {
return true;
}

public void setUserId(Long userId) {
id = userId;
}
}
18 changes: 18 additions & 0 deletions src/main/java/net/teumteum/core/security/dto/TokenResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package net.teumteum.core.security.dto;

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

@Getter
@NoArgsConstructor
public class TokenResponse {
private String accessToken;
private String refreshToken;

@Builder
public TokenResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package net.teumteum.core.security.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.teumteum.core.property.JwtProperty;
import net.teumteum.core.security.UserAuthentication;
import net.teumteum.core.security.service.AuthService;
import net.teumteum.core.security.service.JwtService;
import net.teumteum.user.domain.User;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final AuthService authService;
private final JwtProperty jwtProperty;

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
/* Cors Preflight Request */
if (request.getMethod().equals("OPTIONS")) {
return;
}

try {
String token = this.resolveTokenFromRequest(request);
if (checkTokenExistenceAndValidation(token)) {
User user = this.authService.findUserByToken(token).get();
UserAuthentication authentication = new UserAuthentication(user);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (InsufficientAuthenticationException e) {
log.info("JwtAuthentication UnauthorizedUserException!");
}
filterChain.doFilter(request, response);
}

private boolean checkTokenExistenceAndValidation(String token) {
return StringUtils.hasText(token) && this.jwtService.validateToken(token);
}

private String resolveTokenFromRequest(HttpServletRequest request) {
String token = request.getHeader(jwtProperty.getAccess().getHeader());
if (!ObjectUtils.isEmpty(token) && token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) {
return token.substring(jwtProperty.getBearer().length()).trim();
}
return null;
}
}
Loading

0 comments on commit 836166d

Please sign in to comment.