diff --git a/.gitignore b/.gitignore index c2065bc..91e5ec4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ out/ ### VS Code ### .vscode/ +*.yml diff --git a/build.gradle b/build.gradle index d6dd785..e55e651 100644 --- a/build.gradle +++ b/build.gradle @@ -26,11 +26,18 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // 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' } tasks.named('test') { diff --git a/src/main/java/com/example/springhw4/config/CorsMvcConfig.java b/src/main/java/com/example/springhw4/config/CorsMvcConfig.java new file mode 100644 index 0000000..b666a37 --- /dev/null +++ b/src/main/java/com/example/springhw4/config/CorsMvcConfig.java @@ -0,0 +1,16 @@ +package com.example.springhw4.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsMvcConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry corsRegistry) { + + corsRegistry.addMapping("/**") + .allowedOrigins("http://localhost:8080"); + } +} diff --git a/src/main/java/com/example/springhw4/config/SecurityConfig.java b/src/main/java/com/example/springhw4/config/SecurityConfig.java new file mode 100644 index 0000000..f665d14 --- /dev/null +++ b/src/main/java/com/example/springhw4/config/SecurityConfig.java @@ -0,0 +1,95 @@ +package com.example.springhw4.config; + +import com.example.springhw4.jwt.JWTFilter; +import com.example.springhw4.jwt.JWTUtil; +import com.example.springhw4.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 { + + private final AuthenticationConfiguration authenticationConfiguration; + + private final JWTUtil jwtUtil; + + public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil) { + this.authenticationConfiguration = authenticationConfiguration; + this.jwtUtil = jwtUtil; + } + + @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; + } + }))); + + http + .csrf((auth) -> auth.disable()); + + http + .formLogin((auth) -> auth.disable()); + + http + .httpBasic((auth) -> auth.disable()); + + http + .authorizeHttpRequests((auth) -> auth + .requestMatchers("/login", "/", "/join").permitAll() + .anyRequest().authenticated()); + + http + .addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class); + + 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/example/springhw4/controller/AdminController.java b/src/main/java/com/example/springhw4/controller/AdminController.java new file mode 100644 index 0000000..a67df84 --- /dev/null +++ b/src/main/java/com/example/springhw4/controller/AdminController.java @@ -0,0 +1,15 @@ +package com.example.springhw4.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@ResponseBody +public class AdminController { + + @GetMapping("/admin") + public String adminP() { + return "admin Controller"; + } +} diff --git a/src/main/java/com/example/springhw4/controller/JoinController.java b/src/main/java/com/example/springhw4/controller/JoinController.java new file mode 100644 index 0000000..1b3efe2 --- /dev/null +++ b/src/main/java/com/example/springhw4/controller/JoinController.java @@ -0,0 +1,27 @@ +package com.example.springhw4.controller; + +import com.example.springhw4.dto.JoinDTO; +import com.example.springhw4.service.JoinService; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@ResponseBody +public class JoinController { + + private final JoinService joinService; + + public JoinController(JoinService joinService) { + this.joinService = joinService; + } + + @PostMapping("/join") + public String joinProcess(JoinDTO joinDTO) { + + System.out.println(joinDTO.getUsername()); + joinService.joinProcess(joinDTO); + + return "ok"; + } +} diff --git a/src/main/java/com/example/springhw4/controller/MainController.java b/src/main/java/com/example/springhw4/controller/MainController.java new file mode 100644 index 0000000..aa481f8 --- /dev/null +++ b/src/main/java/com/example/springhw4/controller/MainController.java @@ -0,0 +1,31 @@ +package com.example.springhw4.controller; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import java.util.Collection; +import java.util.Iterator; + +@Controller +@ResponseBody +public class MainController { + + @GetMapping("/") + public String mainP() { + + String name = SecurityContextHolder.getContext().getAuthentication().getName(); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + Collection authorities = authentication.getAuthorities(); + Iterator iter = authorities.iterator(); + GrantedAuthority auth = iter.next(); + String role = auth.getAuthority(); + + return "main Controller: " + name + role; + } +} diff --git a/src/main/java/com/example/springhw4/dto/CustomUserDetails.java b/src/main/java/com/example/springhw4/dto/CustomUserDetails.java new file mode 100644 index 0000000..6d6b123 --- /dev/null +++ b/src/main/java/com/example/springhw4/dto/CustomUserDetails.java @@ -0,0 +1,63 @@ +package com.example.springhw4.dto; + +import com.example.springhw4.entity.UserEntity; +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 UserEntity userEntity; + + public CustomUserDetails(UserEntity userEntity) { + this.userEntity = userEntity; + } + + @Override + public Collection getAuthorities() { + Collection collection = new ArrayList<>(); + + collection.add(new GrantedAuthority() { + + @Override + public String getAuthority() { + + return userEntity.getRole(); + } + }); + + return collection; + } + + @Override + public String getPassword() { + return userEntity.getPassword(); + } + + @Override + public String getUsername() { + return userEntity.getUsername(); + } + + @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/example/springhw4/dto/JoinDTO.java b/src/main/java/com/example/springhw4/dto/JoinDTO.java new file mode 100644 index 0000000..f1c40f8 --- /dev/null +++ b/src/main/java/com/example/springhw4/dto/JoinDTO.java @@ -0,0 +1,14 @@ +package com.example.springhw4.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class JoinDTO { + + private String username; + + private String password; + +} diff --git a/src/main/java/com/example/springhw4/entity/UserEntity.java b/src/main/java/com/example/springhw4/entity/UserEntity.java new file mode 100644 index 0000000..34fab55 --- /dev/null +++ b/src/main/java/com/example/springhw4/entity/UserEntity.java @@ -0,0 +1,24 @@ +package com.example.springhw4.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +public class UserEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private int id; + + private String username; + + private String password; + + private String role; +} diff --git a/src/main/java/com/example/springhw4/jwt/JWTFilter.java b/src/main/java/com/example/springhw4/jwt/JWTFilter.java new file mode 100644 index 0000000..dd05165 --- /dev/null +++ b/src/main/java/com/example/springhw4/jwt/JWTFilter.java @@ -0,0 +1,72 @@ +package com.example.springhw4.jwt; + +import com.example.springhw4.dto.CustomUserDetails; +import com.example.springhw4.entity.UserEntity; +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 { + String authorization= request.getHeader("Authorization"); + + //Authorization 헤더 검증 + if (authorization == null || !authorization.startsWith("Bearer ")) { + + System.out.println("token null"); + filterChain.doFilter(request, response); + + //조건이 해당되면 메소드 종료 (필수) + return; + } + + System.out.println("authorization now"); + //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); + + //userEntity를 생성하여 값 set + UserEntity userEntity = new UserEntity(); + userEntity.setUsername(username); + userEntity.setPassword("temppassword"); + userEntity.setRole(role); + + //UserDetails에 회원 정보 객체 담기 + CustomUserDetails customUserDetails = new CustomUserDetails(userEntity); + + //스프링 시큐리티 인증 토큰 생성 + Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()); + //세션에 사용자 등록 + SecurityContextHolder.getContext().setAuthentication(authToken); + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/example/springhw4/jwt/JWTUtil.java b/src/main/java/com/example/springhw4/jwt/JWTUtil.java new file mode 100644 index 0000000..52cec4c --- /dev/null +++ b/src/main/java/com/example/springhw4/jwt/JWTUtil.java @@ -0,0 +1,48 @@ +package com.example.springhw4.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("${spring.jwt.secret}")String secret) { + + + 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/example/springhw4/jwt/LoginFilter.java b/src/main/java/com/example/springhw4/jwt/LoginFilter.java new file mode 100644 index 0000000..7238f3b --- /dev/null +++ b/src/main/java/com/example/springhw4/jwt/LoginFilter.java @@ -0,0 +1,67 @@ +package com.example.springhw4.jwt; + +import com.example.springhw4.dto.CustomUserDetails; +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) { + 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/src/main/java/com/example/springhw4/repository/UserRepository.java b/src/main/java/com/example/springhw4/repository/UserRepository.java new file mode 100644 index 0000000..fd11bfd --- /dev/null +++ b/src/main/java/com/example/springhw4/repository/UserRepository.java @@ -0,0 +1,12 @@ +package com.example.springhw4.repository; + +import com.example.springhw4.entity.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + + Boolean existsByUsername(String username); + + UserEntity findByUsername(String username); + +} diff --git a/src/main/java/com/example/springhw4/service/CustomUserDetailsService.java b/src/main/java/com/example/springhw4/service/CustomUserDetailsService.java new file mode 100644 index 0000000..b31a236 --- /dev/null +++ b/src/main/java/com/example/springhw4/service/CustomUserDetailsService.java @@ -0,0 +1,31 @@ +package com.example.springhw4.service; + +import com.example.springhw4.dto.CustomUserDetails; +import com.example.springhw4.entity.UserEntity; +import com.example.springhw4.repository.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; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final UserRepository userRepository; + + public CustomUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + + UserEntity userData = userRepository.findByUsername(username); + + if (userData != null) { + return new CustomUserDetails(userData); + } + + return null; + } +} diff --git a/src/main/java/com/example/springhw4/service/JoinService.java b/src/main/java/com/example/springhw4/service/JoinService.java new file mode 100644 index 0000000..4b31c63 --- /dev/null +++ b/src/main/java/com/example/springhw4/service/JoinService.java @@ -0,0 +1,40 @@ +package com.example.springhw4.service; + +import com.example.springhw4.dto.JoinDTO; +import com.example.springhw4.entity.UserEntity; +import com.example.springhw4.repository.UserRepository; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +public class JoinService { + + private final UserRepository userRepository; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + public JoinService(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder) { + this.userRepository = userRepository; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + } + + public void joinProcess(JoinDTO joinDTO) { + + String username = joinDTO.getUsername(); + String password = joinDTO.getPassword(); + + Boolean isExist = userRepository.existsByUsername(username); + + if (isExist) { + return; + } + + UserEntity data = new UserEntity(); + + data.setUsername(username); + data.setPassword(bCryptPasswordEncoder.encode(password)); + data.setRole("ROLE_ADMIN"); + + userRepository.save(data); + + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index 9b78d0c..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,15 +0,0 @@ -spring: - datasource: - url: jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 - username: your_username - password: your_password - driver-class-name: com.mysql.cj.jdbc.Driver - - jpa: - hibernate: - ddl-auto: create - show-sql: true - properties: - hibernate: - format_sql: true - dialect: org.hibernate.dialect.MySQL8Dialect \ No newline at end of file