Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 유저 관련 로직 추가 #16

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
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
109 changes: 109 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.1'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.asciidoctor.jvm.convert' version "3.3.2"
}

group = 'com.fondant'
version = '0.0.1-SNAPSHOT'

configurations {
asciidoctorExtensions
}


java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

//SPRING_SECURITY
implementation "org.springframework.boot:spring-boot-starter-security"

//SPRING_REST_DOCS
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

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

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

//POSTGRESQL
runtimeOnly 'org.postgresql:postgresql'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

//LOMBOK
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

//QUERY_DSL
implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta"
implementation "com.querydsl:querydsl-core:5.0.0"
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"

testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}


def querydslDir = "build/generated/querydsl"

sourceSets {
main.java.srcDirs += [querydslDir]
}

tasks.withType(JavaCompile) {
options.getGeneratedSourceOutputDirectory().set(file(querydslDir))
}

clean.doLast {
file(querydslDir).deleteDir()
}


ext {
snippetsDir = file('build/generated-snippets')
}

test {
outputs.dir snippetsDir
}

asciidoctor {
inputs.dir snippetsDir
dependsOn test
configurations 'asciidoctorExtensions'
}

bootJar {
dependsOn asciidoctor
copy {
from("${asciidoctor.outputDir}")
into 'src/main/resources/static/docs'
}
}

tasks.named('test') {
useJUnitPlatform()
}
16 changes: 16 additions & 0 deletions src/main/java/com/fondant/global/config/CorsMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.fondant.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {


@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000");
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/fondant/global/config/QueryDslConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.fondant.global.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/fondant/global/config/SchedulingConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.fondant.global.config;


import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class SchedulingConfig {
}
90 changes: 90 additions & 0 deletions src/main/java/com/fondant/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.fondant.global.config;

import com.fondant.infra.jwt.filter.CustomLogoutFilter;
import com.fondant.infra.jwt.filter.JWTFilter;
import com.fondant.infra.jwt.application.JWTUtil;
import com.fondant.infra.jwt.filter.LoginFilter;

import com.fondant.infra.jwt.domain.repository.RefreshRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.cors.CorsConfiguration;
import java.util.Collections;

@EnableWebSecurity
@Configuration
public class SecurityConfig {

private final AuthenticationConfiguration authenticationConfiguration;
private final JWTUtil jwtUtil;
private final RefreshRepository refreshRepository;


public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, RefreshRepository refreshRepository) {
this.authenticationConfiguration = authenticationConfiguration;
this.jwtUtil = jwtUtil;
this.refreshRepository = refreshRepository;
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

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

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

http
.cors((cors) -> cors
.configurationSource(request -> {
CorsConfiguration configration = new CorsConfiguration();
configration.setAllowedOrigins(Collections.singletonList("http://localhost:3000"));
configration.setAllowedMethods(Collections.singletonList("*"));
configration.setAllowCredentials(true);
configration.setAllowedHeaders(Collections.singletonList("*"));
configration.setMaxAge(3600L);
configration.setExposedHeaders(Collections.singletonList("Authorization"));
return configration;
}));

http
.formLogin(AbstractHttpConfigurer::disable);

http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/api/user/join", "/api/user/login", "/api/user/reissue").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated());

http
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);

http
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository), UsernamePasswordAuthenticationFilter.class);

http
.addFilterAt(new CustomLogoutFilter(jwtUtil, refreshRepository), LogoutFilter.class);

http
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

return http.build();
}
}
59 changes: 59 additions & 0 deletions src/main/java/com/fondant/infra/jwt/application/JWTUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.fondant.infra.jwt.application;

import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;

@Component
public class JWTUtil {

private final SecretKey secretKey;

public JWTUtil(@Value("${spring.jwt.secret}")String secret) {
this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
}

public String getUserIdFromToken(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("userId").toString();
}

public String getUserRoleFromToken(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role").toString();
}

public boolean isTokenExpired(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration()
.before(new Date(System.currentTimeMillis()));
}

public String getType(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("type").toString();
}

public LocalDateTime getExpiration(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration()
.toInstant().atZone(ZoneId.of("Asia/Seoul")).toLocalDateTime();
}

public String generateToken(String type,String userId, String role, Long expiredMs) {
return Jwts.builder()
.claim("type",type)
.claim("userId", userId)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.fondant.infra.jwt.application;

import com.fondant.infra.jwt.domain.repository.RefreshRepository;
import jakarta.transaction.Transactional;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;

@Service
@Transactional
public class SchedulerService {
private final RefreshRepository refreshRepository;

public SchedulerService(RefreshRepository refreshRepository) {
this.refreshRepository = refreshRepository;
}

@Scheduled(fixedRate = 60 * 60 * 24 * 1000)
public void deleteAllExpires() {
LocalDateTime now = LocalDateTime.now();
refreshRepository.deleteByExpiresBefore(now);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.fondant.infra.jwt.domain.entity;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name="refresh")
public class RefreshEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String userId;
private String refresh;
@Column(name = "expires", columnDefinition = "TIMESTAMP WITHOUT TIME ZONE")
private LocalDateTime expires;

@Builder
public RefreshEntity(String userId, String refresh, LocalDateTime expires) {
this.userId = userId;
this.refresh = refresh;
this.expires = expires;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.fondant.infra.jwt.domain.repository;

import java.time.LocalDateTime;

public interface JwtRepositoryCustom {
void deleteByExpiresBefore(LocalDateTime now);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.fondant.infra.jwt.domain.repository;

import com.fondant.infra.jwt.domain.entity.QRefreshEntity;
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;
import java.time.LocalDateTime;

@RequiredArgsConstructor
public class JwtRepositoryImpl implements JwtRepositoryCustom {

private final JPAQueryFactory queryFactory;

public void deleteByExpiresBefore(LocalDateTime now) {
QRefreshEntity refresh = QRefreshEntity.refreshEntity;

try {
queryFactory.delete(refresh)
.where(refresh.expires.before(now))
.execute();
} catch (Exception e) {
throw new RuntimeException("만료된 리프레쉬 토큰을 제거하는데 실패했습니다.");
}
}
}
Loading