스프링 시큐리티란?
스프링 시큐리티는 스프링 프레임워크에서 제공하는 보안 프레임워크로, 인증과 인가를 통해 웹 애플리케이션 보안을 구축할 수 있게 해줍니다. 스프링 시큐리티는 다양한 인증 방식을 지원하며, 세션 관리, CSRF 방어, 보안 헤더 추가 등 다양한 보안 기능을 제공합니다.
스프링 시큐리티는 FilterChainProxy를 이용하여 보안 필터 체인을 구성하며, 각 필터는 요청에 대해 인증과 인가를 검사합니다. 스프링 시큐리티는 다양한 인증 제공자(Authentication Provider)를 제공하며, 사용자 데이터베이스, LDAP, OAuth2 등 다양한 인증 방식을 지원합니다.
스프링 시큐리티는 보안 설정을 자바 코드 또는 XML 파일로 작성할 수 있으며, 스프링 부트에서는 자동 설정을 지원하여 보다 쉽게 보안 설정을 구현할 수 있습니다.
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를 활용하여 안전한 인증 및 인가 구현 방법을 소개합니다.
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
를 구현해야 합니다. AuthenticationProvider
는 authenticate
메서드를 구현하여 인증을 처리합니다.
@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
를 구현해야 합니다. UserDetailsService
는 loadUserByUsername
메서드를 구현하여 사용자 정보를 조회합니다.
@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를 사용하면 서버와 클라이언트 간의 인증 정보를 안전하게 전송할 수 있으며, 스프링 시큐리티를 이용하여 인증 및 인가 처리를 수행할 수 있습니다. 이를 통해 보다 안전한 웹 애플리케이션을 구현할 수 있습니다.