Skip to content

Commit

Permalink
Merge pull request #12 from PawWithU/feat/11-Auth
Browse files Browse the repository at this point in the history
[Feature] JWT, Redis 기본 설정
  • Loading branch information
hojeong2747 authored Oct 26, 2023
2 parents 58ac813 + eef583d commit 433fd98
Show file tree
Hide file tree
Showing 19 changed files with 742 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .github/workflows/dev-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
# application-jwt.yml 파일 생성
- name: make application-jwt.yml
run: |
cd ./src/main/resources
touch ./application-jwt.yml
echo "${{ secrets.APPLICATION_JWT_YML }}" > ./application-jwt.yml
shell: bash

# application-dev.yml 파일 생성
- name: make application-dev.yml
run: |
Expand Down
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// Spring Data Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// jwt
implementation 'com.auth0:java-jwt:4.2.1'
// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
// Spring Web
Expand All @@ -40,6 +44,8 @@ dependencies {
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

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

@EnableJpaAuditing
@EnableCaching
@SpringBootApplication
public class ConnectdogApplication {

Expand Down
81 changes: 81 additions & 0 deletions src/main/java/com/pawwithu/connectdog/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.pawwithu.connectdog.config;


import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.*;

import java.time.Duration;

@Slf4j
@Configuration
@EnableRedisRepositories
public class RedisConfig extends CachingConfigurerSupport {
@Value("${spring.data.redis.port}")
private int port;

@Value("${spring.data.redis.host}")
private String host;

@PostConstruct // 해당 메서드는 객체의 모든 의존성이 주입된 직후에 자동으로 호출
public void printValues() {
log.info("Redis Host: " + host);
log.info("Redis Port: " + port);
}


@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}

@Bean(name = "redisTemplate")
public RedisTemplate<Long, String> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Long, String> redisTemplate = new RedisTemplate<>();

redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new GenericToStringSerializer<Long>(Long.class)); // Long 타입에 대한 직렬화 도구로 GenericToStringSerializer 사용
redisTemplate.setValueSerializer(new StringRedisSerializer());

return redisTemplate;
}

@Bean(name = "redisBlackListTemplate")
public RedisTemplate<String, String> redisBlackListTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();

redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());

return redisTemplate;
}

// Redis Cache 적용을 위한 RedisCacheManager 설정
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext
.SerializationPair.fromSerializer(new StringRedisSerializer())) // StringRedisSerializer: binary 데이터로 저장되기 때문에 이를 String 으로 변환
.serializeValuesWith(RedisSerializationContext
.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofDays(14).plusHours(2)); // TTL 2주 + 2시간으로 설정

return RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}

}
59 changes: 59 additions & 0 deletions src/main/java/com/pawwithu/connectdog/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.pawwithu.connectdog.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import static org.springframework.security.config.Customizer.withDefaults;

@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {


@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
http
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
.csrf(csrf -> csrf.disable())
.cors(withDefaults())
.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(request ->
request.requestMatchers(mvcMatcherBuilder.pattern("/login")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/api/sign-up")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/h2-console/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/css/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/js/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/images/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/error")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/favicon.ico")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/swagger-ui/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/swagger-resources/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/v3/api-docs/**")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/api/sign-up/email")).permitAll()
.requestMatchers(mvcMatcherBuilder.pattern("/api/members/userName/isDuplicated")).permitAll()
.anyRequest().authenticated());

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.pawwithu.connectdog.domain.auth.controller;

import com.pawwithu.connectdog.domain.auth.dto.request.SignUpRequest;
import com.pawwithu.connectdog.domain.auth.service.AuthService;
import com.pawwithu.connectdog.error.dto.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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;

@Tag(name = "Sign-Up", description = "Sign-Up API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/sign-up")
public class SignUpController {

private final AuthService authService;

@Operation(summary = "자체 회원가입", description = "이메일을 사용해 회원가입을 합니다.",
responses = {@ApiResponse(responseCode = "204", description = "자체 회원가입 성공")
, @ApiResponse(responseCode = "400"
, description = "1. 이미 존재하는 이메일입니다. \t\n 2. 이미 존재하는 사용자 닉네임입니다."
, content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
})
@PostMapping
public ResponseEntity<Void> signUp(@RequestBody SignUpRequest signUpRequest) {
authService.signUp(signUpRequest);
return ResponseEntity.noContent().build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.pawwithu.connectdog.domain.auth.dto.request;

import com.pawwithu.connectdog.domain.member.entity.Member;
import com.pawwithu.connectdog.domain.member.entity.Role;

public record SignUpRequest(
String email, String password, String nickname,
String name, String phone,
String url,
Boolean isOptionAgr,
Role role
) {
public Member toEntity() {
return Member.builder()
.email(email)
.password(password)
.nickname(nickname)
.name(name)
.phone(phone)
.url(url)
.isOptionAgr(isOptionAgr)
.role(role)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.pawwithu.connectdog.domain.auth.service;

import com.pawwithu.connectdog.domain.auth.dto.request.SignUpRequest;
import com.pawwithu.connectdog.domain.member.entity.Member;
import com.pawwithu.connectdog.domain.member.repository.MemberRepository;
import com.pawwithu.connectdog.error.exception.custom.BadRequestException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import static com.pawwithu.connectdog.error.ErrorCode.ALREADY_EXIST_EMAIL;
import static com.pawwithu.connectdog.error.ErrorCode.ALREADY_EXIST_NICKNAME;

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class AuthService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;

public void signUp(SignUpRequest signUpRequest) {

if (memberRepository.existsByEmail(signUpRequest.email())) {
throw new BadRequestException(ALREADY_EXIST_EMAIL);
}
if (memberRepository.existsByNickname(signUpRequest.nickname())) {
throw new BadRequestException(ALREADY_EXIST_NICKNAME);
}

Member member = signUpRequest.toEntity();
member.passwordEncode(passwordEncoder);
memberRepository.save(member);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.pawwithu.connectdog.domain.member.entity;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.crypto.password.PasswordEncoder;

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

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

private String email; // 이메일
private String password; // 비밀번호
private String nickname; // 닉네임
private String name; // 이름
private String phone; // 이동봉사자 휴대폰 번호
private String url; // 이동봉사 단체 링크
private Boolean isOptionAgr; // 선택 이용약관 체크 여부

@Enumerated(EnumType.STRING)
private Role role;

@Enumerated(EnumType.STRING)
private SocialType socialType; // KAKAO, NAVER

private String socialId; // 로그인한 소셜 타입 식별자 값 (일반 로그인의 경우 null)

@Builder
public Member(String email, String password, String nickname, String name, String phone, String url, Boolean isOptionAgr, Role role, SocialType socialType, String socialId) {
this.email = email;
this.password = password;
this.nickname = nickname;
this.name = name;
this.phone = phone;
this.url = url;
this.isOptionAgr = isOptionAgr;
this.role = role;
this.socialType = socialType;
this.socialId = socialId;
}

public void passwordEncode(PasswordEncoder passwordEncoder) {
this.password = passwordEncoder.encode(this.password);
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.pawwithu.connectdog.domain.member.entity;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Role {

// 첫 로그인, 테스트 여부 구분
GUEST("ROLE_GUEST"), USER("ROLE_USER"), ADMIN("ROLE_ADMIN");

private final String key;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.pawwithu.connectdog.domain.member.entity;

public enum SocialType {
KAKAO, NAVER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.pawwithu.connectdog.domain.member.repository;

import com.pawwithu.connectdog.domain.member.entity.Member;
import com.pawwithu.connectdog.domain.member.entity.SocialType;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByEmail(String email);
Optional<Member> findByNickname(String nickname);

// 소셜 타입과 소셜의 식별값으로 회원 찾는 메소드 -> 추가 정보를 입력받아 회원가입 진행 시 이용
Optional<Member> findBySocialTypeAndSocialId(SocialType socialType, String socialId);

Boolean existsByEmail(String email);
Boolean existsByNickname(String nickname);

}
Loading

0 comments on commit 433fd98

Please sign in to comment.