diff --git a/src/main/java/com/cmc/suppin/global/security/SecurityConfig.java b/src/main/java/com/cmc/suppin/global/security/SecurityConfig.java new file mode 100644 index 0000000..c8d1c65 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/security/SecurityConfig.java @@ -0,0 +1,110 @@ +package com.cmc.suppin.global.security; + +import com.cmc.suppin.global.security.jwt.JWTFilter; +import com.cmc.suppin.global.security.jwt.JWTUtil; +import com.cmc.suppin.global.security.jwt.LoginFilter; +import jakarta.servlet.http.HttpServletRequest; +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; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; + +import java.util.Collections; + +@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; + } + + //AuthenticationManager Bean 등록 + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { + + return configuration.getAuthenticationManager(); + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + http + .cors((corsCustomizer -> corsCustomizer.configurationSource(new CorsConfigurationSource() { + + @Override + public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { + + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(Collections.singletonList("http://localhost:3000")); + configuration.setAllowedMethods(Collections.singletonList("*")); + configuration.setAllowCredentials(true); + configuration.setAllowedHeaders(Collections.singletonList("*")); + configuration.setMaxAge(3600L); + + configuration.setExposedHeaders(Collections.singletonList("Authorization")); + + return configuration; + } + }))); + + //csrf disable + http + .csrf((auth) -> auth.disable()); + + //From 로그인 방식 disable + http + .formLogin((auth) -> auth.disable()); + + //http basic 인증 방식 disable + http + .httpBasic((auth) -> auth.disable()); + + //경로별 인가 작업 + http + .authorizeHttpRequests((auth) -> auth + .requestMatchers("/login", "/", "/join").permitAll() + .requestMatchers("/admin").hasRole("ADMIN") + .anyRequest().authenticated()); + + //JWTFilter 추가 + http + .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class); + + //LoginFilter 추가 LoginFilter()는 인자를 받음 (AuthenticationManager() 메소드에 authenticationConfiguration 객체를 넣어야 함) 따라서 등록 필요 + http + .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class); + + //세션 설정 + http + .sessionManagement((session) -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + + return http.build(); + } +} + diff --git a/src/main/java/com/cmc/suppin/global/security/jwt/JWTFilter.java b/src/main/java/com/cmc/suppin/global/security/jwt/JWTFilter.java new file mode 100644 index 0000000..8374cf1 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/security/jwt/JWTFilter.java @@ -0,0 +1,74 @@ +package com.cmc.suppin.global.security.jwt; + +import com.cmc.suppin.member.controller.dto.MemberDetails; +import com.cmc.suppin.member.domain.Member; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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 { + + //request에서 Authorization 헤더를 찾음 + String authorization = request.getHeader("Authorization"); + + //Authorization 헤더 검증(토큰이 없는 경우 처리해주는 부분) + if (authorization == null || !authorization.startsWith("Bearer ")) { + + System.out.println("token null"); + filterChain.doFilter(request, response); + + //조건이 해당되면 메소드 종료 (필수) + return; + } + + //Bearer 부분 제거 후 순수 토큰만 획득 + String token = authorization.split(" ")[1]; + + //토큰 소멸 시간 검증(토큰 만료시 처리해주는 부분) + if (jwtUtil.isExpired(token)) { + + System.out.println("token expired"); + filterChain.doFilter(request, response); + + //조건이 해당되면 메소드 종료 (필수) + return; + } + + //토큰에서 username과 role 획득 + String username = jwtUtil.getUsername(token); + String role = jwtUtil.getRole(token); + + // Member Entity를 생성하여 값 set + Member member = new Member(); + member.setName(username); + member.setPassword("tempPassword"); + member.setRole(role); + + //MemberDetails에 회원 정보 객체 담기 + MemberDetails customUserDetails = new MemberDetails(member); + + //스프링 시큐리티 인증 토큰 생성 + Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()); + //세션에 사용자 등록 + SecurityContextHolder.getContext().setAuthentication(authToken); + + filterChain.doFilter(request, response); + } +} + diff --git a/src/main/java/com/cmc/suppin/global/security/jwt/JWTUtil.java b/src/main/java/com/cmc/suppin/global/security/jwt/JWTUtil.java new file mode 100644 index 0000000..e9f31d1 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/security/jwt/JWTUtil.java @@ -0,0 +1,48 @@ +package com.cmc.suppin.global.security.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; + + public JWTUtil(@Value("${JWT_TOKEN_SECRET}") String secret) { + + this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm()); + } + + public String getUsername(String token) { + + return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class); + } + + 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/src/main/java/com/cmc/suppin/global/security/jwt/LoginFilter.java b/src/main/java/com/cmc/suppin/global/security/jwt/LoginFilter.java new file mode 100644 index 0000000..fd9a419 --- /dev/null +++ b/src/main/java/com/cmc/suppin/global/security/jwt/LoginFilter.java @@ -0,0 +1,77 @@ +package com.cmc.suppin.global.security.jwt; + +import com.cmc.suppin.member.controller.dto.MemberDetails; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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); + + //스프링 시큐리티에서 username과 password를 검증하기 위해서는 token에 담아야 함 + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null); + + //token에 담은 검증을 위한 AuthenticationManager로 전달 + return authenticationManager.authenticate(authToken); + } + + //로그인 성공시 실행하는 메소드 (여기서 JWT를 발급됨) + @Override + protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) { + + MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal(); + + String username = memberDetails.getUsername(); + + Collection authorities = authentication.getAuthorities(); + if (authorities.isEmpty()) { + throw new IllegalStateException("권한이 없습니다."); + } + + Iterator iterator = authorities.iterator(); + GrantedAuthority auth = iterator.next(); + + String role = auth.getAuthority(); + + long expirationTime = 1000 * 60 * 60 * 24 * 7; // 7일 + + String token = jwtUtil.createJwt(username, role, expirationTime); + + response.addHeader("Authorization", "Bearer " + token); + } + + + //로그인 실패시 실행하는 메소드 + @Override + protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) { + + //로그인 실패시 401 응답코드 반환 + response.setStatus(401); + } +} + diff --git a/src/main/java/com/cmc/suppin/member/controller/dto/MemberDetails.java b/src/main/java/com/cmc/suppin/member/controller/dto/MemberDetails.java new file mode 100644 index 0000000..053536e --- /dev/null +++ b/src/main/java/com/cmc/suppin/member/controller/dto/MemberDetails.java @@ -0,0 +1,62 @@ +package com.cmc.suppin.member.controller.dto; + +import com.cmc.suppin.member.domain.Member; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; + +public class MemberDetails implements UserDetails { + + private final Member member; + + public MemberDetails(Member member) { + this.member = member; + } + + @Override + public Collection getAuthorities() { + + Collection collection = new ArrayList<>(); + + collection.add(new GrantedAuthority() { + @Override + public String getAuthority() { + return member.getRole(); + } + }); + + return collection; + } + + @Override + public String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getName(); + } + + @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/com/cmc/suppin/member/domain/Member.java b/src/main/java/com/cmc/suppin/member/domain/Member.java index fff34e7..cfb630d 100644 --- a/src/main/java/com/cmc/suppin/member/domain/Member.java +++ b/src/main/java/com/cmc/suppin/member/domain/Member.java @@ -2,12 +2,16 @@ import com.cmc.suppin.event.domain.Event; import com.cmc.suppin.global.domain.BaseDateTimeEntity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; -import javax.persistence.*; import java.util.ArrayList; import java.util.List; @Entity +@Getter +@Setter public class Member extends BaseDateTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -32,5 +36,7 @@ public class Member extends BaseDateTimeEntity { @Column(columnDefinition = "VARCHAR(13)", nullable = false) private String phoneNumber; + private String role; + } diff --git a/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java b/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java index c0c6309..f113cd5 100644 --- a/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java +++ b/src/main/java/com/cmc/suppin/member/domain/repository/MemberRepository.java @@ -5,4 +5,7 @@ public interface MemberRepository extends JpaRepository { + Boolean existsByName(String name); + + Member findByName(String name); }