본문 바로가기

Backend/Spring

[Spring Boot] 회원 가입 & 로그인(4)

반응형
SMALL

이번엔 Security 설정과 Token에 대해 설정하려고 합니다.

 

먼저 패스워드 암호화를 적용하기 위한 Config 파일을 만들었습니다.

 

PasswordEncoderConfig.java

@Configuration
public class PasswordEncoderConfig {

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

 

다음 순서에 따라 작성된 Security 설정입니다.

  1. SecurityFilterChain filterChain(HttpSecurity http) 메서드:
    • 이 메서드는 SecurityFilterChain을 빈으로 등록하고, 필터 체인을 구성하는 역할을 합니다.
  2. http.headers().frameOptions().sameOrigin():
    • X-Frame-Options 헤더를 설정하여, 웹 애플리케이션이 iframe 내에서 보호되도록 합니다. sameOrigin() 옵션은 같은 출처(Origin)에서만 iframe으로 해당 페이지를 렌더링할 수 있도록 제한합니다.
  3. formLogin().disable():
    • 기본 로그인 폼 기능을 비활성화합니다. 이것은 커스텀 로그인 방법을 사용하고 있으므로, Spring Security의 기본 로그인 폼을 사용하지 않겠다는 의미입니다.
  4. csrf().disable():
    • CSRF(Cross-Site Request Forgery) 공격 방지를 비활성화합니다. 이 설정은 테스트 목적 등에서는 비활성화할 수 있지만, 보안상 중요한 애플리케이션에서는 신중하게 고려해야 합니다.
  5. cors().configurationSource(configurationSource()):
    • CORS (Cross-Origin Resource Sharing) 구성을 설정합니다. configurationSource() 메서드를 통해 CORS 구성 정보를 지정합니다.
  6. httpBasic().disable():
    • HTTP Basic 인증을 비활성화합니다.
  7. sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS):
    • 세션 관리를 구성합니다. STATELESS로 설정하면, 애플리케이션은 세션을 생성하지 않고, 상태를 유지하지 않는 상태로 작동합니다. 이는 RESTful API와 같이 상태를 저장하지 않는 애플리케이션에서 사용됩니다.
  8. authorizeRequests():
    • URL 경로에 대한 인가 규칙을 설정합니다. 이 코드에서는 "/api/v1/**" 경로에 대한 요청은 모두 허용하도록 설정되어 있습니다.
  9. exceptionHandling():
    • 예외 처리를 구성합니다. accessDeniedHandler 및 authenticationEntryPoint를 설정합니다. 이것은 권한이 없는 경우와 인증되지 않은 요청에 대한 처리 방법을 정의합니다.
  10. http.addFilterBefore(new JwtAuthFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class):
    • JwtAuthFilter를 UsernamePasswordAuthenticationFilter 앞에 추가합니다. 이 필터는 JWT 토큰을 검증하고 사용자를 인증하는 역할을 합니다.
  11. return http.build():
    • 설정이 완료된 http 객체를 반환하고, 이를 SecurityFilterChain 빈으로 등록합니다.
  12. AuthenticationManage 빈을 생성합니다.
  13. CORS(Cross-Origin Resource Sharing)구성을 설정하는 빈을 생성합니다.

SecuityConfig.java

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final CustomAuthenticationEntryPoint authenticationEntryPoint;
    private final CustomAccessDeniedHandler accessDeniedHandler;
    private final CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
    private final CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    private final CustomOAuth2UserService customOAuth2UserService;
    private final JwtProvider jwtProvider;


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.headers().frameOptions().sameOrigin()
                .and()
                .formLogin().disable()
                .csrf().disable()
                .cors().configurationSource(configurationSource())

                .and()
                .httpBasic().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                    .antMatchers("/api/v1/**").permitAll()

                .and()
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint)

                http.addFilterBefore(new JwtAuthFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception{
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public CorsConfigurationSource configurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of());
        configuration.setAllowCredentials(true);  // 토큰 주고 받을 때
        configuration.addAllowedHeader("*");
        configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PATCH", "PUT", "DELETE", "OPTIONS"));
        configuration.setMaxAge(3600L);

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

}

 

JWT를 사용하여 사용자의 인증 및 권한 부여를 처리하는 필터를 작성 했습니다.

JwtAuthFilter.java

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;

    @Value("${jwt.secret}")
    private String secretKey;

    private static final String TOKEN_PREFIX = "Bearer ";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // request Header에서 accessToken을 가져온다.
        String accessToken = resolveToken(request);

        // 토큰 검사 생략 (모두 허용 URL인 경우 토큰 검사 통과)
        if (!StringUtils.hasText(accessToken)) {
            doFilter(request, response, filterChain);
            return;
        }

        // AccessToken을 검증하고 만료 되었을 경우 예외 발생
        if (!jwtProvider.verifyToken(accessToken)) {
            throw new JwtException(ErrorCode.ACCESS_TOKEN_EXPIRATION);
        }

        // AccessToken 값이 있고, 유효한 경우에만 진행
        if (jwtProvider.verifyToken(accessToken)) {
            // AccessToken 내부 payload에 있는 email로 user를 찾는다 -> 없다면 정상 토큰이 아님 (오류 반환)
            Authentication authentication = jwtProvider.getAuthentication(accessToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }

    // AccessToken 값만 추출
    public String resolveToken(HttpServletRequest httpServletRequest) {
        String bearerToken = httpServletRequest.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) {
            return bearerToken.substring(TOKEN_PREFIX.length());
        }
        return null;
    }
}

 

Jwt 토큰을 위한 JwtPovider를 작성했습니다.

JwtProvider.java

@Service
@RequiredArgsConstructor
public class JwtProvider implements InitializingBean {

    private final UserDetailsService userDetailsService;

    @Value("${jwt.secret}")
    private String secret;
    private Key signingKey;

    private static final String ROLE_KEY = "role";

    // signingKey 생성
    @Override
    public void afterPropertiesSet() throws Exception {
        byte[] secretKeyByte = secret.getBytes();
        signingKey = Keys.hmacShaKeyFor(secretKeyByte);
    }


    public String generateAccessToken(String email, String role) {
        long tokenPeriod = 1000L * 60L * 30L; // 30분

        // 새로운 클레임 객체를 생성, 이메일과 역할 (권한) 설정
        Claims claims = Jwts.claims().setSubject(email);
        claims.put(ROLE_KEY, role);

        Date now = new Date();

        return Jwts.builder()
                // Payload를 구성하는 속성들 정의
                .setClaims(claims)
                // 발행 일자
                .setIssuedAt(now)
                // 토큰 만료 일시
                .setExpiration(new Date(now.getTime() + tokenPeriod))
                // 저장된 서명 알고리즘과 비밀 키로 토큰 서명
                .signWith(signingKey, SignatureAlgorithm.HS256)
                .compact();
    }

    public String generateRefreshToken() {
        long refreshPeriod = 1000L * 60L * 60L * 24L * 14L; // 2주

        Date now = new Date();

        return Jwts.builder()
                .setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + refreshPeriod))
                .signWith(signingKey, SignatureAlgorithm.HS256)
                .compact();
    }


    // 유효 토큰 확인
    public Boolean verifyToken(String token) {
        try {

            Jws<Claims> claims = Jwts.parserBuilder()
                    .setSigningKey(signingKey) // 비밀 키 설정하여 파싱
                    .build().parseClaimsJws(token); // 주어진 토큰 파싱하여 claims 객체 빼오기

            // 토큰 만료 기간과 현재 시간 비교
            return claims.getBody()
                    .getExpiration()
                    .after(new Date());  // 만료 시간이 현재 시간 이후인지 확인하여 유효성 결과를 반환
        } catch (Exception e) {
            return false;
        }
    }

    // 권한 생성
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(getEmail(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // 토큰에서 이메일 추출
    public String getEmail(String token) {

        return Jwts.parserBuilder()
                .setSigningKey(signingKey)
                .build().parseClaimsJws(token)
                .getBody().getSubject();
    }

    // 토큰에서 권한 추출
    public String getTokenRole (String token) {

        return Jwts.parserBuilder()
                .setSigningKey(signingKey)
                .build().parseClaimsJws(token)
                .getBody().get(ROLE_KEY, String.class);
    }

    // AccessToken 만료 시간이 5분 보다 적게 남았는지 확인
    public boolean getExpiration(String token) {
        try {

            Jws<Claims> claims = Jwts.parserBuilder()
                    .setSigningKey(signingKey) // 비밀 키 설정하여 파싱
                    .build().parseClaimsJws(token); // 주어진 토큰 파싱하여 claims 객체 빼오기

            Date expiration = claims.getBody().getExpiration();

            Date now = new Date();
            long timeDiff = expiration.getTime() - now.getTime();
            int fiveMinutes = 1000 * 60 * 5; // 5분

            return timeDiff < fiveMinutes;
        } catch (Exception e) {
            return false;
        }
    }
}

 

여기까지가 자체 로그인 마무리 입니다. 다음 글에서는 소셜 로그인에 대한 코드를 올리도록 하겠습니다.

 

 

[Spring Boot] 회원 가입 & 로그인(3)

로그인을 하기 위해 JWT 토큰을 사용하기로 결정 했습니다. 그래서 그전에 JWT 토큰이 무엇인지 어떻게 사용되는 것인지 먼저 간단하게 알아보려고 합니다. JWT(JSON Web Token)란 인증에 필요한 정보

classruntime.tistory.com

 

[Spring Boot]회원 가입 & 로그인(2)

이번엔 도메인 설정 이후 컨트롤러와 서비스를 만들어 보도록 하겠습니다. 먼저 반환 형식을 맞추려고 합니다. Response { "status" : "success", "message" : "회원 가입 성공", "data" : { "userId" : 1 } } 위와 같

classruntime.tistory.com

 

[Spring Boot]회원 가입 & 로그인 (1)

기본 회원 가입 및 로그인을 구현하면서 소셜 로그인도 같이 병합해서 사용해야겠다는 생각이 들어 만들어 봤습니다. ERD는 쇼핑몰을 생각하면서 만들어 봤습니다. 먼저 기본 회원 가입 ERD 입니

classruntime.tistory.com

 

 

GitHub - Llimy1/Auth_Spring

Contribute to Llimy1/Auth_Spring development by creating an account on GitHub.

github.com

 

반응형
LIST