Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ dependencies {
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}

test {
Expand Down
14 changes: 12 additions & 2 deletions src/main/java/com/mycom/socket/auth/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.mycom.socket.auth.config;

import com.mycom.socket.auth.jwt.JWTFilter;
import com.mycom.socket.auth.jwt.JWTUtil;
import com.mycom.socket.auth.service.MemberDetailsService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -8,26 +11,33 @@
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig{

private final JWTUtil jwtUtil;
private final MemberDetailsService memberDetailsService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues()))
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)

.addFilterBefore(new JWTFilter(jwtUtil, memberDetailsService), UsernamePasswordAuthenticationFilter.class)

.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/","/api/auth/**"
"/","/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**"
).permitAll()
.anyRequest()
.authenticated());
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/com/mycom/socket/auth/controller/AuthController.java
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);
}
}
Empty file.
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
@@ -1,10 +1,10 @@
package com.mycom.socket.go_socket.dto.request;
package com.mycom.socket.auth.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record MemberRegisterDto(
public record RegisterRequestDto(
@NotBlank(message = "์ด๋ฉ”์ผ์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค")
@Email(message = "์˜ฌ๋ฐ”๋ฅธ ์ด๋ฉ”์ผ ํ˜•์‹์ด ์•„๋‹™๋‹ˆ๋‹ค")
String email,
Expand Down
Empty file.
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);
}
}
62 changes: 62 additions & 0 deletions src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java
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;
}
}
57 changes: 57 additions & 0 deletions src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java
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;
}
}
Copy link

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;
}

Committable suggestion skipped: line range outside the PR's diff.


public String getEmail(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
}
84 changes: 84 additions & 0 deletions src/main/java/com/mycom/socket/auth/security/LoginFilter.java
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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue

JWT ํ† ํฐ ์ฟ ํ‚ค ์„ค์ • ์‹œ ์ทจ์•ฝ์  ํ™•์ธ
Authorization ์ฟ ํ‚ค์— JWT๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋Š”๋ฐ, ํ”„๋ก ํŠธ์—”๋“œ์—์„œ CSRF ๊ณต๊ฒฉ์— ๋Œ€ํ•ด ๋Œ€๋น„ํ•˜๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. HTTP-Only, Secure ์„ค์ •์€ ์ž˜ ๋˜์–ด ์žˆ์œผ๋‚˜, ๋ณด์•ˆ ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ SameSite ์„ค์ •(SameSite=Strict ๋˜๋Š” Lax)์„ ๊ณ ๋ คํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.


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));
}
}
55 changes: 55 additions & 0 deletions src/main/java/com/mycom/socket/auth/security/MemberDetails.java
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;
}
}
Loading
Loading