[Venus] Team Project - 인증/인가 구현 (1)

0

LikeLion_Backend_Plus

목록 보기
1/1

백엔드 (Spring Boot & Spring Security)

개발 환경

Frontend : Next.js 15 (React 19, React-dom 19), Next-Auth
Backend : Spring Boot 3.x.x, Spring Security

의존성 추가 (build.gradle)

build.gradle.kts

dependencies {
    // Spring Security
    implementation 'org.springframework.boot:spring-boot-starter-security'

    // Spring Web
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // JPA (데이터베이스 연동 - 필요에 따라)
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // OAuth 2.0 Client
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

    // Lombok (선택 사항, 코드 간결화)
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    // JSON Web Token (JWT)
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

    // Google Client Libraries
    implementation 'com.google.api-client:google-api-client:1.32.1'
    implementation 'com.google.oauth-client:google-oauth-client:1.34.1'

    // 데이터베이스 드라이버 (예: H2, MySQL, PostgreSQL)
    runtimeOnly 'com.h2database:h2' // 예시로 H2 데이터베이스 사용
}

User 엔티티 생성

package com.example.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.util.List;

@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String email;
    private String password;
    private String provider; // 인증 제공자 (google, local 등)
    private String providerId; // provider 에서 제공하는 고유 id
    @ElementCollection(fetch = FetchType.EAGER)
    private List<String> roles; // 사용자 역할
}

UserRepository 생성

package com.example.auth.repository;

import com.example.auth.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    Optional<User> findByProviderAndProviderId(String provider, String providerId);

}

SecurityConfig 설정

// SecurityConfig.java
package com.example.auth.config;

import com.example.auth.security.JwtAuthenticationFilter;
import com.example.auth.security.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;


@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtTokenProvider jwtTokenProvider;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // BCryptPasswordEncoder 사용
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            // CSRF 비활성화 (JWT 사용 시 불필요)
            .csrf((csrf) -> csrf.disable())
            // 세션 사용 안함
            .sessionManagement(session -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests((auth) -> auth
                    .requestMatchers("/auth/register", "/auth/login", "/auth/google", "/auth/google/callback").permitAll()  // 모든 사용자 허용
                    .anyRequest().authenticated()  // 나머지 요청은 인증 필요
            )
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();

        // CORS 설정
        config.setAllowedOrigins(List.of("http://localhost:3000")); // 허용할 오리진
        config.setAllowedMethods(List.of("GET","POST", "PUT", "DELETE", "PATCH", "OPTIONS")); // 허용할 HTTP 메서드
        config.setAllowedHeaders(List.of("*")); // 허용할 헤더
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

JwtTokenProvider 클래스

package com.example.auth.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Date;
import java.util.List;

@Component
public class JwtTokenProvider {
    @Value("${jwt.secret}")
    private String secretKey;

    @Value("${jwt.expiration}")
    private long tokenValidTime;

    private SecretKey key;

    @PostConstruct
    public void init(){
        this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
    }

    // JWT 토큰 생성
    public String createToken(String email, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(email);
        claims.put("roles", roles);

        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + tokenValidTime))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
    }

    // JWT 토큰에서 인증 정보 조회
    public Authentication getAuthentication(String token) {
        Claims claims = Jwts.parserBuilder()
                            .setSigningKey(key)
                            .build()
                            .parseClaimsJws(token)
                            .getBody();
        String email = claims.getSubject();
        List<String> roles = (List<String>)claims.get("roles");
        List<SimpleGrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new).toList();

        return new UsernamePasswordAuthenticationToken(email, "", authorities);
    }


    // 토큰에서 이메일 추출
    public String getEmailFromToken(String token) {
         return Jwts.parserBuilder()
                 .setSigningKey(key)
                 .build()
                 .parseClaimsJws(token)
                 .getBody()
                 .getSubject();
    }


    // JWT 토큰 유효성 확인
    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parserBuilder()
                                        .setSigningKey(key)
                                        .build()
                                        .parseClaimsJws(token);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

JwtAuthenticationFilter 클래스

package com.example.auth.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
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = resolveToken(request);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

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

AuthController

package com.example.auth.controller;

import com.example.auth.dto.LoginDto;
import com.example.auth.dto.RegisterDto;
import com.example.auth.entity.User;
import com.example.auth.security.JwtTokenProvider;
import com.example.auth.service.UserService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthenticationController {

    private final UserService userService;
    private final PasswordEncoder passwordEncoder;
    private final JwtTokenProvider jwtTokenProvider;

    @Value("${google.client-id}")
    private String googleClientId;
    @Value("${google.client-secret}")
    private String googleClientSecret;
    @Value("${google.redirect-uri}")
    private String googleRedirectUri;

    @PostMapping("/register")
    public ResponseEntity<String> register(@RequestBody RegisterDto registerDto) {
        User user = User.builder()
                .email(registerDto.getEmail())
                .password(passwordEncoder.encode(registerDto.getPassword()))
                .roles(List.of("ROLE_USER"))
                .provider("local")
                .build();
        userService.register(user);
        return new ResponseEntity<>("User registered successfully", HttpStatus.CREATED);
    }

    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody LoginDto loginDto) {
        User user = userService.getUserByEmail(loginDto.getEmail()).orElseThrow(()->new IllegalArgumentException("등록되지 않은 유저입니다."));
        if (!passwordEncoder.matches(loginDto.getPassword(), user.getPassword())) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }
        String token = jwtTokenProvider.createToken(user.getEmail(), user.getRoles());
        return new ResponseEntity<>(token, HttpStatus.OK);
    }

    @GetMapping("/google")
    public void googleLogin(HttpServletResponse response) throws IOException {

        String reqUrl = UriComponentsBuilder.fromUriString("https://accounts.google.com/o/oauth2/v2/auth")
            .queryParam("client_id", googleClientId)
            .queryParam("redirect_uri", googleRedirectUri)
            .queryParam("response_type", "code")
            .queryParam("scope", "email profile")
            .build()
            .toUriString();

        response.sendRedirect(reqUrl);
    }

    @GetMapping("/google/callback")
    public ResponseEntity<String> googleCallback(@RequestParam String code) throws IOException {
        RestTemplate restTemplate = new RestTemplate();
        String tokenUrl = UriComponentsBuilder.fromUriString("https://oauth2.googleapis.com/token")
            .queryParam("code", code)
            .queryParam("client_id", googleClientId)
            .queryParam("client_secret", googleClientSecret)
            .queryParam("redirect_uri", googleRedirectUri)
            .queryParam("grant_type", "authorization_code")
            .build()
            .toUriString();

        ResponseEntity<String> tokenResponse = restTemplate.postForEntity(tokenUrl, null, String.class);

        ObjectMapper objectMapper = new ObjectMapper();
        JsonNode tokenJson = objectMapper.readTree(tokenResponse.getBody());
        String accessToken = tokenJson.get("access_token").asText();

        String userInfoUrl = UriComponentsBuilder.fromUriString("https://www.googleapis.com/oauth2/v2/userinfo")
            .queryParam("access_token", accessToken)
            .build()
            .toUriString();

        ResponseEntity<String> userInfoResponse = restTemplate.getForEntity(userInfoUrl, String.class);

        JsonNode userInfoJson = objectMapper.readTree(userInfoResponse.getBody());
        String email = userInfoJson.get("email").asText();
        String googleId = userInfoJson.get("id").asText();

        User user = userService.getUserByProviderAndProviderId("google", googleId).orElse(null);

        if(user == null) {
            User newUser = User.builder()
                    .email(email)
                    .provider("google")
                    .providerId(googleId)
                    .roles(List.of("ROLE_USER"))
                    .build();
            userService.register(newUser);
             return new ResponseEntity<>(jwtTokenProvider.createToken(email, newUser.getRoles()), HttpStatus.OK);
        }
         return new ResponseEntity<>(jwtTokenProvider.createToken(user.getEmail(), user.getRoles()), HttpStatus.OK);
    }
}

UserService 인터페이스 및 구현

UserService.java

package com.example.auth.service;

import com.example.auth.entity.User;

import java.util.Optional;

public interface UserService {
    void register(User user);
    Optional<User> getUserByEmail(String email);
    Optional<User> getUserByProviderAndProviderId(String provider, String providerId);

}

UserServiceImpl.java

// UserServiceImpl.java
package com.example.auth.service;

import com.example.auth.entity.User;
import com.example.auth.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
    private final UserRepository userRepository;
    @Override
    public void register(User user) {
        userRepository.save(user);
    }

    @Override
    public Optional<User> getUserByEmail(String email) {
        return userRepository.findByEmail(email);
    }

    @Override
    public Optional<User> getUserByProviderAndProviderId(String provider, String providerId) {
        return userRepository.findByProviderAndProviderId(provider,providerId);
    }
}

DTO

SignupDto.java

package com.example.demo.dto;

import lombok.Data;

@Data
public class SignupDto {
    private String email;
    private String password;
}

SigninDto.java

package com.example.demo.dto;

import lombok.Data;

@Data
public class SigninDto {
    private String email;
    private String password;
}
profile
꿈을 계속 간직하고 있으면 반드시 실현할 때가 온다. - 괴테.

0개의 댓글