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
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ dependencies {

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

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

// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springframework.boot:spring-boot-configuration-processor'
}

tasks.named('test') {
Expand Down
61 changes: 61 additions & 0 deletions src/main/java/com/example/UMC/domain/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.example.UMC.domain.config;

import com.example.UMC.domain.user.security.CustomUserDetailsService;
import com.example.UMC.domain.user.security.JwtAuthFilter;
import com.example.UMC.domain.user.util.JwtUtil;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomUserDetailsService customUserDetailsService;
private final JwtUtil jwtUtil;

private final String[] allowUris = {
"/signup",
"/login",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
};

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 X
.formLogin(AbstractHttpConfigurer::disable) // 폼 로그인 비활성화
.httpBasic(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(requests -> requests
.requestMatchers(allowUris).permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public JwtAuthFilter jwtAuthFilter() {
return new JwtAuthFilter(jwtUtil, customUserDetailsService);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
5 changes: 5 additions & 0 deletions src/main/java/com/example/UMC/domain/enums/entity/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.UMC.domain.enums.entity;

public enum Role {
ROLE_ADMIN, ROLE_USER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.example.UMC.domain.user.controller;


import com.example.UMC.domain.user.dto.request.UserReqDTO;
import com.example.UMC.domain.user.entity.User;
import com.example.UMC.domain.user.exception.code.UserErrorCode;
import com.example.UMC.domain.user.security.CustomUserDetails;
import com.example.UMC.domain.user.service.UserService;
import com.example.UMC.domain.user.util.JwtUtil;
import com.example.UMC.global.apiPayload.ApiResponse;
import com.example.UMC.global.apiPayload.code.GeneralSucessCode;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;


import java.util.Optional;

@RestController
@RequiredArgsConstructor
public class UserController {

private final UserService userService;
private final JwtUtil jwtUtil;

// 회원가입 API
@PostMapping("/signup")
public ApiResponse<String> signUp(@RequestBody @Valid UserReqDTO.JoinDTO dto) {
// 1. 회원가입 처리
User user = userService.signup(dto);

// 2. 바로 CustomUserDetails 생성 + JWT 발급
CustomUserDetails userDetails = new CustomUserDetails(user);
String accessToken = jwtUtil.createAccessToken(userDetails);

return ApiResponse.onSucess(GeneralSucessCode.OK, accessToken);
}

// 로그인 API
@PostMapping("/login")
public ApiResponse<String> login(@RequestParam String email,
@RequestParam String password) {

Optional<CustomUserDetails> userDetailsOpt = userService.loginForJwt(email, password);

if (userDetailsOpt.isEmpty()) {
return ApiResponse.onFailure(UserErrorCode.INVALID_CREDENTIALS, null);
}

String accessToken = jwtUtil.createAccessToken(userDetailsOpt.get());
return ApiResponse.onSucess(GeneralSucessCode.OK, accessToken);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.UMC.domain.user.converter;

import com.example.UMC.domain.enums.entity.Role;
import com.example.UMC.domain.user.dto.request.UserReqDTO;
import com.example.UMC.domain.user.entity.User;

public class UserConverter {
public static User toUser(
UserReqDTO.JoinDTO dto,
String password,
Role role
) {
return User.builder()
.name(dto.name())
.email(dto.email())
.password(password)
.role(role)
.birth(dto.birth())
.address(dto.address())
.gender(dto.gender())
.loginId(dto.email())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.UMC.domain.user.dto.request;

import com.example.UMC.domain.enums.entity.Gender;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;


public class UserReqDTO {

public record JoinDTO(
@NotBlank
String name,
@Email
String email,
@NotBlank
String password,
@NotNull
Gender gender,
@NotNull
String birth,
@NotNull
String address
) {}


// 로그인
public record LoginDTO(
@NotBlank
String email,
@NotBlank
String password
){}
}
20 changes: 20 additions & 0 deletions src/main/java/com/example/UMC/domain/user/entity/User.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.UMC.domain.user.entity;

import com.example.UMC.domain.enums.entity.Gender;
import com.example.UMC.domain.enums.entity.Role;
import com.example.UMC.domain.enums.entity.Status;
import com.example.UMC.global.commmon.BaseEntity;
import com.example.UMC.domain.mission.entity.UserMission;
Expand Down Expand Up @@ -57,6 +58,9 @@ public class User extends BaseEntity {
@Column(name = "password", nullable = false, length = 100)
private String password;

@Enumerated(EnumType.STRING)
private Role role;

@Column(name = "point", nullable = false)
private Integer point;

Expand Down Expand Up @@ -85,4 +89,20 @@ public class User extends BaseEntity {
// 유저 미션 (User_Mission) 1:N
@OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE)
private List<UserMission> missions;

@PrePersist
public void prePersist() {
if (this.status == null) {
this.status = Status.ACTIVE; // 원하는 기본 상태
}
if (this.privacyAgree == null) {
this.privacyAgree = true; // 기본 동의
}
if (this.point == null) {
this.point = 0; // 기본 포인트 0
}
if (this.role == null) {
this.role = Role.ROLE_USER; // 기본 권한
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
@Getter
@AllArgsConstructor
public enum UserErrorCode implements BaseErrorCode {
INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "USER4001", "아이디 또는 비밀번호가 일치하지 않습니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER4004", "해당 사용자를 찾을 수 없습니다."),
DUPLICATE_EMAIL(HttpStatus.BAD_REQUEST, "USER4002","이미 존재하는 이메일입니다.");

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example.UMC.domain.user.security;

import com.example.UMC.domain.user.entity.User;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;

import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

private final User user;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(() -> user.getRole().toString());
}

@Override
public String getPassword() {
return user.getPassword();
}

@Override
public String getUsername() {
return user.getEmail();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.UMC.domain.user.security;

import com.example.UMC.domain.user.entity.User;
import com.example.UMC.domain.user.exception.UserException;
import com.example.UMC.domain.user.exception.code.UserErrorCode;
import com.example.UMC.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(
String username
) throws UsernameNotFoundException {
// 검증할 Member 조회
User user = userRepository.findByEmail(username)
.orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND));
// CustomUserDetails 반환
return new CustomUserDetails(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.example.UMC.domain.user.security;

import com.example.UMC.domain.user.util.JwtUtil;
import com.example.UMC.global.apiPayload.ApiResponse;
import com.example.UMC.global.apiPayload.code.GeneralErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailsService;

@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {

try {
// 토큰 가져오기
String token = request.getHeader("Authorization");
// token이 없거나 Bearer가 아니면 넘기기
if (token == null || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// Bearer이면 추출
token = token.replace("Bearer ", "");
// AccessToken 검증하기: 올바른 토큰이면
if (jwtUtil.isValid(token)) {
// 토큰에서 이메일 추출
String email = jwtUtil.getEmail(token);
// 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성

UserDetails user = customUserDetailsService.loadUserByUsername(email);
Authentication auth = new UsernamePasswordAuthenticationToken(
user,
null,
user.getAuthorities()
);
// 인증 완료 후 SecurityContextHolder에 넣기
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
} catch (Exception e) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

ApiResponse<Void> errorResponse = ApiResponse.onFailure(
GeneralErrorCode.UNAUTHORIZED,
null
);

ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), errorResponse);
}
}
}
Loading