스프링 시큐리티와 JWT를 활용한 안전한 인증 및 인가 구현

스프링 시큐리티란?

스프링 시큐리티는 스프링 프레임워크에서 제공하는 보안 프레임워크로, 인증과 인가를 통해 웹 애플리케이션 보안을 구축할 수 있게 해줍니다. 스프링 시큐리티는 다양한 인증 방식을 지원하며, 세션 관리, CSRF 방어, 보안 헤더 추가 등 다양한 보안 기능을 제공합니다.

스프링 시큐리티는 FilterChainProxy를 이용하여 보안 필터 체인을 구성하며, 각 필터는 요청에 대해 인증과 인가를 검사합니다. 스프링 시큐리티는 다양한 인증 제공자(Authentication Provider)를 제공하며, 사용자 데이터베이스, LDAP, OAuth2 등 다양한 인증 방식을 지원합니다.

스프링 시큐리티는 보안 설정을 자바 코드 또는 XML 파일로 작성할 수 있으며, 스프링 부트에서는 자동 설정을 지원하여 보다 쉽게 보안 설정을 구현할 수 있습니다.

Spring Security

JWT의 개념과 구성

JWT(Json Web Token)는 JSON 형태로 인증 정보를 전송하기 위한 개방형 표준입니다. JWT는 Header, Payload, Signature 세 부분으로 구성되어 있으며, Header는 토큰의 유형과 해싱 알고리즘, Payload는 클레임(Claim) 정보, Signature는 토큰의 유효성 검증을 위한 서명 정보를 담고 있습니다.

클레임(Claim)은 JWT에 담긴 정보를 의미하며, Registered Claim, Public Claim, Private Claim으로 구분됩니다. Registered Claim은 JWT에 대한 정보를 담고 있으며, Public Claim은 자유롭게 정의할 수 있는 정보를 담고 있으며, Private Claim은 서비스에서 정의한 정보를 담고 있습니다.

JWT는 인증 및 인가에 대한 정보를 안전하게 전송하기 위해 사용됩니다. JWT는 서버에서 발급되며, 클라이언트에서는 JWT를 저장하여 서버와의 통신 시 인증 정보를 전송합니다.

JWT

안전한 인증 및 인가 구현 방법

스프링 시큐리티와 JWT를 활용하여 안전한 인증 및 인가 구현 방법을 소개합니다.

1. JWT 발급

JWT를 발급하기 위해서는 서버에서 JWT를 생성하고, 클라이언트에게 전송해야 합니다. 스프링 시큐리티에서는 JWT 생성을 위해 io.jsonwebtoken 패키지의 Jwts 클래스를 사용할 수 있습니다.

String token = Jwts.builder()
    .setSubject("user")
    .setIssuedAt(new Date())
    .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
    .signWith(SignatureAlgorithm.HS512, "secret")
    .compact();

위 코드에서 setSubject 메서드는 클레임 중 하나인 sub을 설정하며, setIssuedAt 메서드는 클레임 중 하나인 iat을 설정합니다. setExpiration 메서드는 클레임 중 하나인 exp을 설정하며, 토큰의 만료 시간을 설정합니다. signWith 메서드는 Signature 정보를 설정하며, HS512 알고리즘과 secret 키를 사용하여 서명합니다.

2. JWT 검증

클라이언트에서 JWT를 받아 서버로 전송한 후, 서버에서는 JWT의 유효성을 검증해야 합니다. 스프링 시큐리티에서는 JWT 검증을 위해 io.jsonwebtoken 패키지의 Jwts 클래스를 사용할 수 있습니다.

try {
    Jwts.parser().setSigningKey("secret").parseClaimsJws(token);
    // Valid token
} catch (JwtException e) {
    // Invalid token
}

위 코드에서 setSigningKey 메서드는 Signature 정보를 설정하며, parseClaimsJws 메서드를 통해 토큰을 검증합니다.

3. 스프링 시큐리티 설정

스프링 시큐리티에서 JWT를 사용하기 위해서는 스프링 시큐리티 설정에서 JwtAuthenticationTokenFilter를 등록해야 합니다.

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .authorizeRequests()
        .antMatchers("/api/authenticate").permitAll()
        .anyRequest().authenticated()
        .and()
        .addFilterBefore(new JwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}

위 코드에서 addFilterBefore 메서드를 이용하여 JwtAuthenticationTokenFilter를 등록합니다.

4. JWT 인증

JWT 인증을 위해서는 AuthenticationProvider를 구현해야 합니다. AuthenticationProviderauthenticate 메서드를 구현하여 인증을 처리합니다.

@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
        String token = jwtAuthenticationToken.getToken();

        try {
            Jwts.parser().setSigningKey("secret").parseClaimsJws(token);
            return new JwtAuthenticationToken(token);
        } catch (JwtException e) {
            throw new BadCredentialsException("Invalid JWT token", e);
        }
    }

    @Override
    public boolean supports(Class authentication) {
        return JwtAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

위 코드에서 authenticate 메서드는 JWT 검증을 수행하고, JwtAuthenticationToken을 반환합니다. supports 메서드는 JwtAuthenticationToken을 지원하는지 확인합니다.

5. 인가 처리

인가 처리를 위해서는 WebSecurityConfigurerAdapter를 상속받은 클래스에서 configure 메서드를 오버라이드하여 인가 처리를 구현합니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/authenticate").permitAll()
            .antMatchers(HttpMethod.GET, "/api/users/**").hasRole("USER")
            .antMatchers(HttpMethod.POST, "/api/users/**").hasRole("ADMIN")
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(new JwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(jwtAuthenticationProvider());
    }

    @Bean
    public JwtAuthenticationProvider jwtAuthenticationProvider() {
        return new JwtAuthenticationProvider();
    }
}

위 코드에서 antMatchers 메서드를 이용하여 URL 패턴과 권한을 설정합니다.

6. 사용자 인증

사용자 인증을 위해서는 UserDetailsService를 구현해야 합니다. UserDetailsServiceloadUserByUsername 메서드를 구현하여 사용자 정보를 조회합니다.

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));

        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
                new ArrayList());
    }
}

위 코드에서 loadUserByUsername 메서드는 UserRepository를 이용하여 사용자 정보를 조회하고, UserDetails 객체를 반환합니다.

7. 사용자 등록

사용자 등록을 위해서는 UserRepository를 구현해야 합니다. UserRepository는 사용자 정보를 저장하고 조회하는 메서드를 구현합니다.

public interface UserRepository extends JpaRepository {
    Optional findByUsername(String username);
}

위 코드에서 findByUsername 메서드는 사용자 이름으로 사용자 정보를 조회합니다.

8. 로그인 API

로그인 API를 구현하기 위해서는 AuthenticationManager를 이용하여 인증을 수행해야 합니다.

@PostMapping("/authenticate")
public ResponseEntity authenticate(@RequestBody LoginRequest loginRequest) {
    Authentication authentication = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));

    SecurityContextHolder.getContext().setAuthentication(authentication);
    String token = jwtTokenProvider.createToken(authentication);

    return ResponseEntity.ok(new JwtAuthenticationResponse(token));
}

위 코드에서 authenticationManager는 스프링 시큐리티에서 제공하는 인증 매니저입니다. UsernamePasswordAuthenticationToken을 이용하여 인증 정보를 생성합니다. 인증에 성공하면 JWT 토큰을 생성하여 반환합니다.

9. JWT 토큰 생성

JWT 토큰 생성을 위해서는 JwtTokenProvider 클래스를 구현해야 합니다.

@Component
public class JwtTokenProvider {

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

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

    public String createToken(Authentication authentication) {
        User user = (User) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);

        return Jwts.builder()
            .setSubject(user.getUsername())
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }
}

위 코드에서 createToken 메서드는 Authentication 객체에서 사용자 정보를 추출하고, JWT 토큰을 생성합니다.

10. JWT 토큰 추출

JWT 토큰 추출을 위해서는 JwtAuthenticationTokenFilter 클래스를 구현해야 합니다.

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Autowired
    private UserService userService;

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

        if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
            String username = jwtTokenProvider.getUsernameFromJWT(jwt);
            UserDetails userDetails = userService.loadUserByUsername(username);
            JwtAuthenticationToken authentication = new JwtAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            authentication.setToken(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7, bearerToken.length());
        }
        return null;
    }
}

위 코드에서 doFilterInternal 메서드는 JWT 토큰을 추출하고, 검증하여 인증 정보를 생성합니다. getJwtFromRequest 메서드는 HTTP 요청 헤더에서 JWT 토큰을 추출합니다.

결론

스프링 시큐리티와 JWT를 활용하여 안전한 인증 및 인가 구현 방법을 소개했습니다. JWT를 사용하면 서버와 클라이언트 간의 인증 정보를 안전하게 전송할 수 있으며, 스프링 시큐리티를 이용하여 인증 및 인가 처리를 수행할 수 있습니다. 이를 통해 보다 안전한 웹 애플리케이션을 구현할 수 있습니다.

스프링 시큐리티와 JWT를 활용한 안전한 인증 및 인가 구현

스프링 시큐리티란 무엇인가?

스프링 시큐리티는 스프링 프레임워크에서 제공하는 보안 프레임워크로, 웹 어플리케이션에서 인증과 인가를 구현하는데 사용된다. 스프링 시큐리티는 사용자 인증, 권한 부여 및 보안 보호를 위한 다양한 기능을 제공한다. 이를 통해 웹 어플리케이션에서 안전하게 사용자 인증 및 인가를 구현할 수 있다.

스프링 시큐리티는 다양한 인증 및 인가 방식을 제공한다. 사용자 인증 방식으로는 폼 인증, 기본 인증, OAuth2 인증 등이 있으며, 인가 방식으로는 URL 기반 인가, 메소드 기반 인가, 표현식 기반 인가 등이 있다. 이러한 다양한 방식을 제공하여 웹 어플리케이션에서 필요한 보안 요구사항을 충족할 수 있다.

스프링 시큐리티는 다양한 모듈로 구성되어 있다. 가장 기본적인 모듈은 Core 모듈로, 인증 및 인가를 구현하는데 필요한 기본적인 기능을 제공한다. 이외에도 LDAP, OAuth2, OpenID 등의 모듈이 존재하여 다양한 인증 및 인가 방식을 구현할 수 있다.

스프링 시큐리티는 다른 스프링 프레임워크 모듈과 연동하여 사용할 수 있다. 스프링 프레임워크에서 제공하는 IoC(Inversion of Control) 기능을 사용하여 스프링 시큐리티를 적용할 수 있다. 이를 통해 스프링 기반의 웹 어플리케이션에서 쉽게 보안 기능을 적용할 수 있다.

Spring Security

JWT를 이용한 안전한 인증 및 인가 방법

JWT(Json Web Token)는 웹 어플리케이션에서 인증 정보를 안전하게 전송하기 위한 방법 중 하나이다. JWT는 JSON 형태로 구성되어 있으며, Header, Payload, Signature 세 부분으로 구성된다. Header는 토큰의 유형과 사용하는 알고리즘 등의 정보를 담고 있으며, Payload는 토큰에 포함되는 클레임(Claim) 정보를 담고 있다. Signature는 토큰의 유효성 검증에 사용되는 정보이다.

JWT는 클라이언트에서 서버로 전송하는 인증 정보가 토큰 형태로 전송되기 때문에 중간에 탈취되어도 복호화가 불가능하다는 장점이 있다. 또한, 서버 측에서는 토큰에 포함된 정보만으로 사용자 인증 및 인가를 수행할 수 있기 때문에 DB에 저장된 사용자 정보를 조회하는 등의 작업이 필요하지 않아 성능이 향상된다.

JWT를 사용하여 인증을 구현할 경우, 클라이언트에서 로그인 요청을 보내면 서버에서는 JWT를 발급하여 클라이언트에게 전송한다. 이후 클라이언트는 요청 시 발급받은 JWT를 헤더에 포함하여 전송하면 서버에서는 해당 토큰의 유효성을 검증하여 사용자 인증 및 인가를 수행한다.

JWT

스프링 시큐리티와 JWT를 결합한 안전한 웹 어플리케이션 구현 방법

스프링 시큐리티와 JWT를 결합하여 안전한 웹 어플리케이션을 구현하는 방법은 다음과 같다.

1. 스프링 시큐리티 설정

스프링 시큐리티 설정 클래스에서 JWT를 사용하기 위한 Filter를 등록한다. 이때, JWT를 생성하고 검증하는 기능을 수행하는 JWTUtil 클래스를 함께 등록한다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

    @Autowired
    private JwtRequestFilter jwtRequestFilter;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        // configure AuthenticationManagerBuilder
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // configure HttpSecurity
        httpSecurity.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

2. JWT 생성

로그인 요청이 들어온 경우, JWTUtil 클래스에서 JWT를 생성하고 클라이언트에게 전송한다.

@Service
public class JwtUtil {

    private String secret = "mysecretkey";

    public String generateToken(UserDetails userDetails) {
        Map claims = new HashMap();
        // set claims
        return Jwts.builder().setClaims(claims).setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        // validate token
    }
}

3. JWT 검증 및 사용자 인증

클라이언트에서 요청이 들어온 경우, JWTRequestFilter 클래스에서 헤더에서 JWT를 추출하여 검증하고, 검증에 성공한 경우 SecurityContextHolder를 사용하여 사용자 인증 정보를 설정한다.

public class JwtRequestFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

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

        // get JWT from header

        // validate JWT

        // set authentication
        final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                userDetails, null, userDetails.getAuthorities());
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }
}

위와 같이 스프링 시큐리티와 JWT를 결합하여 안전한 웹 어플리케이션을 구현할 수 있다.

Spring Security JWT