Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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;
}
}
60 changes: 60 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,60 @@
package com.mycom.socket.auth.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
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
@Slf4j
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) {
log.warn("JWT ํ† ํฐ ๊ฒ€์ฆ ์ค‘ ์—๋Ÿฌ ๋ฐœ์ƒ: {}", e.getMessage());
return false;
}
}

public String getEmail(String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
}
85 changes: 85 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,85 @@
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);
}
}

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException {
MemberDetails memberDetails = (MemberDetails) authResult.getPrincipal();
Member member = memberDetails.getMember();

String token = jwtUtil.createToken(member.getEmail());

// HTTP Only ์ฟ ํ‚ค์— JWT ํ† ํฐ ์ €์žฅ
Cookie cookie = new Cookie("Authorization", token);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(1800); // ์ฟ ํ‚ค ๋งŒ๋ฃŒ์‹œ๊ฐ„ 30๋ถ„

// SameSite ์†์„ฑ ์„ค์ • ์ถ”๊ฐ€
response.setHeader("Set-Cookie",
String.format("Authorization=%s; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=1800", token));

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