diff --git a/.gitignore b/.gitignore index c2065bc..d6709aa 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +*.yml \ No newline at end of file diff --git a/build.gradle b/build.gradle index d6dd785..43b5228 100644 --- a/build.gradle +++ b/build.gradle @@ -25,12 +25,19 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-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..b764667 --- /dev/null +++ b/src/main/java/com/example/springhw4/config/CorsMvcConfig.java @@ -0,0 +1,15 @@ +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:3000"); + } +} 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..a2420b9 --- /dev/null +++ b/src/main/java/com/example/springhw4/config/SecurityConfig.java @@ -0,0 +1,99 @@ +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() + .requestMatchers("/admin").hasRole("ADMIN") + .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..b515bc4 --- /dev/null +++ b/src/main/java/com/example/springhw4/controller/AdminController.java @@ -0,0 +1,14 @@ +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..2afa28f --- /dev/null +++ b/src/main/java/com/example/springhw4/controller/JoinController.java @@ -0,0 +1,21 @@ +package com.example.springhw4.controller; + +import com.example.springhw4.dto.JoinDto; +import com.example.springhw4.service.JoinService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@ResponseBody +@RequiredArgsConstructor +public class JoinController { + + private final JoinService joinService; + @PostMapping("/join") + public String joinProcess(JoinDto joinDto){ + 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..e70498b --- /dev/null +++ b/src/main/java/com/example/springhw4/controller/MainController.java @@ -0,0 +1,28 @@ +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"; + } +} 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..b21b813 --- /dev/null +++ b/src/main/java/com/example/springhw4/dto/CustomUserDetails.java @@ -0,0 +1,62 @@ +package com.example.springhw4.dto; + +import com.example.springhw4.entity.UserEntity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +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..ad82730 --- /dev/null +++ b/src/main/java/com/example/springhw4/dto/JoinDto.java @@ -0,0 +1,13 @@ +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..0a33313 --- /dev/null +++ b/src/main/java/com/example/springhw4/entity/UserEntity.java @@ -0,0 +1,21 @@ +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..33c28ff --- /dev/null +++ b/src/main/java/com/example/springhw4/jwt/JWTFilter.java @@ -0,0 +1,70 @@ +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 { + //request에서 Authorization 헤더를 찾음 + String authorization= request.getHeader("Authorization"); + + //authorization 헤더 검증 + if (authorization == null || !authorization.startsWith("Bearer ")) { + + System.out.println("token null"); + filterChain.doFilter(request, response); + + //조건이 해당되면 메소드 종료 (필수) + return; + } + + 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..0fa56a1 --- /dev/null +++ b/src/main/java/com/example/springhw4/jwt/JWTUtil.java @@ -0,0 +1,46 @@ +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.security.Security; +import java.util.Date; + +@Component +public class JWTUtil { + private SecretKey secretKey; + public JWTUtil(@Value("${spring.jwt.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/example/springhw4/jwt/LoginFilter.java b/src/main/java/com/example/springhw4/jwt/LoginFilter.java new file mode 100644 index 0000000..1724915 --- /dev/null +++ b/src/main/java/com/example/springhw4/jwt/LoginFilter.java @@ -0,0 +1,62 @@ +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 { + String username = obtainUsername(request); + String password = obtainPassword(request); + + System.out.println(username); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null); + + return authenticationManager.authenticate(authToken); + + } + + @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..96ffec5 --- /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..2943dd7 --- /dev/null +++ b/src/main/java/com/example/springhw4/service/CustomUserDetailsService.java @@ -0,0 +1,33 @@ +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..8d7b6fd --- /dev/null +++ b/src/main/java/com/example/springhw4/service/JoinService.java @@ -0,0 +1,35 @@ +package com.example.springhw4.service; + +import com.example.springhw4.dto.JoinDto; +import com.example.springhw4.entity.UserEntity; +import com.example.springhw4.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class JoinService { + private final UserRepository userRepository; + private final 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