Authentication
은 principal
의 identity
를 증명하는 과정
principal
는 유저, 기기, 시스템 등, 보통 유저(사용자)를 의미
principal
는 자신을 인증해달라고 신원 증명 정보, credential
를 제시
principal
가 유저일 경우 credential
은 대개 패스워드
Authorization
은 인증을 마친 유저에게 authority
를 부여하여 대상 애플리케이션의 특정 리소스에 접근할 수 있게 허가하는 과정
Authorization
는 반드시 Authentication
과정 이후 수행, authority
는 role
형태로 부여하는 게 일반적
Access control
은 애플리케이션 리소스에 접근하는 행위를 제어하는 일
어떤 유저가 어떤 리소스에 접근하도록 허락할지를 결정하는 행위, Access controll decision
리소스의 접근 속성과 유저에게 부여된 권한, 다른 속성을 견주어 결정
Authentication
: 특정 리소스에 액세스하려는 사용자의 신원을 확인하는 방법
사용자를 인증하는 일반적인 방법은 username
과 password
를 입력하도록 요구하는 것
Spring Security의 PasswordEncoder
인터페이스는 password
를 안전하게 저장하기 위해 변환해주는 기능을 가짐
password
를 날 것 그대로 저장하면 보안에 좋지 않음
hash
과정을 덧붙임
Authentication
을 시도하면 기존의 저장되어있는 hashed password
와 그들이 적은 password
를 hash
한 값을 비교
hash
는 단방향이므로 기존의 hashed password
로 password
를 유추하기가 어려움
Rainbow Tables
라 불리는 테이블로 password
를 매번 작성할 때마다 그에 따른 hash
값을 테이블에 저장
hashed password
가 동일한 모든 사용자에 대해서 같은 password
로 Authentication
이 가능
salt
를 덧붙여 각 사용자별로 password
가 동일하더라도 hashed password
가 서로 다르게 생성
또한 Authentication
과정에서 일부러 resource
(i.e. CPU, memory, etc) 사용량을 극대화시켜 (예를 들면 password
검증에 1초가 걸리게) 시도 횟수를 현저하게 줄여 보안을 유지
기존의 PasswordEncoder
는 다음과 같은 문제가 있다
password encodings
들이 많고, 그것들이 쉽게 병합되지 않음 password storage
는 password
를 다시 변경하는 것 Spring Security
는 주요 변경 사항을 자주 만들수 없음 Instead Spring Security introduces DelegatingPasswordEncoder which solves all of the problems by:
대신 DelegatingPasswordEncoder
를 사용하면 해당 문제를 해결 가능하다
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
빈번하게 발생하는 Exploits
으로부터의 Protection
을 제공
CSRF
공격에 대해 Spring Security
는 다음과 같은 두 가지 기능을 제공
Security HTTP Response Headers
Spring Security
의 Servlet
은 Servlet Filter
에 기반을 둠
Filter
는 다음과 같은 순서로 진행
SecurityContextHolder
: Spring Security
가 인증을 마친 유저의 세부 정보를 저장하는 곳 SecurityContext
Authentication
GrantedAuthority
: Authentication
에서 principal
에게 부여되는 authority
AuthenticationManager
: Spring Security
의 Filters
가 Authentication
을 수행하는 방법을 정의하는 API ProviderManager
: AuthenticationManager
를 수행하는 가장 흔한 방식AuthenticationProvider
: ProviderManager
에서 특정 유형의 Authentication
을 수행하는 데 사용 AuthenticationEntryPoint
: AbstractAuthenticationProcessingFilter
: 인증에 사용되는 기본 필터
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
// ...
.authorizeHttpRequests(authorize -> authorize // 1
.mvcMatchers("/resources/**", "/signup", "/about").permitAll() // 2
.mvcMatchers("/admin/**").hasRole("ADMIN") // 3
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // 4
.anyRequest().denyAll() // 5
);
return http.build();
}
https://github.com/umtuk/ec-spring/tree/master/springsecurity
build.gradle
plugins {
id 'org.springframework.boot' version '2.6.7'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'org.ec'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
tasks.named('test') {
useJUnitPlatform()
}
IndexController.java
package org.ec.springsecurity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping
public class IndexController {
@GetMapping
public String index() {
return "index";
}
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<h1>index</h1>
</body>
</html>
콘솔에 password
Using generated security password: c0c0f62d-0339-47ec-b84f-0c225269510c
Spring Security
Starter를 추가한 것만으로도 다음과 같은 보안 구성이 제공
AuthorController.java
package org.ec.springsecurity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/author")
public class AuthorController {
@GetMapping
public String auth() {
return "author";
}
}
SecurityConfig.java
package org.ec.springsecurity.security;
import org.springframework.beans.factory.annotation.Autowired;
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.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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
@Autowired
public SecurityConfig(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
;
http
.authorizeRequests()
.antMatchers("/author").access("hasRole('ROLE_USER')")
.antMatchers("/", "/**").access("permitAll")
.and()
.httpBasic();
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(encoder())
;
}
}
http.csrf()
를 편하게 활성화/비활성화 가능
여기서는 POST를 정상적으로 수행하기 위해 비활성화
/author
페이지에 들어가기 위해서는 ROLE_USER
권한을 가지고 있어야 함
userDetailsService
를 활용하기 위해 아래 코드를 추가
User.java
package org.ec.springsecurity.user.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import java.util.Arrays;
import java.util.Collection;
@Entity
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
public User() {}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails
를 implements
해 username
, password
, authority
를 반환하는 메서드를 오버라이드
아래의 네 가지 boolean
을 리턴하는 메서드로 해당 User
의 상태가 어떤지를 반환
UserRepository.java
package org.ec.springsecurity.user.repository;
import org.ec.springsecurity.user.entity.User;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Long> {
User findByUsername(String username);
}
UserRepositoryUserDetailsService.java
package org.ec.springsecurity.user.service;
import org.ec.springsecurity.user.entity.User;
import org.ec.springsecurity.user.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserRepositoryUserDetailsService implements UserDetailsService {
private UserRepository userRepository;
@Autowired
public UserRepositoryUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user != null)
return user;
throw new UsernameNotFoundException("User '" + username + "' not found");
}
}
해당 Service
는 UserDetailsService
를 implements
해 loadUserByUsername
라는 메서드를 오버라이드
UserRepository
를 활용하여 User
객체를 load
User의 회원 가입 및 조회를 위해 다음과 같은 코드를 작성
RegistrationForm.java
package org.ec.springsecurity.user.dto;
import lombok.Data;
import org.ec.springsecurity.user.entity.User;
import org.springframework.security.crypto.password.PasswordEncoder;
@Data
public class RegistrationForm {
private String username;
private String password;
public User toUser(PasswordEncoder passwordEncoder) {
return new User(username, passwordEncoder.encode(password));
}
}
UserController.java
package org.ec.springsecurity.user.controller;
import org.ec.springsecurity.user.dto.RegistrationForm;
import org.ec.springsecurity.user.entity.User;
import org.ec.springsecurity.user.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/register")
public class UserController {
private UserRepository userRepository;
private PasswordEncoder passwordEncoder;
@Autowired
public UserController(
UserRepository userRepository,
PasswordEncoder passwordEncoder
) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@GetMapping
public Iterable<User> getAll() {
return userRepository.findAll();
}
@PostMapping
public ResponseEntity<User> singUp(@RequestBody RegistrationForm registrationForm) {
User user = userRepository.save(registrationForm.toUser(passwordEncoder));
return ResponseEntity.ok(user);
}
}
/register
에는 authorizeRequests
가 적용되어 있지 않아 모든 사용자가 조회할 수 있어 실제로는 좋은 코드가 아님
User
에 passwordEncoder
를 적용한 모습을 쉽게 관찰하기 위함
index.html에 진입 (보안이 적용되어 있지 않음)
author.html에 진입 (보안이 적용 ROLE_USER
권한을 가진 사용자만 접근 가능)
/register
에 GET 요청으로 현재 사용자 조회
실행 결과 : 비어 있음
사용자 등록을 위해/register
POST 요청
/register
POST 요청 결과
password가 암호화 후 User 객체로 변환 및 DB에 저장됨을 알 수 있음
이전에 작성한 User 객체로 인증
암호화 된 password를 입력하는 게 아닌 그대로의 password를 입력
author.html 진입
스프링 5 레시피
스프링 인 액션
https://docs.spring.io/spring-security/reference/index.html