-
Notifications
You must be signed in to change notification settings - Fork 0
๐ Jwt Login #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
๐ Jwt Login #24
Changes from 7 commits
ad676f1
a85b4ff
d3f377b
c4ef48d
95633fc
23a0573
4ce724a
8f39fd6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package com.mycom.socket.auth.controller; | ||
|
|
||
| import com.mycom.socket.auth.dto.request.LoginRequestDto; | ||
| import com.mycom.socket.auth.dto.request.RegisterRequestDto; | ||
| import com.mycom.socket.auth.dto.response.LoginResponseDto; | ||
| import com.mycom.socket.auth.service.AuthService; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
| import org.springframework.web.bind.annotation.RequestMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/auth") | ||
| @RequiredArgsConstructor | ||
| public class AuthController { | ||
|
|
||
| private final AuthService authService; | ||
|
|
||
| @PostMapping("/login") | ||
| public LoginResponseDto login(@Valid @RequestBody LoginRequestDto request, | ||
| HttpServletResponse response) { | ||
| return authService.login(request, response); | ||
| } | ||
|
|
||
| @PostMapping("/logout") | ||
| public void logout(HttpServletResponse response) { | ||
| authService.logout(response); | ||
| } | ||
|
|
||
| @PostMapping("/register") | ||
| public Long register(@Valid @RequestBody RegisterRequestDto request) { | ||
| return authService.register(request); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.mycom.socket.auth.dto.request; | ||
|
|
||
| import jakarta.validation.constraints.Email; | ||
| import jakarta.validation.constraints.NotBlank; | ||
|
|
||
| public record LoginRequestDto( | ||
| @NotBlank(message = "์ด๋ฉ์ผ์ ํ์์ ๋๋ค") | ||
| @Email(message = "์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค") | ||
| String email, | ||
|
|
||
| @NotBlank(message = "๋น๋ฐ๋ฒํธ๋ ํ์์ ๋๋ค") | ||
| String password | ||
| ) {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| package com.mycom.socket.auth.dto.response; | ||
|
|
||
| public record LoginResponseDto( | ||
| String email, | ||
| String nickname | ||
| ) { | ||
| public static LoginResponseDto of(String email, String nickname) { | ||
| return new LoginResponseDto(email, nickname); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package com.mycom.socket.auth.jwt; | ||
|
|
||
| import com.mycom.socket.auth.service.MemberDetailsService; | ||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.Cookie; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
| import org.springframework.security.core.context.SecurityContextHolder; | ||
| import org.springframework.security.core.userdetails.UserDetails; | ||
| import org.springframework.util.StringUtils; | ||
| import org.springframework.web.filter.OncePerRequestFilter; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| @RequiredArgsConstructor | ||
| public class JWTFilter extends OncePerRequestFilter { | ||
|
|
||
| private final JWTUtil jwtUtil; | ||
| private final MemberDetailsService memberDetailsService; | ||
|
|
||
| @Override | ||
| protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||
| throws ServletException, IOException { | ||
|
|
||
| String token = resolveTokenFromCookie(request); | ||
|
|
||
| try { | ||
| if (StringUtils.hasText(token) && jwtUtil.validateToken(token)) { | ||
| String email = jwtUtil.getEmail(token); | ||
| UserDetails userDetails = memberDetailsService.loadUserByUsername(email); | ||
|
|
||
| UsernamePasswordAuthenticationToken authentication = | ||
| new UsernamePasswordAuthenticationToken( | ||
| userDetails, | ||
| null, | ||
| userDetails.getAuthorities() | ||
| ); | ||
|
|
||
| SecurityContextHolder.getContext().setAuthentication(authentication); | ||
| } | ||
| } catch (Exception e) { | ||
| SecurityContextHolder.clearContext(); | ||
| } | ||
|
|
||
| filterChain.doFilter(request, response); | ||
| } | ||
|
|
||
| private String resolveTokenFromCookie(HttpServletRequest request) { | ||
| Cookie[] cookies = request.getCookies(); | ||
| if (cookies != null) { | ||
| for (Cookie cookie : cookies) { | ||
| if ("Authorization".equals(cookie.getName())) { | ||
| return cookie.getValue(); | ||
| } | ||
| } | ||
| } | ||
| return null; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package com.mycom.socket.auth.jwt; | ||
|
|
||
| import io.jsonwebtoken.Claims; | ||
| import io.jsonwebtoken.Jwts; | ||
| import io.jsonwebtoken.security.Keys; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| import javax.crypto.SecretKey; | ||
| import java.nio.charset.StandardCharsets; | ||
| import java.util.Date; | ||
|
|
||
| @Component | ||
| public class JWTUtil { | ||
|
|
||
| private final SecretKey secretKey; | ||
|
|
||
| public JWTUtil(@Value("${jwt.secret}") String secret) { | ||
| this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); | ||
| } | ||
|
|
||
| public String createToken(String email) { | ||
| Claims claims = Jwts.claims().subject(email).build(); | ||
| Date now = new Date(); | ||
| // 30๋ถ | ||
| long accessTokenValidityInMilliseconds = 1000 * 60 * 30; | ||
| Date validity = new Date(now.getTime() + accessTokenValidityInMilliseconds); | ||
|
|
||
| return Jwts.builder() | ||
| .claims(claims) | ||
| .issuedAt(now) | ||
| .expiration(validity) | ||
| .signWith(secretKey) | ||
| .compact(); | ||
| } | ||
|
|
||
| public boolean validateToken(String token) { | ||
| try { | ||
| Jwts.parser() | ||
| .verifyWith(secretKey) | ||
| .build() | ||
| .parseSignedClaims(token); | ||
| return true; | ||
| } catch (Exception e) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| public String getEmail(String token) { | ||
| return Jwts.parser() | ||
| .verifyWith(secretKey) | ||
| .build() | ||
| .parseSignedClaims(token) | ||
| .getPayload() | ||
| .getSubject(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package com.mycom.socket.auth.security; | ||
|
|
||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||
| import com.mycom.socket.auth.jwt.JWTUtil; | ||
| import com.mycom.socket.global.dto.ApiResponse; | ||
| import com.mycom.socket.auth.dto.request.LoginRequestDto; | ||
| import com.mycom.socket.auth.dto.response.LoginResponseDto; | ||
| import com.mycom.socket.go_socket.entity.Member; | ||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.http.Cookie; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.MediaType; | ||
| 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.web.authentication.UsernamePasswordAuthenticationFilter; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| @RequiredArgsConstructor | ||
| public class LoginFilter extends UsernamePasswordAuthenticationFilter { | ||
|
|
||
| private final JWTUtil jwtUtil; // JwtProvider ๋์ JWTUtil ์ฌ์ฉ | ||
| private final AuthenticationManager authenticationManager; | ||
| private final ObjectMapper objectMapper; | ||
|
|
||
| @Override | ||
| public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) | ||
| throws AuthenticationException { | ||
| try { | ||
| LoginRequestDto loginRequest = objectMapper.readValue(request.getInputStream(), LoginRequestDto.class); | ||
|
|
||
| UsernamePasswordAuthenticationToken authenticationToken = | ||
| new UsernamePasswordAuthenticationToken(loginRequest.email(), loginRequest.password()); | ||
|
|
||
| return authenticationManager.authenticate(authenticationToken); | ||
|
|
||
| } catch (IOException e) { | ||
| throw new RuntimeException("๋ก๊ทธ์ธ ์์ฒญ ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.", e); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, | ||
| FilterChain chain, Authentication authResult) throws IOException { | ||
| MemberDetails memberDetails = (MemberDetails) authResult.getPrincipal(); | ||
| Member member = memberDetails.getMember(); | ||
|
|
||
| // JWT ํ ํฐ ์์ฑ | ||
| String token = jwtUtil.createToken(member.getEmail()); | ||
|
|
||
| // HTTP Only ์ฟ ํค์ JWT ํ ํฐ ์ ์ฅ | ||
| Cookie cookie = new Cookie("Authorization", token); | ||
| cookie.setHttpOnly(true); // JavaScript์์ ์ ๊ทผ ๋ถ๊ฐ๋ฅํ๊ฒ ์ค์ | ||
| cookie.setSecure(true); // HTTPS์์๋ง ์ ์ก๋๋๋ก ์ค์ | ||
| cookie.setPath("/"); // ๋ชจ๋ ๊ฒฝ๋ก์์ ์ฟ ํค ์ ๊ทผ ๊ฐ๋ฅ | ||
| cookie.setMaxAge(1800); // ์ฟ ํค ๋ง๋ฃ์๊ฐ 30๋ถ | ||
| response.addCookie(cookie); | ||
|
||
|
|
||
| LoginResponseDto loginResponse = new LoginResponseDto( | ||
| member.getEmail(), | ||
| member.getNickname() | ||
| ); | ||
|
|
||
| response.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||
| response.setCharacterEncoding("UTF-8"); | ||
| objectMapper.writeValue(response.getWriter(), ApiResponse.success("๋ก๊ทธ์ธ ์ฑ๊ณต", loginResponse)); | ||
| } | ||
|
|
||
| @Override | ||
| protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, | ||
| AuthenticationException failed) throws IOException { | ||
| response.setStatus(HttpStatus.UNAUTHORIZED.value()); | ||
| response.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||
| response.setCharacterEncoding("UTF-8"); | ||
|
|
||
| String errorMessage = "๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค. ์ด๋ฉ์ผ ๋๋ ๋น๋ฐ๋ฒํธ๋ฅผ ํ์ธํด์ฃผ์ธ์."; | ||
| objectMapper.writeValue(response.getWriter(), ApiResponse.error(errorMessage)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package com.mycom.socket.auth.security; | ||
|
|
||
| import com.mycom.socket.go_socket.entity.Member; | ||
| import lombok.Getter; | ||
| import org.springframework.security.core.GrantedAuthority; | ||
| import org.springframework.security.core.authority.SimpleGrantedAuthority; | ||
| import org.springframework.security.core.userdetails.UserDetails; | ||
|
|
||
| import java.util.Collection; | ||
| import java.util.Collections; | ||
|
|
||
| @Getter | ||
| public class MemberDetails implements UserDetails { | ||
|
|
||
| private final Member member; | ||
|
|
||
| public MemberDetails(Member member) { | ||
| this.member = member; | ||
| } | ||
|
|
||
| @Override | ||
| public Collection<? extends GrantedAuthority> getAuthorities() { | ||
| return Collections.singletonList(new SimpleGrantedAuthority(member.getRole().name())); | ||
| } | ||
|
|
||
| @Override | ||
| public String getPassword() { | ||
| return member.getPassword(); | ||
| } | ||
|
|
||
| @Override | ||
| public String getUsername() { | ||
| return member.getEmail(); // email์ username์ผ๋ก ์ฌ์ฉ | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isAccountNonExpired() { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isAccountNonLocked() { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isCredentialsNonExpired() { | ||
| return true; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isEnabled() { | ||
| return true; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
๐ ๏ธ Refactor suggestion
[KO] ์์ธ ์ฒ๋ฆฌ ๋ก๊น ๋ณด๊ฐ
validateToken๋ฉ์๋์์ Exception ๋ฐ์ ์ ๋จ์ํreturn false๋ก ์ฒ๋ฆฌํ๊ณ ์์ต๋๋ค. ๋ถํ์ํ ํ ํฐ ์ ๊ทผ์ด ๋ฐ๋ณต๋ ๊ฒฝ์ฐ๋ฅผ ๋๋นํด ์ต์ํ์ ์๋ฌ ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๋ ๊ฒ์ด ์ข์ต๋๋ค.} catch (Exception e) { + log.warn("JWT ํ ํฐ ๊ฒ์ฆ ์ค ์์ธ ๋ฐ์: {}", e.getMessage()); return false; }