오늘은 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 |