diff --git a/src/main/java/umc/spring/post/config/WebConfig.java b/src/main/java/umc/spring/post/config/WebConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..47fe215d791708e457df46c21a1183000e1921dc --- /dev/null +++ b/src/main/java/umc/spring/post/config/WebConfig.java @@ -0,0 +1,17 @@ +package umc.spring.post.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 WebConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "DELETE") + .allowCredentials(false) + .maxAge(3000); + } +} \ No newline at end of file diff --git a/src/main/java/umc/spring/post/config/security/JwtAuthenticationFilter.java b/src/main/java/umc/spring/post/config/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..bc77b2a20bad8b4929fa9730364acc239cf8f6c9 --- /dev/null +++ b/src/main/java/umc/spring/post/config/security/JwtAuthenticationFilter.java @@ -0,0 +1,42 @@ +package umc.spring.post.config.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + + +@RequiredArgsConstructor +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { + String token = resolveToken((HttpServletRequest) request); + if (token != null && jwtTokenProvider.validateToken(token)) { + Authentication authentication = jwtTokenProvider.getAuthentication(token); + System.out.println(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + System.out.println(authentication); + } + chain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) { + return bearerToken.substring(7); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/umc/spring/post/config/security/JwtTokenProvider.java b/src/main/java/umc/spring/post/config/security/JwtTokenProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..d96c1f616ca505da13ca7449e793fd52348aed62 --- /dev/null +++ b/src/main/java/umc/spring/post/config/security/JwtTokenProvider.java @@ -0,0 +1,95 @@ +package umc.spring.post.config.security; + +import io.jsonwebtoken.*; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class JwtTokenProvider { + + private final String secretKey; + + public JwtTokenProvider( @Value("${jwt.secret}") final String secretKey) { + this.secretKey = secretKey; + } + + public TokenInfo generateToken(Authentication authentication) { + System.out.println(authentication); + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + System.out.println("auth " + authorities); + long now = (new Date()).getTime(); + Date accessTokenExpiration = new Date(now + 3600000); // 1h + Date refreshTokenExpiration = new Date(now + 1209600000); // 14d + String accessToken = Jwts.builder() + .setSubject(authentication.getName()) + .claim("auth", authorities) + .setExpiration(accessTokenExpiration) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + + String refreshToken = Jwts.builder() + .setExpiration(refreshTokenExpiration) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + + TokenInfo tokenInfo = new TokenInfo(); + tokenInfo.setGrantType("Bearer"); + tokenInfo.setAccessToken(accessToken); + tokenInfo.setRefreshToken(refreshToken); + + return tokenInfo; + } + + public Authentication getAuthentication(String accessToken) { + Claims claims = parseClaims(accessToken); + System.out.println("log" + claims); + if (claims.get("auth") == null) { + throw new RuntimeException("권한 정보가 없는 토큰입니다."); + } + + Collection<? extends GrantedAuthority> authorities = + Arrays.stream(claims.get("auth").toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + UserDetails principal = new User(claims.getSubject(), "", authorities); + return new UsernamePasswordAuthenticationToken(principal, "", authorities); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + System.out.println("Invalid JWT Token" + e); + } catch (ExpiredJwtException e) { + System.out.println("Expired JWT Token" + e); + } catch (UnsupportedJwtException e) { + System.out.println("Unsupported JWT Token" + e); + } catch (IllegalArgumentException e) { + System.out.println("JWT claims string is empty." + e); + } + return false; + } + + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(accessToken).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } +} \ No newline at end of file diff --git a/src/main/java/umc/spring/post/config/security/Role.java b/src/main/java/umc/spring/post/config/security/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..2bfb3da667e4a525b102ce30943d24cf46d7f1c0 --- /dev/null +++ b/src/main/java/umc/spring/post/config/security/Role.java @@ -0,0 +1,16 @@ +package umc.spring.post.config.security; + +public enum Role { + USER("ROLE_USER"), + ADMIN("ROLE_ADMIN"); + + private final String value; + + public String getValue() { + return value; + } + + Role(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/src/main/java/umc/spring/post/config/security/SecurityConfig.java b/src/main/java/umc/spring/post/config/security/SecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..1d26fefe2b3c847c306722b1148d3bb0eee61964 --- /dev/null +++ b/src/main/java/umc/spring/post/config/security/SecurityConfig.java @@ -0,0 +1,43 @@ +package umc.spring.post.config.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +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.CsrfConfigurer; +import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired + public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { + this.jwtAuthenticationFilter = jwtAuthenticationFilter; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.httpBasic(HttpBasicConfigurer::disable) + .csrf(CsrfConfigurer::disable) + .cors(Customizer.withDefaults()) + .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authorize -> authorize.requestMatchers("/**").permitAll()) + .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return PasswordEncoderFactories.createDelegatingPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/umc/spring/post/config/security/SecurityUtil.java b/src/main/java/umc/spring/post/config/security/SecurityUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..5577af306438c6b9ff3bd963cbb575b3d975a408 --- /dev/null +++ b/src/main/java/umc/spring/post/config/security/SecurityUtil.java @@ -0,0 +1,22 @@ +package umc.spring.post.config.security; + + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import umc.spring.post.data.dto.UserInfoDto; + +public class SecurityUtil { + public static UserInfoDto getCurrentMemberId() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || authentication.getName() == null) { + throw new RuntimeException("No authentication information."); + } + + UserInfoDto userInfoDto = new UserInfoDto(); + userInfoDto.setUserId(authentication.getName()); + userInfoDto.setMemberRole(authentication.getAuthorities().stream().toList().get(0).toString().replaceAll("ROLE_", "")); + + return userInfoDto; + } +} \ No newline at end of file diff --git a/src/main/java/umc/spring/post/config/security/TokenInfo.java b/src/main/java/umc/spring/post/config/security/TokenInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..7f9ad8464b611eb541568221dfd251116130c9b0 --- /dev/null +++ b/src/main/java/umc/spring/post/config/security/TokenInfo.java @@ -0,0 +1,52 @@ +package umc.spring.post.config.security; + +import lombok.Data; + +@Data +public class TokenInfo { + private String grantType; + private String accessToken; + private String refreshToken; + private String email; + private String memberRole; + + public String getGrantType() { + return grantType; + } + + public void setGrantType(String grantType) { + this.grantType = grantType; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getMemberRole() { + return memberRole; + } + + public void setMemberRole(String memberRole) { + this.memberRole = memberRole; + } +} \ No newline at end of file diff --git a/src/main/java/umc/spring/post/controller/AuthController.java b/src/main/java/umc/spring/post/controller/AuthController.java new file mode 100644 index 0000000000000000000000000000000000000000..14fc05b92468625f866aed7ac081b21e3213d58e --- /dev/null +++ b/src/main/java/umc/spring/post/controller/AuthController.java @@ -0,0 +1,37 @@ +package umc.spring.post.controller; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import umc.spring.post.config.security.TokenInfo; +import umc.spring.post.data.dto.UserInfoDto; +import umc.spring.post.data.dto.UserJoinDto; +import umc.spring.post.data.dto.UserLoginDto; +import umc.spring.post.service.AuthService; + +@RestController +@RequestMapping("/user") +public class AuthController { + + @Autowired + private final AuthService authService; + + public AuthController(AuthService authService) { + this.authService = authService; + } + + @PostMapping("/login") + public TokenInfo login(@RequestBody UserLoginDto userLoginDto) { + return authService.login(userLoginDto); + } + + @PostMapping("/register") + public void register(@RequestBody UserJoinDto userJoinDto) { + authService.join(userJoinDto); + } + + @GetMapping("/info") + public UserInfoDto info() { + return authService.info(); + } +} diff --git a/src/main/java/umc/spring/post/service/AuthService.java b/src/main/java/umc/spring/post/service/AuthService.java new file mode 100644 index 0000000000000000000000000000000000000000..ee097e894d5ea6685aae319d3228ca593aa98b8c --- /dev/null +++ b/src/main/java/umc/spring/post/service/AuthService.java @@ -0,0 +1,15 @@ +package umc.spring.post.service; + + +import umc.spring.post.config.security.TokenInfo; +import umc.spring.post.data.dto.UserInfoDto; +import umc.spring.post.data.dto.UserJoinDto; +import umc.spring.post.data.dto.UserLoginDto; + +public interface AuthService { + TokenInfo login(UserLoginDto userLoginDto); + + void join(UserJoinDto userJoinDto); + + UserInfoDto info(); +} diff --git a/src/main/java/umc/spring/post/service/AuthServiceImpl.java b/src/main/java/umc/spring/post/service/AuthServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..1880a4e3ff792acdd8d845e39b59e764208f9e37 --- /dev/null +++ b/src/main/java/umc/spring/post/service/AuthServiceImpl.java @@ -0,0 +1,90 @@ +package umc.spring.post.service; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + + + +import umc.spring.post.data.entity.User; +import umc.spring.post.config.security.JwtTokenProvider; +import umc.spring.post.config.security.Role; +import umc.spring.post.config.security.SecurityUtil; +import umc.spring.post.config.security.TokenInfo; +import umc.spring.post.data.dto.UserInfoDto; +import umc.spring.post.data.dto.UserJoinDto; +import umc.spring.post.data.dto.UserLoginDto; +import umc.spring.post.repository.UserRepository; + + +@Service +public class AuthServiceImpl implements AuthService, UserDetailsService { + + @Autowired + private final UserRepository userRepository; + + @Autowired + private final PasswordEncoder passwordEncoder; + + @Autowired + private final JwtTokenProvider jwtTokenProvider; + + public AuthServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtTokenProvider jwtTokenProvider) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public TokenInfo login(UserLoginDto userLoginDto) { + User user = userRepository.findByUserId(userLoginDto.getUserId()).orElseThrow(() -> new UsernameNotFoundException("아이디 혹은 비밀번호를 확인하세요.")); + + boolean matches = passwordEncoder.matches(userLoginDto.getPassword(), user.getPassword()); + if (!matches) throw new BadCredentialsException("아이디 혹은 비밀번호를 확인하세요."); + + Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUserId(), user.getPassword(), user.getAuthorities()); + + TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication); + tokenInfo.setEmail(user.getUserId()); + + tokenInfo.setMemberRole(user.getRole().toString()); + return tokenInfo; + } + + @Override + public void join(UserJoinDto userJoinDto) { + User user = new User(); + user.setUserId(userJoinDto.getUserId()); + user.setPassword(passwordEncoder.encode(userJoinDto.getPassword())); + user.setUserName(userJoinDto.getUserName()); + userRepository.save(user); + } + + @Override + public UserInfoDto info() { + UserInfoDto userInfoDto = SecurityUtil.getCurrentMemberId(); + return userInfoDto; + } + + @Override + public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { + return userRepository.findByUserId(userId) + .map(this::createUserDetails) + .orElseThrow(() -> new UsernameNotFoundException("해당하는 유저를 찾을 수 없습니다.")); + } + + private UserDetails createUserDetails(User user) { + return org.springframework.security.core.userdetails.User.builder() + .username(user.getUsername()) + .password(passwordEncoder.encode(user.getPassword())) + .roles(user.getRole().toString()) + .build(); + } +}