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