diff --git a/refrigerator/.idea/modules/refrigerator.main.iml b/refrigerator/.idea/modules/refrigerator.main.iml index 397c268..afc1871 100644 --- a/refrigerator/.idea/modules/refrigerator.main.iml +++ b/refrigerator/.idea/modules/refrigerator.main.iml @@ -4,20 +4,5 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/refrigerator/src/main/java/moja/refrigerator/aggregate/user/User.java b/refrigerator/src/main/java/moja/refrigerator/aggregate/user/User.java index c10c057..03eb039 100644 --- a/refrigerator/src/main/java/moja/refrigerator/aggregate/user/User.java +++ b/refrigerator/src/main/java/moja/refrigerator/aggregate/user/User.java @@ -31,8 +31,6 @@ public class User { @CreationTimestamp private LocalDate joinDate; -// @Column(name = "leave_date") -// private LocalDate leaveDate; - - + @Column(name = "user_role") + private String userRole = "ROLE_USER"; } diff --git a/refrigerator/src/main/java/moja/refrigerator/config/SecurityConfig.java b/refrigerator/src/main/java/moja/refrigerator/config/SecurityConfig.java index 948026c..462533f 100644 --- a/refrigerator/src/main/java/moja/refrigerator/config/SecurityConfig.java +++ b/refrigerator/src/main/java/moja/refrigerator/config/SecurityConfig.java @@ -1,22 +1,44 @@ package moja.refrigerator.config; +import moja.refrigerator.jwt.JWTFilter; +import moja.refrigerator.jwt.JWTUtil; +import moja.refrigerator.jwt.LoginFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +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.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig { + // AuthenticationManager가 인자로 받을 AuthenticationConfiguraion 객체 생성자 주입 + private final AuthenticationConfiguration authenticationConfiguration; + // JWTUtil 주입 + private final JWTUtil jwtUtil; + + public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil) { + this.authenticationConfiguration = authenticationConfiguration; + this.jwtUtil = jwtUtil; + } + @Bean // 비밀번호 암호화를 위한 인코더 public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } + // AuthenticationManager Bean 등록 + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // csrf 보안 비활성화 @@ -43,6 +65,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .sessionManagement((session) -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // JWT 사용을 위한 세션리스 설정 + // 로그인 필터 추가 + http + .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class); + http + .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class); + return http.build(); } } \ No newline at end of file diff --git a/refrigerator/src/main/java/moja/refrigerator/controller/user/UserController.java b/refrigerator/src/main/java/moja/refrigerator/controller/user/UserController.java index 324055a..7adacab 100644 --- a/refrigerator/src/main/java/moja/refrigerator/controller/user/UserController.java +++ b/refrigerator/src/main/java/moja/refrigerator/controller/user/UserController.java @@ -15,10 +15,11 @@ public UserController(UserService userService) { this.userService = userService; } -// @GetMapping("/") -// public String getMainPage() { -// return "user Controller"; -// } + // 토큰 검증 로직 확인용 + @GetMapping("/") + public String getMainPage() { + return "user Controller"; + } // 회원 가입 처리 @PostMapping("/auth/join") diff --git a/refrigerator/src/main/java/moja/refrigerator/dto/user/CustomUserDetails.java b/refrigerator/src/main/java/moja/refrigerator/dto/user/CustomUserDetails.java new file mode 100644 index 0000000..2fb2d20 --- /dev/null +++ b/refrigerator/src/main/java/moja/refrigerator/dto/user/CustomUserDetails.java @@ -0,0 +1,53 @@ +package moja.refrigerator.dto.user; + +import moja.refrigerator.aggregate.user.User; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; + +public class CustomUserDetails implements UserDetails { + private final User user; + + public CustomUserDetails(User user) { + this.user = user; + } + + @Override + public Collection getAuthorities() { + Collection collection = new ArrayList<>(); + collection.add(() -> user.getUserRole()); + return collection; + } + + @Override + public String getPassword() { + return user.getUserPw(); + } + + @Override + public String getUsername() { + return user.getUserId(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/refrigerator/src/main/java/moja/refrigerator/jwt/JWTFilter.java b/refrigerator/src/main/java/moja/refrigerator/jwt/JWTFilter.java new file mode 100644 index 0000000..e22cfe8 --- /dev/null +++ b/refrigerator/src/main/java/moja/refrigerator/jwt/JWTFilter.java @@ -0,0 +1,64 @@ +package moja.refrigerator.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import moja.refrigerator.aggregate.user.User; +import moja.refrigerator.dto.user.CustomUserDetails; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class JWTFilter extends OncePerRequestFilter { + private final JWTUtil jwtUtil; + + public JWTFilter(JWTUtil jwtUtil) { + this.jwtUtil = jwtUtil; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // 헤더에서 토큰 추출 + String authorization = request.getHeader("Authorization"); + + // Authorization 헤더 검증 + if (authorization == null || !authorization.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + String token = authorization.split(" ")[1]; + + // 토큰 소멸 시간 검증 + if (jwtUtil.isExpired(token)) { + // 만료된 토큰이면 그냥 통과 + filterChain.doFilter(request, response); + return; + } + + // 토큰에서 정보 추출 + String username = jwtUtil.getUsername(token); + String role = jwtUtil.getRole(token); + + // User를 생성하여 값 set + User user = new User(); + user.setUserId(username); + user.setUserPw("temppassword"); + user.setUserRole(role); + + // UserDetails에 회원 정보 객체 담기 + CustomUserDetails customUserDetails = new CustomUserDetails(user); + + // 스프링 시큐리티 인증 토큰 생성 + Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()); + + // 세션에 사용자 등록 + SecurityContextHolder.getContext().setAuthentication(authToken); + + filterChain.doFilter(request, response); + } +} diff --git a/refrigerator/src/main/java/moja/refrigerator/jwt/JWTUtil.java b/refrigerator/src/main/java/moja/refrigerator/jwt/JWTUtil.java new file mode 100644 index 0000000..df05cc4 --- /dev/null +++ b/refrigerator/src/main/java/moja/refrigerator/jwt/JWTUtil.java @@ -0,0 +1,46 @@ +package moja.refrigerator.jwt; + +import io.jsonwebtoken.Jwts; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Component +public class JWTUtil { + private SecretKey secretKey; + + // application.yml에서 jwt secret key를 가져옴 + public JWTUtil(@Value("${spring.jwt.secret}") String secret) { + this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); + } + + // 토큰에서 username 추출 + public String getUsername(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class); + } + + // 토큰에서 role(권한) 추출 + public String getRole(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class); + } + + // 토큰 만료 여부 확인 + public Boolean isExpired(String token) { + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); + } + + // 토큰 생성 + public String createJwt(String username, String role, Long expiredMs) { + return Jwts.builder() + .claim("username", username) + .claim("role", role) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiredMs)) + .signWith(secretKey) + .compact(); + } +} diff --git a/refrigerator/src/main/java/moja/refrigerator/jwt/LoginFilter.java b/refrigerator/src/main/java/moja/refrigerator/jwt/LoginFilter.java new file mode 100644 index 0000000..622aac9 --- /dev/null +++ b/refrigerator/src/main/java/moja/refrigerator/jwt/LoginFilter.java @@ -0,0 +1,63 @@ +package moja.refrigerator.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import moja.refrigerator.dto.user.CustomUserDetails; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import java.util.Collection; +import java.util.Iterator; + +public class LoginFilter extends UsernamePasswordAuthenticationFilter { + private final AuthenticationManager authenticationManager; + private final JWTUtil jwtUtil; + + public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) { + this.authenticationManager = authenticationManager; + this.jwtUtil = jwtUtil; + } + + // 로그인 시도 처리 + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + // 클라이언트 요청에서 username, password 추출 + String username = obtainUsername(request); + String password = obtainPassword(request); + + // 이 정보를 토큰으로 만듦 (아직 인증되지 않은 상태) + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null); + + // AuthenticationManager에게 검증 요청 + return authenticationManager.authenticate(authToken); + } + + // 로그인 성공 처리 - JWT 토큰 발급 + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) { + CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); + + String username = customUserDetails.getUsername(); + + Collection authorities = authentication.getAuthorities(); + Iterator iterator = authorities.iterator(); + GrantedAuthority auth = iterator.next(); + + String role = auth.getAuthority(); + + String token = jwtUtil.createJwt(username, role, 60*60*10L); + + response.addHeader("Authorization", "Bearer " + token); + } + + // 로그인 실패 처리 + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) { + response.setStatus(401); + } +} diff --git a/refrigerator/src/main/java/moja/refrigerator/repository/user/UserRepository.java b/refrigerator/src/main/java/moja/refrigerator/repository/user/UserRepository.java index 80f3056..b3b44c0 100644 --- a/refrigerator/src/main/java/moja/refrigerator/repository/user/UserRepository.java +++ b/refrigerator/src/main/java/moja/refrigerator/repository/user/UserRepository.java @@ -8,9 +8,9 @@ @Repository public interface UserRepository extends JpaRepository { - boolean existsByUserId(String userId); + boolean existsByUserId(String userId); boolean existsByUserEmail(String userEmail); boolean existsByUserNickname(String userNickname); Optional findByUserPk(long userPk); - + Optional findByUserId(String userId); } diff --git a/refrigerator/src/main/java/moja/refrigerator/service/user/CustomUserDetailsService.java b/refrigerator/src/main/java/moja/refrigerator/service/user/CustomUserDetailsService.java new file mode 100644 index 0000000..604afac --- /dev/null +++ b/refrigerator/src/main/java/moja/refrigerator/service/user/CustomUserDetailsService.java @@ -0,0 +1,27 @@ +package moja.refrigerator.service.user; + +import moja.refrigerator.aggregate.user.User; +import moja.refrigerator.dto.user.CustomUserDetails; +import moja.refrigerator.repository.user.UserRepository; +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; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + private final UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + @Transactional(readOnly = true) + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + User userData = userRepository.findByUserId(username) + .orElseThrow(() -> new UsernameNotFoundException("입력하신 아이디로 가입된 사용자를 찾을 수 없습니다.: " + username)); + return new CustomUserDetails(userData); + } +} diff --git a/refrigerator/src/main/java/moja/refrigerator/service/user/UserServiceImpl.java b/refrigerator/src/main/java/moja/refrigerator/service/user/UserServiceImpl.java index b52fd64..7cd888b 100644 --- a/refrigerator/src/main/java/moja/refrigerator/service/user/UserServiceImpl.java +++ b/refrigerator/src/main/java/moja/refrigerator/service/user/UserServiceImpl.java @@ -35,10 +35,12 @@ public void createUser(UserCreateRequest request) { // 비밀번호 암호화 user.setUserPw(passwordEncoder.encode(request.getUserPw())); + user.setUserRole("ROLE_USER"); userRepository.save(user); } + private void checkDuplicateUser(UserCreateRequest request) { List errors = new ArrayList<>(); if (userRepository.existsByUserId(request.getUserId())) {