[Tistory] Springboot3 + Swagger + Jwt (3)

원글 페이지 : 바로가기

Swagger 초기 셋팅 2024.06.20 – [BE/Java] – Springboot3 + Swagger + Jwt (2) Springboot3 + Swagger + Jwt (2) 프로젝트 진행 이유 및 개발 환경2024.06.20 – [BE/Java] – Springboot3 + Swagger + Jwt (1) Swagger란?REST API 개발을 진행하는경우 Restful한 서비스를 만들때 @RestController를 읽어서 API 문서를 자동으로 생성해 tistory.slowtuttle.co.kr 진행할 내용 1. JPA 셋팅 2. SpringSecurity 셋팅 3. DB 셋팅 4. 사용자 Entity 생성 5. 단순 회원가입 테스트 (swagger, security, db, jpa 이상이 없는지 테스트) JPA 설정값 셋팅 application.yml ##############################################
### jpa
##############################################
spring:
jpa:
hibernate:
ddl-auto: update # DB 생성 방식(개발이라 update로 셋팅)
show-sql: true
properties:
hibernate:
format_sql: true P6Spy 설정 추가 일반적인 jpa 의 로깅 방식이 깔끔하지 않고, 파라미터를 직관적으로 확인할 수 없다는 점에서 추가하게됨 build.gradle 의존성 추가 // p6spy
implementation ‘com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0’ P6SpyFomatter.java (로그 포맷 설정 추가) package org.jjuni.swaggerjwt.config;

import com.p6spy.engine.logging.Category;
import com.p6spy.engine.spy.P6SpyOptions;
import com.p6spy.engine.spy.appender.MessageFormattingStrategy;
import jakarta.annotation.PostConstruct;
import org.hibernate.engine.jdbc.internal.FormatStyle;
import org.springframework.context.annotation.Configuration;

import java.util.Locale;

@Configuration
public class P6SpyFomatter implements MessageFormattingStrategy {
@PostConstruct
public void setLogMessageFormat() {
P6SpyOptions.getActiveInstance().setLogMessageFormat(this.getClass().getName());
}

@Override
public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) {
sql = formatSql(category, sql);
return String.format(“[%s] | %d ms | %s”, category, elapsed, formatSql(category, sql));
}

private String formatSql(String category, String sql) {
if (sql != null && !sql.trim().isEmpty() && Category.STATEMENT.getName().equals(category)) {
String trimmedSQL = sql.trim().toLowerCase(Locale.ROOT);
if (trimmedSQL.startsWith(“create”) || trimmedSQL.startsWith(“alter”) || trimmedSQL.startsWith(“comment”)) {
sql = FormatStyle.DDL.getFormatter().format(sql);
} else {
sql = FormatStyle.BASIC.getFormatter().format(sql);
}
return sql;
}
return sql;
}
} application.yml ##############################################
### logging
##############################################
logging:
level:
org.hibernate.type: TRACE # parameter 값 보기
com.p6spy: DEBUG # P6Spy 로깅 설정 SpringSecurity 적용 build.gradle – JWT 때 사용하려고 했었는데, 비밀번호 암호화에서 사용해야하기 때문에 주석 해제 implementation ‘org.springframework.boot:spring-boot-starter-security’ // security
testImplementation ‘org.springframework.security:spring-security-test’ // security SecurityConfig.java package org.jjuni.swaggerjwt.config;

import lombok.RequiredArgsConstructor;
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.web.SecurityFilterChain;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

@Bean
MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) {
return new MvcRequestMatcher.Builder(introspector);
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, MvcRequestMatcher.Builder mvc)
throws Exception {

// api 서버로 사용하기 때문에 csrf 해제 (jwt로 대체)
httpSecurity.csrf(config -> config.disable());

// 로그인 인증창이 뜨지 않게 비활성화
httpSecurity.httpBasic(config -> config.disable());

// form 로그인 해제
httpSecurity.formLogin(config -> config.disable());

// jSessionId 사용 거부
httpSecurity.sessionManagement(config -> config
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

// 인증, 권한 필터 설정
httpSecurity.authorizeHttpRequests(config -> config
.requestMatchers(
mvc.pattern(“/”),
mvc.pattern(“/api/**”),
mvc.pattern(“/swagger-ui/**”),
mvc.pattern(“/swagger.html”),
mvc.pattern(“/swagger-resource/**”)
).permitAll()
.anyRequest().authenticated());
return httpSecurity.getOrBuild();
}
} PasswordEncoderConfig.java package org.jjuni.swaggerjwt.common;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class PasswordEncoderConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 비밀번호는 외부로 유출되면 안되기 때문에 HASH 처리 이후 단방향 암호화
return new BCryptPasswordEncoder();
}
} DB 설정 application.yml spring:
##############################################
### h2
##############################################
h2:
console:
enabled: true
path: /console
##############################################
### db connection info
##############################################
datasource:
driver-class-name: org.h2.Driver
# url: jdbc:h2:/Users/test/~~폴더경로 # embeded (mac)
# url: jdbc:h2:C:\Users\~~폴더경로 # embeded (window)
url: jdbc:h2:mem:test # In-Memory
username: jjuni
password: 비밀번호 실제 DB랑 연동할 내용이 아니고 간단하게 진행하기 위해서 In-Memory로 작업 진행 사용자 정보 생성 Member.java – 이전에는 Secrity를 사용하지 않았었기 때문에 User 로 해도 무방했었지만, Security 자체적으로 UserDetails의 구현체인 User를 사용하기 때문에 헷갈리지 않도록 Member로 짓는게 편안! package org.jjuni.swaggerjwt.member.entity;

import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.data.annotation.CreatedDate;

import java.time.LocalDateTime;

@Entity
@Builder
@Table(name = “tb_user”)
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // jpa 자동생성
private Long id;

@Comment(“아이디”)
@Column(columnDefinition = “varchar(50)”, nullable = false)
private String userId;

@Comment(“비밀번호”)
@Column(nullable = false)
private String password;

@Comment(“이름”)
@Column(columnDefinition = “varchar(10)”, nullable = false)
private String name;

@Comment(“전화번호”)
@Column(columnDefinition = “varchar(14)”)
private String phoneNum;

@Comment(“이메일”)
@Column(columnDefinition = “varchar(50)”, nullable = false)
private String email;

@Comment(“생성일”)
@CreationTimestamp
@Column(updatable = false)
private LocalDateTime created;

@Comment(“수정일”)
@UpdateTimestamp
@Column(updatable = false)
private LocalDateTime updated;
} 기본적인 회원가입 AuthControler.java (controller) package org.jjuni.swaggerjwt.auth.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.xml.bind.ValidationException;
import lombok.AllArgsConstructor;
import org.jjuni.swaggerjwt.auth.dto.SignUpRequest;
import org.jjuni.swaggerjwt.auth.service.AuthService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@Tag(name = “Auth”, description = “회원가입, 로그인, 로그아웃 인증처리 API”)
@RestController
@RequestMapping(“/api/v1/auth”)
@AllArgsConstructor
public class AuthController {

private final AuthService authService;

/**
* 사용자 회원가입
*
* @param request
* @return
*/
@Operation(summary = “회원가입”, description = “신규 사용자 회원가입”)
@ApiResponses(value = {
@ApiResponse(responseCode = “200”,
description = “User Create Success”
)
})
@ResponseBody
@PostMapping(“sign-up”)
public ResponseEntity signUp(@Validated @RequestBody SignUpRequest request) throws ValidationException {
authService.signUp(request);
return new ResponseEntity<>(HttpStatus.OK);
}
} AuthService.java (service) package org.jjuni.swaggerjwt.auth.service;

import jakarta.transaction.Transactional;
import jakarta.xml.bind.ValidationException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jjuni.swaggerjwt.auth.dto.SignUpRequest;
import org.jjuni.swaggerjwt.member.entity.Member;
import org.jjuni.swaggerjwt.member.repository.MemberRepository;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@AllArgsConstructor
public class AuthService {

private final MemberRepository memberRepository;
private final PasswordEncoder encoder;

/**
* 사용자 회원가입
*
* @param req
*/
@Transactional
public void signUp(SignUpRequest req) throws ValidationException {
// id로 사용자 정보 조회
Member memberInfo = memberRepository.findByUserId(req.getUserId());
if (memberInfo != null) { // 이미 회원 가입을 진행 한 경우
throw new ValidationException(“이미 회원가입한 고객입니다. ” + memberInfo.getUserId());
}

Member newMember = req.toEntity();
newMember.setPassword(encoder.encode(req.getPassword()));
memberRepository.save(newMember);
}
} MemberRepository.java (repository) package org.jjuni.swaggerjwt.member.repository;

import org.jjuni.swaggerjwt.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository {
Member findByUserId(String userId);
} SignUpRequest.java (dto) package org.jjuni.swaggerjwt.auth.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.*;
import org.jjuni.swaggerjwt.member.entity.Member;

@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SignUpRequest {

@Size(min = 2, max = 50)
@Schema(description = “사용자ID”, defaultValue = “leejj9999”)
@NotBlank(message = “아이디를 입력해주세요”)
private String userId;

@Size(min = 9, max = 30)
@Schema(description = “비밀번호”, defaultValue = “test1234!@#$”)
@NotBlank(message = “비밀번호를 입력해주세요”)
private String password;

@Size(min = 2, max = 30)
@Schema(description = “이름”, defaultValue = “테스트”)
@NotBlank(message = “이름을 입력해주세요”)
private String name;

@Size(min = 13, max = 14)
@Schema(description = “연락처”, defaultValue = “010-1234-1234”)
@NotBlank(message = “연락처를 입력해주세요”)
private String phoneNum;

@Size(min = 11, max = 20)
@Schema(description = “이메일”, defaultValue = “test9999@naver.com”)
@NotBlank(message = “이메일을 입력해주세요”)
private String email;

public Member toEntity() {
return Member.builder()
.userId(userId)
.password(password)
.name(name)
.phoneNum(phoneNum)
.email(email)
.build();
}
} 회원가입 테스트 기본적인 테스트는 Swagger를 통하여 진행 요청 응답 DB에서 확인 – localhost:8080/console 접속 403…….? security 문제인 것 같은데… 일단 현재 글과 좀 다르고 너무 길어지다보니 해당 문제는 아래 글로 이동해서 확인! 2024.06.24 – [BE/Java] – Springboot3 + SpringSecurity + H2 403 Springboot3 + SpringSecurity + H2 403 문제SpringSecurity를 적용한 이후 이전까지 잘 접속되던 h2 콘솔에 접속이 안되고 403 에러가 나왔다.h2 콘솔 관련 filter 설정을 안해서 그런것 같다고 생각은 하였지만 간단하게 해결되지 않아서 해 tistory.slowtuttle.co.kr DB에 접속해서 확인해보면 아래와같이 잘 들어가있는 것을 확인할 수 있다. 느낀점… SpringSecurity를 적용하면서 Swagger가 접속이 안되서 많이 당황스러웠었다… 접속 url, api url만 풀어서 되는게 아니라 Swagger 내부적으로 사용하는 부분이 있어서 따로 해제를 해야되는 부분을 몰라서 많은 검색을 하며 찾아나갔다. 간단한 회원가입 로직을 만들면서 User Entity를 만들때 아래와 같은 생각을 했었다. 일반 사용자 입장에서 회원가입을 하는경우 아이디, 비밀번호, 연락처 등 을 입력하다보니 아이디가 PK(@ID)가 되야하지 않을까? 만약 그렇다면 사용자 ID를 직접할당해서 String 기본키로 사용하는게 더 유리하지 않을까? 사실 이 문제는 이전까지 MariaDB, PostgreSQL 등 현업에서는 자연스럽게 sequence를 pk로 생성해서 사용했었었고, 기본적으로 구축되어있는 상태에서 작업을 했었었기 때문에 크게 신경쓰지 않고 있었던 내용이었다. String보다 Long 을 기본키로 설정하여 연산하게되면 데이터베이스 관리와 성능에 유리하기 때문에 Long을 기본키로 사용한다는 것이었다. (추가적으로 놓친 부분이 있으면 댓글 부탁드립니다…) refs P6Spy 포맷 : https://velog.io/@cvcvcx9/p6spy%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%BF%BC%EB%A6%AC-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98-%EB%A1%9C%EA%B7%B8 p6spy를 이용한 쿼리 매개변수 로그 https://shanepark.tistory.com/415 위 글을 참조해서 설정했다. 기본적으로 DB쿼리를 볼 수 있게 설정할 때, 아래와 같은 설정을 이용한다. 하지만 위 글대로 설정하면 불편한 점이 있다. 매개변수 따로, velog.io Security Swagger 접속 이슈 : https://velog.io/@blaze241/Spring-Swagger-ui-Failed-to-load-remote-configuration [Spring] Swagger – ui Failed to load remote configuration. Swagger 로 자동 문서화 하는 방법을 다루던 도중 해당 문제를 마주하게 되었다.나는 분명 해당 문서 주소에 대한 접근권한을 풀었는데 말이다.그래서 콘솔을 열어보니 “spec-actions.js:19 undefined /api-d velog.io https://velog.io/@zinna_1109/Toy-Project-Swagger-%EB%8F%84%EC%9E%85-%EC%8B%9C-Spring-Security-%EC%84%A4%EC%A0%95 [Toy Project] Swagger 도입 시 Spring Security 설정 제품, 카테고리, 사용자에 대한 기본적인 CRUD API만 만들었는데도, API의 수가 많아졌다 이에 이를 간단하게 문서화하여, Frontend 화면을 만드는 친구에게 남기기 위해, 그리고 나 스스로를 위해 (시 velog.io userId를 pk로 잡지 않는 이유 : https://m.blog.naver.com/21thjojo/220636318112 회원 table의 PK를 회원ID로 하면 어떤 문제가 생길까? 회원 정보 테이블을 만들 적에 대부분 하나의 테이블에 사용자 정보 ( 이름/전화번호/주소/이… blog.naver.com

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다