사실... Appjam 합숙 전에 모든걸 다 끝내고 들어가려 했으나 못 했고 회고식아니면 나중에라도 더 구체화 시켜서 올려보려고 한다... 아래 있는 글들은 우선 먼저 적어놨던거라도 올리고자 이렇게 적게 되었다.
https://github.com/guswnsj0112/spring-auth-board
계기
이번 SOPT 38기 AppJam에서 운 좋게 Server로 참여 할 수 있게 되었다. 하지만 가장 큰 문제가 있었다. 바로 내가 백엔드를 올해 처음한다는 점이였다. 물론 파트장님께서는 세미나 때 친철하고 잘 알려주셨긴 했지만 학기 중 시간 관리를 못 했던 나에게는 아직 미숙한 부분이 많이 보였다. 그래서 Appjam 시작 전까지는 인증/인가, 엔터티 등 다양한걸 공부해서 가려고 한다.
구조
우선 서버 리드님과 상의했을 때 이번에 아키텍처를 모듈식 모놀리스로 가져가는게 어떻냐고 이야기 들었다. 나는 많은 걸 배우고 싶었기 때문에 좋다고 했고 그 후 계속 공부하면서 모듈식 아키텍처에 관해 배워가고 있다
학습 목표 설계

우선 GPT를 통해서 백엔드 프로젝트 목적을 세우게 되었다. 관련 레포는 여기서 확인 가능하다. 이걸 본 후 ERD를 먼저 설계 하였다.
ERD

User 1 : N Post
User 1 : N Comment
User 1 : N PostLike
User 1 : 1 RefreshToken
Post 1 : N Comment
Post 1 : N PostLike
- 리프레쉬 토큰은 한 유저 당 한개 씩으로 1:1 관계로 지정하였다
회원 가입 구현하기
User 엔티티
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String nickname;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;
public User(String email, String nickname, String password) {
this.email = email;
this.nickname = nickname;
this.password = password;
this.role = Role.USER;
}
}
- User 엔터티에 있는 role 에 관해 어떻게 작성해야할지 고민이 되었다…
- Role이 Enum인데 어디에 넣어야 하는가? 에 대한 고민이 들었는데
- ⇒ User와 강하게 관련된 값임으로 Entity 패키지 안에 넣어두었다
- 어떤 방식으로 ENUM을 저장할 것 인지?
- 이 방법은 DB에 순서에 따른 숫자로 저장되기 때문에 나중에 Role 추가 된다면 다른 Role로 지정될 수 있다
- @Enumerated(EnumType.STRING)
- @Enumerated(EnumType.ORDINAL) private Role role;
- 생성자에서 Role을 받으면 안된다..일반 회원가입은 무조건 User 임으로 User로 설정하자
- 회원가입할 때 사용자가 ADMIN 으로 가입하면 안되니까
- public User(String email, String password, String nickname) { this.email = email; this.password = password; this.nickname = nickname; this.role = Role.USER; }
- 생성자 실수처음에 그냥 @NoArgsConstructor 면 public 기본 생성자가 생기게 된다 이렇게 되면 아래와 같은 게 가능해진다이러면 email, password, nickname이 없는 불안전한 User가 만들어질 수 있다
- 그래서 JPA용 기본 생성자는 열어두되, 외부에서는 막는 느낌으로 protected 를 많이 쓴다
- User user = new User();
- @NoArgsConstructor
암호화
핵심흐름
요청 들어옴
→ JwtAuthenticationFilter 실행
→ Authorization Header에서 Bearer Token 추출
→ JWT 검증
→ userId 추출
→ Authentication 객체 생성
→ SecurityContextHolder에 저장
→ Controller/Service에서 로그인 유저 사용
의존성
implementation 'org.springframework.security:spring-security-crypto'
auth/build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
app/build.gradle
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
비밀번호 암호화만 auth에서 할 거면 spring-security-crypto를 auth에 둬도 되지만, SecurityConfig와 Filter는 app에서 관리하는 게 깔끔하다.
@Configuration // 스프링 설정 클래스
public class PasswordEncoderConfig {
// 📌 BCryptPasswordEncoder를 Spring Bean으로 등록
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- @Bean을 사용하여 BCryptPasswordEncoder를 스프링 빈으로 등록
- 이제 어디서든 DI(의존성 주입)를 통해 passwordEncoder를 사용할 수 있음
- BCryptPasswordEncoder는 비밀번호를 안전하게 해시화하고 검증하는 역할을 함
→ 앞으로 PasswordEncoder가 필요하면 BCryptPasswordEncoder 객체를 주입해준다
그래서 아래와 같이 AuthService에서 사용할 수 있다
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
//회원가입
@Transactional
public User register(SignupUserRequest request) {
if (userRepository.existsByEmail(request.email())) {
throw new IllegalArgumentException("이미 존재하는 이메일입니다.");
}
String hashedPassword = passwordEncoder.encode(request.password());
User savedUser = userRepository.save(new User(
request.email(),
request.nickName(),
hashedPassword
));
return savedUser;
}
}
공부 용임으로 User엔터티를 전체 반환했다
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.headers(headers -> headers.frameOptions(frame -> frame.disable()))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs/**",
"/api/auth/signup",
"/api/auth/login",
"/h2-console/**"
).permitAll()
.anyRequest().permitAll()
)
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable())
.build();
}
}
app/src/main/java/org/sopt/appjam/global/config/SecurityConfig.java
모듈러 모놀리식 구조를 사용 중임으로 app 모듈 안에 있는 config에 설정해놨다.
추가로 공부 중이라서 anyRequest().permitAll() 으로 열었지만 나중에 JWT 필터를 붙일 때 .anyRequest().authenticated() 이렇게 할 것이다
JWT 토큰 적용하기
의존성 추가
// 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'
TokenProvider 추가
JWT 인증 시스템에서 토큰 생성, 검증, 정보 추출을 담당하는 클래스
openssl rand -base64 32
우선, 터미널에 위에처럼 입력한다
- 윈도우에서는 Git bash에 입력하면 됨
- 복사는 ctrl + insert
- 붙여넣기는 shift + insert
정말 합숙 기간 전에는 다 끝내려고 했는데.. 여행이나 뭐 핑계 대느라 안했던 것 같다... 그래서 현재 개발 중인 NearBy 서비스를 더 자세히 분석해서 올리고자 한다!