본문 바로가기
JAVA

Java Spring Security

by 얍얍1234 2024. 6. 22.

오늘은 Java Spring Security에 대해서 이야기해볼게요. 요즘 웹 애플리케이션 개발할 때 보안은 정말 중요한 부분이잖아요? Spring Security는 이런 보안 문제를 해결해주는 정말 강력한 프레임워크예요.

 

1. Spring Security란?

Spring Security는 Spring Framework 기반의 애플리케이션에서 인증(Authentication)과 인가(Authorization)를 담당하는 보안 프레임워크예요. 쉽게 말해서, 누가 내 애플리케이션에 접근할 수 있는지, 그리고 어떤 권한을 가지고 있는지를 관리해주는 도구라고 생각하면 돼요. 이걸 사용하면 우리가 직접 보안 로직을 구현하지 않아도 되고, 보안성 높은 애플리케이션을 만들 수 있어요.


2. Spring Security의 주요 기능

Spring Security가 제공하는 주요 기능들을 간단하게 정리해볼게요.

- 인증(Authentication): 사용자가 누구인지 확인하는 과정이에요. 보통 아이디와 비밀번호를 통해 이루어지지만, OAuth2, JWT, 소셜 로그인 등 다양한 방법을 지원해요.
- 인가(Authorization): 사용자가 어떤 권한을 가지고 있는지 확인하는 과정이에요. 예를 들어, 관리자만 특정 페이지에 접근할 수 있게 하거나, 특정 API를 호출할 수 있게 하는 거죠.
- 보안 컨텍스트(Security Context): 현재 사용자의 보안 정보를 담고 있는 컨텍스트를 제공해요. 이걸 통해 애플리케이션 내에서 언제든지 사용자 정보를 확인할 수 있어요.
- 세션 관리: 사용자의 세션을 관리하고, 세션 고정 공격(Session Fixation Attack)을 방지하는 기능도 제공해요.
- 비밀번호 저장: 안전하게 비밀번호를 저장하고, 해싱하는 기능도 지원해요.

 


3. Spring Security 설정하기

Spring Security를 설정하는 방법을 알아볼게요. 먼저, Spring Boot 프로젝트를 만들고, 필요한 의존성을 추가해야 해요.

<!-- build.gradle -->
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // 기타 필요한 의존성들
}




이제 보안 설정 클래스를 만들어볼게요. `WebSecurityConfigurerAdapter`를 상속받아 설정을 진행하면 돼요.

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()  // 이 경로는 모두 접근 가능
                .anyRequest().authenticated()  // 나머지 요청은 인증 필요
                .and()
            .formLogin()
                .loginPage("/login")  // 로그인 페이지 설정
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }
}



위 코드에서는 기본적으로 모든 요청을 인증된 사용자만 접근할 수 있게 하고, `/`와 `/home` 경로는 모두에게 허용했어요. 그리고 로그인 페이지를 `/login`으로 설정했어요.

 


4. 사용자 인증 처리

Spring Security에서 사용자를 인증하는 방법은 여러 가지가 있어요. 가장 기본적인 방법은 메모리 상에 사용자를 설정하는 거예요.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("user").password("{noop}password").roles("USER")
            .and()
            .withUser("admin").password("{noop}admin").roles("ADMIN");
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        UserDetails user =
             User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build();

        UserDetails admin =
             User.withDefaultPasswordEncoder()
                .username("admin")
                .password("admin")
                .roles("ADMIN")
                .build();

        return new InMemoryUserDetailsManager(user, admin);
    }
}


위 코드에서는 메모리 상에 `user`와 `admin` 두 사용자를 설정했어요. 이렇게 하면 간단하게 인증 기능을 구현할 수 있지만, 실제 애플리케이션에서는 데이터베이스나 외부 인증 서버를 사용하는 게 일반적이에요.

 


5. 커스텀 로그인 페이지 만들기

 

기본 로그인 페이지 대신 커스텀 로그인 페이지를 만들고 싶다면, 로그인 폼을 직접 만들어야 해요. `login.html` 파일을 만들어볼게요.

<!-- src/main/resources/templates/login.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <form th:action="@{/login}" method="post">
        <div>
            <label>Username:</label>
            <input type="text" name="username"/>
        </div>
        <div>
            <label>Password:</label>
            <input type="password" name="password"/>
        </div>
        <div>
            <button type="submit">Login</button>
        </div>
    </form>
</body>
</html>



그리고 보안 설정 클래스에서 커스텀 로그인 페이지를 사용하도록 설정해줘요.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/", "/home", "/login").permitAll()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/login")
            .permitAll()
            .and()
        .logout()
            .permitAll();
}


이제 `/login` 경로로 접근하면 우리가 만든 로그인 페이지가 보여질 거예요.

 

6. OAuth2와 JWT 통합

마지막으로, OAuth2와 JWT를 사용해 좀 더 고급 보안을 설정하는 방법을 간단히 소개할게요. 요즘에는 소셜 로그인이나 JWT 토큰을 많이 사용하니까요.

 

OAuth2 로그인 설정

OAuth2 로그인을 설정하려면 `spring-boot-starter-oauth2-client` 의존성을 추가해야 해요.

<!-- build.gradle -->
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}



그리고 application.yml 파일에 OAuth2 클라이언트 설정을 추가해요.

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: YOUR_CLIENT_ID
            client-secret: YOUR_CLIENT_SECRET
            scope: profile, email
        provider:
          google:
            authorization-uri: https://accounts.google.com/o/oauth2/auth
            token-uri: https://oauth2.googleapis.com/token
            user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
            user-name-attribute: sub



이제 보안 설정 클래스에서 OAuth2 로그인을 활성화해요.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/", "/home", "/login", "/oauth2/**").permitAll()
            .anyRequest().authenticated()
            .and()
        .oauth2Login()
            .loginPage("/login")
            .defaultSuccessUrl("/home", true)
            .failureUrl("/login?error=true")
            .permitAll();
}

 


JWT 설정

JWT(JSON Web Token)를 사용하려면 `spring-boot-starter-security`와 `jjwt` 라이브러리를 추가해야 해요.

<!-- build.gradle -->
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
}



그리고 JWT 토큰을 생성하고 검증하는 유틸리티 클래스를 만들어야 해요.

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class JwtTokenUtil {

    private String SECRET_KEY = "secret";

    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    private Claims extractAllClaims(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
    }

    private Boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }

    public String generateToken(Authentication authentication) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, authentication.getName());
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY).compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}



그리고 JWT 필터를 만들어야 해요.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.ExpiredJwtException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        final String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken = null;

        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.extractUsername(jwtToken);
            } catch (IllegalArgumentException e) {
                System.out.println("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                System.out.println("JWT Token has expired");
            }
        } else {
            logger.warn("JWT Token does not begin with Bearer String");
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities());
                usernamePasswordAuthenticationToken
                        .setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
            }
        }
        chain.doFilter(request, response);
    }
}



마지막으로, 보안 설정 클래스에 JWT 필터를 추가해줘요.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests().antMatchers("/authenticate", "/register").permitAll().
            anyRequest().authenticated().and().
            exceptionHandling().and().sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}



이제 JWT 기반 인증을 통해 좀 더 안전하게 애플리케이션을 보호할 수 있어요.

 

마무리

오늘은 Spring Security를 사용해 웹 애플리케이션의 보안을 강화하는 방법에 대해 알아봤어요. 인증과 인가부터 시작해서, OAuth2와 JWT까지 다양한 기능들을 다뤄봤는데요, 실제로 사용해보면 더 많은 기능과 유용함을 느낄 수 있을 거예요.

'JAVA' 카테고리의 다른 글

Java 이너클래스  (0) 2024.06.24
Java 클래스와 객체  (0) 2024.06.23
Java 연산자  (1) 2024.06.16
Java 자료형  (0) 2024.06.10
WebFlux: 반응형 프로그래밍을 이용한 비동기 웹 프레임워크  (0) 2024.06.09