diff --git a/build.gradle b/build.gradle index abb27a08..4cc1321e 100644 --- a/build.gradle +++ b/build.gradle @@ -33,8 +33,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + - // H2 Database +// H2 Database runtimeOnly 'com.h2database:h2' // JSON diff --git a/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java b/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java new file mode 100644 index 00000000..7d932a4e --- /dev/null +++ b/src/main/java/poomasi/domain/auth/config/SecurityBeanGenerator.java @@ -0,0 +1,33 @@ +package poomasi.domain.auth.config; + +import jdk.jfr.Description; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import poomasi.domain.auth.util.JwtUtil; + +@Configuration +public class SecurityBeanGenerator { + + @Bean + @Description("AuthenticationProvider를 위한 Spring bean") + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + @Description("open endpoint를 위한 spring bean") + MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) { + return new MvcRequestMatcher.Builder(introspector); + } + + @Bean + @Description("jwt 토큰 발급을 위한 spring bean") + JwtUtil jwtProvider() { + return new JwtUtil(); + } +} + diff --git a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java index fde82c24..bfae7538 100644 --- a/src/main/java/poomasi/domain/auth/config/SecurityConfig.java +++ b/src/main/java/poomasi/domain/auth/config/SecurityConfig.java @@ -1,15 +1,85 @@ package poomasi.domain.auth.config; +import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import poomasi.domain.auth.security.filter.CustomLogoutFilter; +import poomasi.domain.auth.security.filter.CustomUsernamePasswordAuthenticationFilter; +import poomasi.domain.auth.security.filter.JwtAuthenticationFilter; +import poomasi.domain.auth.util.JwtUtil; +@AllArgsConstructor @Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true) // 인가 처리에 대한 annotation public class SecurityConfig { + private final AuthenticationConfiguration authenticationConfiguration; + private final JwtUtil jwtUtil; + private final MvcRequestMatcher.Builder mvc; + @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + return configuration.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + + // TODO : 나중에 허용될 endpoint가 많아지면 whiteList로 관리 예정 + // 임시로 GET : [api/farms, api/products, api/login, api/signup, /]은 열어둠 + http.authorizeHttpRequests((authorize) -> authorize + .requestMatchers(HttpMethod.GET, "/api/farms/**").permitAll() + .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() + .requestMatchers("/api/login", "/", "/api/signup").permitAll() + .anyRequest(). + authenticated() + ); + + //csrf 해제 + http.csrf(AbstractHttpConfigurer::disable); + + //cors 해제 + http.cors(AbstractHttpConfigurer::disable); + + //session 해제 -> jwt token 로그인 + http.sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ); + + //Oauth2.0 소셜 로그인 필터 구현 + + + //jwt 인증 필터 구현 + http.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), CustomUsernamePasswordAuthenticationFilter.class); + + //로그인 filter 구현 + http.addFilterAt(new CustomUsernamePasswordAuthenticationFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class); + + //form login disable + http.formLogin(AbstractHttpConfigurer::disable); + + //basic login disable + http.httpBasic(AbstractHttpConfigurer::disable); + + //log out filter 추가 + http.addFilterBefore(new CustomLogoutFilter(), CustomLogoutFilter.class); + return http.build(); + } -} \ No newline at end of file +} + + + + diff --git a/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java b/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java new file mode 100644 index 00000000..2429e5b7 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/filter/CustomLogoutFilter.java @@ -0,0 +1,18 @@ +package poomasi.domain.auth.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class CustomLogoutFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // access token 블랙리스트 저장해야 함 + // refresh token 삭제해야 함 + } +} diff --git a/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java b/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java new file mode 100644 index 00000000..2a741148 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/filter/CustomUsernamePasswordAuthenticationFilter.java @@ -0,0 +1,61 @@ +package poomasi.domain.auth.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jdk.jfr.Description; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +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.web.authentication.UsernamePasswordAuthenticationFilter; +import poomasi.domain.auth.security.userdetail.UserDetailsImpl; +import poomasi.domain.auth.util.JwtUtil; + +@RequiredArgsConstructor +public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter { + + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; + + @Description("인증 시도 메서드") + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { + String username = obtainUsername(request); + String password = obtainPassword(request); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null); + return authenticationManager.authenticate(authToken); + } + + @Override + @Description("로그인 성공 시, accessToken과 refreshToken 발급") + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) { + UserDetailsImpl customUserDetails = (UserDetailsImpl) authentication.getPrincipal(); + String username = customUserDetails.getUsername(); + String role = customUserDetails.getAuthority(); + + String accessToken = jwtUtil.generateAccessToken(username, role); + String refreshToken = jwtUtil.generateRefreshToken(username, role); + + response.setHeader("access", accessToken); + response.addCookie(createCookie("refresh", refreshToken)); + response.setStatus(HttpStatus.OK.value()); + } + + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) { + response.setStatus(401); + } + + private Cookie createCookie(String key, String value) { + + Cookie cookie = new Cookie(key, value); + cookie.setMaxAge(24*60*60); + cookie.setHttpOnly(true); + return cookie; + } + +} diff --git a/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java b/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 00000000..4760e0fc --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,81 @@ +package poomasi.domain.auth.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jdk.jfr.Description; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; +import poomasi.domain.auth.util.JwtUtil; +import poomasi.domain.member.entity.Member; +import poomasi.domain.member.entity.Role; + +import java.io.IOException; +import java.io.PrintWriter; + +@Description("access token을 검증하는 필터") +@AllArgsConstructor +@Slf4j +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + String accessToken = request.getHeader("access"); + + // refresh 재발급이나 다른 요청에 대해서 넘어감 + // access <~token~> + if (accessToken == null) { + log.info("access token이 존재하지 않아서 다음 filter로 넘어갑니다."); + filterChain.doFilter(request, response); + return; + } + + // 만료 검사 + if(jwtUtil.isTokenExpired(accessToken)){ + log.warn("[인증 실패] - 토큰이 만료되었습니다."); + PrintWriter writer = response.getWriter(); + writer.print("만료된 토큰입니다."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + // 유효성 검사 + if(jwtUtil.validateToken(accessToken)) { + log.warn("[인증 실패] - 위조된 토큰입니다."); + PrintWriter writer = response.getWriter(); + writer.print("위조된 토큰입니다."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + // access token 추출하기 + String tokenType = jwtUtil.getTokenTypeFromToken(accessToken); + + if(!tokenType.equals("access")){ + log.info("[인증 실패] - 위조된 토큰입니다."); + PrintWriter writer = response.getWriter(); + writer.print("위조된 토큰입니다."); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return; + } + + String username = jwtUtil.getEmailFromToken(accessToken); + String role = jwtUtil.getRoleFromToken(accessToken); + + //TODO : Object, Object, Collection 형태 ..처리 해야 함 + //TODO : userDetailsImpl(), null(password) + //TODO : security context에 저장해야 함. + Member member = new Member(username, Role.valueOf(role)); + + + + + + + } +} diff --git a/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsImpl.java b/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsImpl.java new file mode 100644 index 00000000..83d71a21 --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsImpl.java @@ -0,0 +1,73 @@ +package poomasi.domain.auth.security.userdetail; + +import lombok.Getter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import poomasi.domain.member.entity.Member; + +import java.util.ArrayList; +import java.util.Collection; + + + +@Getter +public class UserDetailsImpl implements UserDetails { + + //private static final long serialVersionUID = 1L; + private final Member member; + + public UserDetailsImpl(Member member) { + this.member = member; + } + + @Override + public Collection getAuthorities() { + Collection collection = new ArrayList<>(); + collection.add(new GrantedAuthority() { + @Override + public String getAuthority() { + return member.getRole() + .name(); + } + }); + + return collection; + } + + public String getAuthority() { + return member.getRole().name(); + } + + @Override + public String getPassword() { + return this.member + .getPassword(); + } + + @Override + public String getUsername() { + return this.member + .getEmail(); + } + + @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/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java b/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java new file mode 100644 index 00000000..e14131fa --- /dev/null +++ b/src/main/java/poomasi/domain/auth/security/userdetail/UserDetailsServiceImpl.java @@ -0,0 +1,27 @@ +package poomasi.domain.auth.security.userdetail; + +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 poomasi.domain.member.entity.Member; +import poomasi.domain.member.repository.MemberRepository; +import poomasi.global.error.BusinessError; +import poomasi.global.error.BusinessException; + +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + private final MemberRepository memberRepository; + public UserDetailsServiceImpl(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BusinessException(BusinessError.MEMBER_NOT_FOUND)); + return new UserDetailsImpl(member); + } + +} diff --git a/src/main/java/poomasi/domain/member/entity/Member.java b/src/main/java/poomasi/domain/member/entity/Member.java index f7f9e752..2f08b496 100644 --- a/src/main/java/poomasi/domain/member/entity/Member.java +++ b/src/main/java/poomasi/domain/member/entity/Member.java @@ -52,6 +52,12 @@ public Member(String email, String password, LoginType loginType, Role role) { this.role = role; } + public Member(String email, Role role){ + this.email = email; + this.role = role; + } + + public void setProfile(MemberProfile profile) { this.profile = profile; if (profile != null) { diff --git a/src/main/java/poomasi/domain/member/entity/Role.java b/src/main/java/poomasi/domain/member/entity/Role.java index 608a4b00..957883c6 100644 --- a/src/main/java/poomasi/domain/member/entity/Role.java +++ b/src/main/java/poomasi/domain/member/entity/Role.java @@ -1,7 +1,15 @@ package poomasi.domain.member.entity; -public enum Role { +import org.springframework.security.core.GrantedAuthority; + +public enum Role implements GrantedAuthority { ROLE_ADMIN, // 관리자 - ROLE_FARMER, // 농부 - ROLE_CUSTOMER // 고객 + ROLE_FARMER, // 농부 역할 + ROLE_CUSTOMER; // 구매자 역할 + + @Override + public String getAuthority() { + return name(); + } } +