From 66b2a5a5d2758157261213129f70a94d89809580 Mon Sep 17 00:00:00 2001
From: kkj6235 <kkj6235@ajou.ac.kr>
Date: Mon, 13 Nov 2023 13:23:36 +0900
Subject: [PATCH] feat : add authentication functionality and login service

---
 .../umc/spring/post/config/WebConfig.java     | 17 ++++
 .../security/JwtAuthenticationFilter.java     | 42 ++++++++
 .../config/security/JwtTokenProvider.java     | 95 +++++++++++++++++++
 .../umc/spring/post/config/security/Role.java | 16 ++++
 .../post/config/security/SecurityConfig.java  | 43 +++++++++
 .../post/config/security/SecurityUtil.java    | 22 +++++
 .../post/config/security/TokenInfo.java       | 52 ++++++++++
 .../post/controller/AuthController.java       | 37 ++++++++
 .../umc/spring/post/service/AuthService.java  | 15 +++
 .../spring/post/service/AuthServiceImpl.java  | 90 ++++++++++++++++++
 10 files changed, 429 insertions(+)
 create mode 100644 src/main/java/umc/spring/post/config/WebConfig.java
 create mode 100644 src/main/java/umc/spring/post/config/security/JwtAuthenticationFilter.java
 create mode 100644 src/main/java/umc/spring/post/config/security/JwtTokenProvider.java
 create mode 100644 src/main/java/umc/spring/post/config/security/Role.java
 create mode 100644 src/main/java/umc/spring/post/config/security/SecurityConfig.java
 create mode 100644 src/main/java/umc/spring/post/config/security/SecurityUtil.java
 create mode 100644 src/main/java/umc/spring/post/config/security/TokenInfo.java
 create mode 100644 src/main/java/umc/spring/post/controller/AuthController.java
 create mode 100644 src/main/java/umc/spring/post/service/AuthService.java
 create mode 100644 src/main/java/umc/spring/post/service/AuthServiceImpl.java

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 0000000..47fe215
--- /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 0000000..bc77b2a
--- /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 0000000..d96c1f6
--- /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 0000000..2bfb3da
--- /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 0000000..1d26fef
--- /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 0000000..5577af3
--- /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 0000000..7f9ad84
--- /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 0000000..14fc05b
--- /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 0000000..ee097e8
--- /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 0000000..1880a4e
--- /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();
+    }
+}
-- 
GitLab