• Spring Security는 Spring 기반 애플리케이션의 보안을 제공하는 강력하고, 유연한 프레임워크입니다.
특징
• 인증(Authentication)과 인가(Authorization) 기능 제공
• 다양한 보안 공격 방어 (CSRF, XSS 등)
• 확장 가능하고 커스터마이징이 용이
프로젝트 라이브러리
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2'
spring security 뿐 아니라 API 에서 jwt 토큰을 인증 및 인가 하는 기능을 사용하려면 jsonwebtoken 라이브러리도 추가하여야 한다.
인증 개념
(Authentication)
• 인증은 사용자의 신원을 확인하는 과정입니다.
인가 개념
(Authorization)
• 인가는 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정입니다.
JwtRequestFilter
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
private UserDetailsService userDetailsService;
private JwtUtil jwtUtil;
@Autowired
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Autowired
public void setJwtUtil(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
JwtUtil
@Component
public class JwtUtil {
// private String SECRET_KEY = Base64.getEncoder().encodeToString("secret".getBytes());
private final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
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.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(UserDetails userDetails) {
return createToken(userDetails.getUsername());
}
private String createToken(String subject) {
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10시간 유효
.signWith(SECRET_KEY)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
SecurityConfig
@Configuration
public class SecurityConfig {
private final JwtRequestFilter jwtRequestFilter;
//@Lazy 이렇게 하면 JwtRequestFilter 빈의 초기화를 지연시켜 순환 참조 문제를 피할 수 있습니다.
@Autowired
public SecurityConfig(@Lazy JwtRequestFilter jwtRequestFilter) {
this.jwtRequestFilter = jwtRequestFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// http
// .authorizeHttpRequests(authorizeRequests ->
// authorizeRequests
// .requestMatchers("/admin/**").hasRole("ADMIN")
// .requestMatchers("/user/**").hasRole("USER")
// .anyRequest().authenticated()
// )
// • authorizeHttpRequests 메서드를 사용하여 HTTP 요청에 대한 인가(Authorization) 규칙을 설정합니다.
// • requestMatchers("/admin/**").hasRole("ADMIN"): URL 패턴이 /admin/**인 요청은 ADMIN 역할을 가진 사용자만 접근할 수 있도록 설정합니다.
// • requestMatchers("/user/**").hasRole("USER"): URL 패턴이 /user/**인 요청은 USER 역할을 가진 사용자만 접근할 수 있도록 설정합니다.
// • anyRequest().authenticated(): 나머지 모든 요청은 인증된 사용자만 접근할 수 있도록 설정합니다.
// .formLogin(formLogin ->
// formLogin
// .loginPage("/login")
// .permitAll()
// )
// • formLogin 메서드를 사용하여 폼 기반 로그인을 설정합니다.
// • loginPage("/login"): 사용자 정의 로그인 페이지를 설정합니다. 사용자가 /login URL로 접근하면 로그인 페이지가 표시됩니다.
// • permitAll(): 로그인 페이지는 인증되지 않은 사용자도 접근할 수 있도록 허용합니다.
//
// .csrf(csrf -> csrf.disable()); // CSRF 보호 비활성화. 필요한 경우 활성화 가능
// • csrf 메서드를 사용하여 CSRF(Cross-Site Request Forgery) 보호 설정을 합니다.
// • csrf.disable(): CSRF 보호를 비활성화합니다. CSRF 보호가 필요할 경우 이 부분을 제거하거나 다른 방법으로 설정할 수 있습니다.
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
var userDetailsManager = new InMemoryUserDetailsManager();
var user = User.withUsername("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();
var admin = User.withUsername("admin")
.password(passwordEncoder().encode("admin"))
.roles("ADMIN")
.build();
userDetailsManager.createUser(user);
userDetailsManager.createUser(admin);
return userDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Spring Security 설정 파일에서 AuthenticationManager를 빈으로 정의해야 합니다.
* Spring Boot 2.x에서는 AuthenticationManager를 직접 정의하는 것이 필요했지만,
* Spring Boot 3.x에서는 이를 자동으로 구성하지 않으므로 명시적으로 설정해야 합니다.
* @param authenticationConfiguration
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
user 와 password 라는 아이디 및 비밀번호를 이용하면 로그인 가능하다.
AuthController
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public String createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
);
final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails);
return jwt;
}
}
AuthRequest
@Getter
@Setter
public class AuthRequest {
private String username;
private String password;
}
로그인시
이렇게 요청할 경우 JWT 토큰을 응답 받고 응답받은 토큰을 API Request 헤더에
Authtication : Bearar {토큰}
으로 넣어주면 인가를 해주면 됩니다.
인가를 원하지 않으면 SecurityConfig 클래스의 securityFilterChain 에 permit 하는 경로를 추가해 주면 됩니다.
• Spring Security 5.4 이후부터는 WebSecurityConfigurerAdapter 대신 SecurityFilterChain을 사용하여 보안 구성을 합니다.
Spring Security에서 인증 및 인가 구현 시 사용되는 주요 개념들
1. Filter 체인
• 설명: Spring Security는 다양한 필터들의 체인으로 구성됩니다. 각 필터는 Request를 가로챈 후 일련의 절차를 처리합니다.
• 주요 필터: UsernamePasswordAuthenticationFilter는 사용자가 제출한 인증 정보를 처리합니다.
2. UsernamePasswordAuthenticationToken 생성
• 설명: UsernamePasswordAuthenticationFilter는 UsernamePasswordAuthenticationToken을 생성하여 AuthenticationManager에게 전달합니다. 이 토큰에는 사용자가 제출한 인증 정보가 포함되어 있습니다.
3. AuthenticationManager
• 설명: AuthenticationManager는 실제로 인증을 수행하는데, 여러 AuthenticationProvider들을 이용합니다.
4. AuthenticationProvider
• 설명: 각각의 Provider들은 특정 유형의 인증을 처리합니다. 예시로 DaoAuthenticationProvider는 사용자 정보를 데이터베이스에서 가져와 인증을 수행합니다.
5. PasswordEncoder
• 설명: 인증과 인가에서 사용될 패스워드의 인코딩 방식을 지정합니다.
6. UserDetailsService
• 설명: AuthenticationProvider는 UserDetailsService를 사용하여 사용자 정보를 가져옵니다. UserDetailsService는 사용자의 아이디를 받아 loadUserByUsername을 호출하여 해당 사용자의 UserDetails를 반환합니다.
7. UserDetails
• 설명: UserDetails에는 사용자의 아이디, 비밀번호, 권한 등이 포함되어 있습니다.
8. Authentication 객체 생성
• 설명: 인증이 성공하면, AuthenticationProvider는 Authentication 객체를 생성하여 AuthenticationManager에게 반환합니다. 이 Authentication 객체에는 사용자의 세부 정보와 권한이 포함되어 있습니다.
9. SecurityContextHolder
• 설명: 현재 실행 중인 스레드에 대한 SecurityContext를 제공합니다.
10. SecurityContext
• 설명: 현재 사용자의 Authentication이 저장되어 있습니다. 애플리케이션은 SecurityContextHolder를 통해 현재 사용자의 권한을 확인하고, 인가 결정을 합니다.
인증 및 인가 설정에 대한 주요 사항
1. CSRF 보호 비활성화
• 설명: CSRF 토큰을 사용하지 않으므로 확인하지 않도록 설정합니다.
2. CORS 설정 적용
• 설명: 다른 도메인의 웹 페이지에서 리소스에 접근할 수 있도록 허용합니다.
3. 폼 로그인과 HTTP 기본 인증 비활성화
• 설명: Spring 웹 페이지에서 제공되는 로그인 폼을 통해 사용자를 인증하는 메커니즘과 HTTP 기본 인증을 비활성화합니다.
4. JwtAuthFilter 추가
• 설명: UsernamePasswordAuthenticationFilter 앞에 JwtAuthFilter를 추가하여, JWT 필터를 거치도록 설정합니다. JwtAuthFilter를 통해서 Authentication을 획득하였다면 인증된 자원에 접근할 수 있습니다.
5. 권한에 대한 규칙 작성
• 설명: anyRequest().permitAll() 설정은 기본적으로 모두 허용해 주지만, EnableGlobalMethodSecurity를 설정하는 이유는 Annotation으로 접근 제한을 설정하기 위함입니다.
• 추가 설명: 만약 Annotation을 통해 접근 제한을 하지 않을 것이라면, anyRequest() 부분에서 접근 승인할 엔드포인트들을 작성해야 합니다.
6. 인증과 인가 실패 시 Exception Handler 추가
• 설명: Security 단계에서 권한 관련 401이나 403 에러 등을 처리해 줄 핸들러를 함께 등록해줍니다.
• authenticationEntryPoint: 인증되지 않은 사용자에 대해 처리하는 핸들러 정의
• accessDeniedHandler: 인증되었지만, 특정 리소스에 대한 권한이 없는 경우(인가) 처리하는 핸들러 정의
요약
CSRF 보호 비활성화: CSRF 토큰 사용하지 않음.
• CORS 설정: 다른 도메인의 웹 페이지에서 리소스 접근 허용.
• 폼 로그인과 HTTP 기본 인증 비활성화: Spring 웹 페이지의 로그인 폼과 HTTP 기본 인증 비활성화.
• JwtAuthFilter 추가: UsernamePasswordAuthenticationFilter 앞에 JWT 필터 추가.
• 권한 규칙 작성: anyRequest().permitAll() 설정과 EnableGlobalMethodSecurity 설정.
• Exception Handler 추가: 인증과 인가 실패 시 처리할 핸들러 정의.
'Spring > spring boot 및 기타' 카테고리의 다른 글
spring boot 예외처리 핸들러 @ControolerAdvice (0) | 2024.08.22 |
---|---|
스프링 Security 및 JWT 활용하여 DB 유저 테이블 회원가입 로그인 API 만들기 (0) | 2024.06.21 |
spring webflux mono excel dowonlad(poi 라이브러리) (0) | 2023.06.27 |
spring boot 3 r2dbc 설정(mysql) (0) | 2023.06.23 |
spring netty thread sleep, webflux 에서 sleep 어떻게 하는게 나을까 (0) | 2023.06.14 |