Skip to content
Merged
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
38 changes: 38 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created
# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle

name: Gradle Package

on:
pull_request:
branches:
- "master"
- "develop"

jobs:
build:

runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: checkout
uses: actions/checkout@v3

- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'


- name: build and test
run: |
chmod +x gradlew
./gradlew build
12 changes: 12 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

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

//db-h2
implementation 'com.h2database:h2'
testImplementation 'com.h2database:h2'
Expand All @@ -44,6 +48,14 @@ dependencies {
//s3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

// 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'

// validator
implementation 'commons-validator:commons-validator:1.7'

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
package com.moplus.moplus_server;

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.servers.Server;
import java.util.Arrays;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@OpenAPIDefinition(servers = {@Server(url = "https://dev.mopl.kr", description = "Default Server URL")})
@SpringBootApplication
@EnableJpaAuditing
public class MoplusServerApplication {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.moplus.moplus_server.domain.auth.controller;

import com.moplus.moplus_server.domain.auth.dto.request.AdminLoginRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.RequiredArgsConstructor;
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;

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

@Operation(summary = "어드민 로그인", description = "아아디 패스워드 로그인 후 토큰 발급합니다.")
@ApiResponses(value = {
@ApiResponse(
responseCode = "200",
description = "로그인 성공",
headers = {
@Header(name = "Authorization", description = "Access Token", schema = @Schema(type = "string")),
@Header(name = "RefreshToken", description = "Refresh Token", schema = @Schema(type = "string"))
}
),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "401", description = "인증 실패")
})
@PostMapping("/admin/login")
public void adminLogin(
@RequestBody AdminLoginRequest request
) {
// 실제 처리는 Security 필터에서 이루어지며, 이 메서드는 Swagger 명세용입니다.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.moplus.moplus_server.domain.auth.dto.request;

public record AdminLoginRequest(
String email,
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.moplus.moplus_server.domain.member.domain;

import com.moplus.moplus_server.global.common.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;

private String email;
private String password;

@Enumerated(EnumType.STRING)
private MemberRole role;

@Builder
public Member(String nickname, String email, String password, MemberRole role) {
this.email = email;
this.password = password;
this.role = role;
}

public boolean isMatchingPassword(String password) {
return this.password.equals(password);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.moplus.moplus_server.domain.member.domain;

import java.util.Arrays;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum MemberRole {
USER("ROLE_USER"),
ADMIN("ROLE_ADMIN");

private final String value;

public static MemberRole findByKey(String value) {
return Arrays.stream(MemberRole.values())
.filter(role -> role.value.equals(value))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No role with key: " + value));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.moplus.moplus_server.domain.member.repository;

import com.moplus.moplus_server.domain.member.domain.Member;
import com.moplus.moplus_server.global.error.exception.ErrorCode;
import com.moplus.moplus_server.global.error.exception.NotFoundException;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);

default Member findByEmailOrThrow(String email) {
return findByEmail(email).orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.moplus.moplus_server.domain.member.service;

import com.moplus.moplus_server.domain.member.domain.Member;
import com.moplus.moplus_server.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;

@Transactional(readOnly = true)
public Member getMemberByEmail(String email) {
return memberRepository.findByEmailOrThrow(email);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.moplus.moplus_server.global.config.properties;

import com.moplus.moplus_server.global.properties.jwt.JwtProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@EnableConfigurationProperties({
JwtProperties.class
})
@Configuration
public class PropertiesConfig {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.moplus.moplus_server.global.config.security;

import com.moplus.moplus_server.domain.member.service.MemberService;
import com.moplus.moplus_server.global.security.filter.EmailPasswordAuthenticationFilter;
import com.moplus.moplus_server.global.security.handler.EmailPasswordSuccessHandler;
import com.moplus.moplus_server.global.security.provider.EmailPasswordAuthenticationProvider;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
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.configuration.WebSecurityCustomizer;
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.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final MemberService memberService;
private final EmailPasswordSuccessHandler emailPasswordSuccessHandler;
private final AuthenticationConfiguration authenticationConfiguration;

private String[] allowUrls = {"/", "/favicon.ico", "/swagger-ui/**", "/v3/**"};

@Value("${cors-allowed-origins}")
private List<String> corsAllowedOrigins;

@Bean
public WebSecurityCustomizer configure() {
// filter 안타게 무시
return (web) -> web.ignoring().requestMatchers(allowUrls);
}


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.cors(customizer -> customizer.configurationSource(corsConfigurationSource()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(request -> request
.requestMatchers(allowUrls).permitAll()
.anyRequest().authenticated());

http
.exceptionHandling(exception ->
exception.authenticationEntryPoint((request, response, authException) ->
response.setStatus(HttpStatus.UNAUTHORIZED.value()))); // 인증,인가가 되지 않은 요청 시 발생시

http
.addFilterAt(emailPasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
// .addFilterBefore(new JwtExceptionFilter(), JwtAuthenticationFilter.class);

return http.build();
}

@Bean
public EmailPasswordAuthenticationFilter emailPasswordAuthenticationFilter() throws Exception {
EmailPasswordAuthenticationFilter emailPasswordAuthenticationFilter = new EmailPasswordAuthenticationFilter(
authenticationManager(authenticationConfiguration));
emailPasswordAuthenticationFilter.setFilterProcessesUrl("/api/v1/auth/admin/login");
emailPasswordAuthenticationFilter.setAuthenticationSuccessHandler(emailPasswordSuccessHandler);
emailPasswordAuthenticationFilter.afterPropertiesSet();
return emailPasswordAuthenticationFilter;
}

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
ProviderManager providerManager = (ProviderManager) authenticationConfiguration.getAuthenticationManager();
providerManager.getProviders().add(emailPasswordAuthenticationProvider());
return configuration.getAuthenticationManager();
}

@Bean
public EmailPasswordAuthenticationProvider emailPasswordAuthenticationProvider() {
return new EmailPasswordAuthenticationProvider(memberService);
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(corsAllowedOrigins);
configuration.addAllowedMethod("*");
configuration.setAllowedHeaders(List.of("*")); // 허용할 헤더
configuration.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration); // 모든 경로에 적용
return source;
}

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@

@Getter
public enum ErrorCode {

//공통
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류, 관리자에게 문의하세요"),
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "잘못된 입력 값입니다"),
BAD_CREDENTIALS(HttpStatus.UNAUTHORIZED, "잘못된 인증 정보입니다"),

//모의고사
PRACTICE_TEST_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 모의고사를 찾을 수 없습니다"),
Expand All @@ -23,6 +27,9 @@ public enum ErrorCode {
//이미지
IMAGE_FILE_EXTENSION_NOT_FOUND(HttpStatus.NOT_FOUND, "지원하지 않는 이미지 확장자입니다"),
IMAGE_FILE_NOT_FOUND_IN_S3(HttpStatus.NOT_FOUND, "S3에 해당 이미지 파일을 찾을 수 없습니다"),

//회원
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원을 찾을 수 없습니다"),
;


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.moplus.moplus_server.global.properties.jwt;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "jwt")
public record JwtProperties(
String accessTokenSecret,
String refreshTokenSecret,
Long accessTokenExpirationTime,
Long refreshTokenExpirationTime,
String issuer
) {

public Long accessTokenExpirationMilliTime() {
return accessTokenExpirationTime * 1000;
}

public Long refreshTokenExpirationMilliTime() {
return refreshTokenExpirationTime * 1000;
}
}
Loading
Loading