본문 바로가기
4. Backend Development/2. Spring Security

1. Spring Security

by H232C 2020. 10. 7.

스프링 시큐리티란?

스프링 시큐리티는 "인증"(A 사용자라고 서버에게 알림), "인가"(A 사용자를 확인하여 권한을 부여)와 CSRF, XSS, 세션변조, ClickJacking 등 다양한 웹 보안 관련 이슈에 대응하도록 도와준다.

1부 스프링 시큐리티: 폼 인증

1. 폼 인증 예제 살펴보기

ㅇ 홈페이지
 - /
 - 인증된 사용자도 접근할 수 있으며 인증하지 않은 사용자도 접근 가능
 - 인증된 사용자가 로그인한 경우 이름을 출력

ㅇ 정보
 - /info
 - 이 페이지는 인증을 하지 않고도 접근 가능, 인증을 한 사용자도 접근 가능

ㅇ 대시보드
 - /dashboard
 - 이 페이지는 반드시 로그인한 사용자만 접근 가능
 - 인증하지 않은 사용자가 접근시 로그인 페이지로 이동

ㅇ 어드민
 - /admin
 - 이 페이지는 반드시 Admin 권한을 가진 사용자만 접근 가능
 - 인증하지 않은 사용자가 접근시 로그인 페이지로 이동
 - 인증은 거쳤으나 권한이 충분하지 않은 경우 에러 메세지 출력

 

2. 스프링 웹 프로젝트 만들기

ㅇ 스프링 부트와 타임리프를 사용해서 간단한 웹 애플리케이션 만들기
 - github.com/h232ch/demo-spring-security-h232ch
ㅇ 타임리프
 - xmlns:th=”http://www.thymeleaf.org” 네임스페이스를 html 태그에 추가.
 - th:text=”${message}” 사용해서 Model에 들어있는 값 출력 가능.

 

h232ch/demo-spring-security-h232ch

Spring Security Basic. Contribute to h232ch/demo-spring-security-h232ch development by creating an account on GitHub.

github.com

 

3. 스프링 시큐리티 연동

ㅇ 스프링 시큐리티 의존성 추가하기

pom.xml : Spring Security 의존성 추가

ㅇ 스프링 시큐리티 추가 후 모든 요청은 인증을 필요로 하며 기본 유저가 생성됨
 - 기본유저 정보 (id : user / pw : 스프링 부트 기동시 임시 발급됨)
 - ex) Using generated security password: 114284e0-656a-4fdf-b623-9b552a85b6c8
 - 의존성 추가로 임시 계정을 사용 및 식별 가능하다
 - 하지만 계정이 1개이고 비밀번호가 로그에 남아있다.

 

4. 스프링 시큐리티 설정하기

ㅇ SecurityConfig.java

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/", "/info").permitAll() // /, info 정보는 누구나 볼수있음
                .mvcMatchers("/admin").hasRole("ADMIN") // admin 페이지 접속시 ADMIN Role을 가지고 있어야함
                .anyRequest().authenticated(); // 기타 다른 요청은 모두 로그인 필요

        http.formLogin(); // 인증 요청시 form 로긴 페이지를 보여줌
        http.httpBasic(); // http basic 인증이란 요청해더에 username, password를 실어 보내면 브라우저 또는 서버가 그 값을 읽어서 인증함
    }
}

ㅇ 위 설정을 통해 요청 URL별 인증 설정이 가능하게 됨
ㅇ 하지만 여전히 계정은 1개이고 Admin 계정이 존재하지 않으며 비밀번호도 로그에 남고 있음

 

5. 스프링 시큐리티 커스터마이징: 인 메모리 유저 추가

ㅇ 지금까지 스프링 부트가 만들어 주던 (임의)유저 정보는?
 - UserDetailsServiceAutoConfiguration
 - SecurityProperties

ㅇ SecurityProperties를 사용해서 기본 유저 정보를 변경 가능
ㅇ SpringSecurity.java

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/", "/info").permitAll() // /, info 정보는 누구나 볼수있음
                .mvcMatchers("/admin").hasRole("ADMIN") // admin 페이지 접속시 ADMIN Role을 가지고 있어야함
                .anyRequest().authenticated(); // 기타 다른 요청은 모두 로그인 필요

        http.formLogin(); // 인증 요청시 form 로긴 페이지를 보여줌
        http.httpBasic(); // http basic 인증이란 요청해더에 username, password를 실어 보내면 브라우저 또는 서버가 그 값을 읽어서 인증함
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("sh").password("{noop}123").roles("USER").and() // 현재 DB는 구성하지 않았으나 해당값이 DB값이라고 보면 됨
                .withUser("admin").password("{noop}123").roles("ADMIN"); // noop prefix의 경우 암호화를 하지 않고 비교하는 것
                // 보통은 prefix에 따라 사용자가 입력한 패스워드를 암호화하여 저장된 데이터와 일치하면 인증을 해줌
    }
}

   
   

ㅇ 인메모리 사용자 추가 및 로컬 AuthenticationManager를 빈으로 노출

 

6. 스프링 시큐리티 커스터마이징 : JPA 연동

ㅇ JPA와 H2 의존성 추가

pom.xml : jpa, h2 의존성 추가

ㅇ Account.java

@Entity
public class Account {

    @Id @GeneratedValue
    private Integer id;

    @Column(unique = true)
    private String username;

    private String password;

    private String role;

ㅇ AccountService.java

@Service
public class AccountService implements UserDetailsService {
// UserDetailsService는 SpringSecurity에서 Authentication을 관리할 때
// DAO(Data Access Object)인터페이스를 통해서 저장소에 저장되어있는 유저 정보를 읽어오는 인터페이스이다.
// 이 인터페이스를 implements 하는 클래스를 빈으로등록만 하면 SpringSecurity가 자동으로 해당 Service를 UserDetailService로 사용함

    @Autowired
    AccountRepository accountRepository; // 만약 JPA를 사용하지 않는다면 여기에 DB를 연결하는 DAO 구현체가 와야함

    @Override // 스프링 시큐리티로 유저 정보를 제공하는 역할 -> 실제 인증을 하는 인터페이스는 AuthentificationManager가 수행
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // username이 해당하는 user정보를 가져와서 userDetails 타입으로 리턴하는 역할
        Account account = accountRepository.findByUsername(username); // 여기서 User를 꺼내오겠다.
        if (account == null) {
            throw new UsernameNotFoundException(username);
        }
        
    return User.builder() // SpringSecurity에서 제공하는 User빌더 (Account형의 User정보를 UserDetails형에 맞도록 UserBuilder(User는 UserDetails의 구현체이다)를 통해 생성하여 리턴)
                .username(account.getUsername())
                .password(account.getPassword())
                .roles(account.getRole())
                .build();
    }
}

ㅇ 해결한 문제 : 패스워드가 코드에 보이지 않음, DB에 들어있는 계정 정보를 사용할 수 있음
ㅇ 새로운 문제 : "{noop}"을 없앨 수는 없을까? 테스트는 매번 이렇게 해야하나?

 

8. 스프링 시큐리티 테스트 1부

https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#test-mockmvc

 

Spring Security Reference

The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them

docs.spring.io

Spring-Security-Test 의존성 추가

pom.xml : Spring-Security-test 의존성 추가

ㅇ RequestPostProcessor를 사용해서 테스트 하는 방법
 - with(user("user"))
 - with(anonymouse())
 - with(user("user").password("123").roles("USER","ADMIN"))
 - 자주 사용하는 user 객체는 리펙토리로 빼내서 재사용 가능

ㅇ 애노테이션을 사용하는 방법
 - @WithMockUser
 - @WithMockUser(roles="ADMIN")
 - 커스텀 애노테이션을 만들어 재사용 가능

ㅇ AccountController.java

@Controller
public class SampleController {

@GetMapping("/")
public String index(Model model, Principal principal){ 

        if(principal == null){
            model.addAttribute("message", "Hello Spring Security"); // Key Value 형태로 모델에 String 객체를 넣어줌
        } else {
            model.addAttribute("message", "Hello, " + principal.getName());
        }
        
        return "index";
}

ㅇ WithUser 애노테이션

package com.h232ch.demospringsecurityh232ch.account;

import org.springframework.security.test.context.support.WithMockUser;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME) // 런타임 종료전까지 유지
@WithMockUser(username="sh", roles = "USER")
public @interface WithUser {
}

ㅇ AccountControllerTest.java

package com.h232ch.demospringsecurityh232ch.account;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;


@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class AccountControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void index_annoymous() throws Exception{
        mockMvc.perform(get("/").with(anonymous())) // anonymous가 / 페이지를 호출 요청, 미로그인 사용자로 테스트
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Test
    public void index_user() throws Exception{
        mockMvc.perform(get("/admin").with(user("sh").roles("USER"))) // user(sh, USER)가 로그인된 상태로 / 페이지 호출 요청
                .andDo(print()) // 위 요청은 Forbidden 에러가 발생함 403 (Cause User is not allowed)
                .andExpect(status().isOk());
    }

    @Test
    public void index_admin() throws Exception{
        mockMvc.perform(get("/admin").with(user("admin").roles("ADMIN"))) // user(sh, USER)가 로그인된 상태로 / 페이지 호출 요청
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Test
    @WithAnonymousUser // anonymous가 / 페이지를 호출 요청, 미로그인 사용자로 테스트 2번째 방법
    public void index_annoymous2() throws Exception{
        mockMvc.perform(get("/")) 
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username="sh", roles = "USER")
    public void index_user2() throws Exception{
        mockMvc.perform(get("/")) // user(sh, USER)가 로그인된 상태로 / 페이지 호출 요청 2번째 방법
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Test
    @WithUser // 해당 어노테이션 인터페이스 내에 @WithMockUser(username="sh", roles = "USER") 정보가 삽입되어 있음
    public void index_user3() throws Exception{
        mockMvc.perform(get("/")) // user(sh, USER)가 로그인된 상태로 / 페이지 호출 요청 3번째 방법 // WithUser 어노테이션 생성후 불러오기
                .andDo(print()) 
                .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username="sh", roles = "ADMIN")
    public void index_admin2() throws Exception{
        mockMvc.perform(get("/admin")) // user(sh, USER)가 로그인된 상태로 / 페이지 호출 요청 2번째 방법
                .andDo(print())
                .andExpect(status().isOk());
    }

    @Autowired
    AccountService accountService;

    private Account createUser() {
        Account account = new Account();
        account.setUsername("sh");
        account.setPassword("123");
        account.setRole("USER");
        return accountService.createNew(account);
    }

    @Test
    @Transactional // 각각의 테스트가 종료되면 롤백이 되도록 설정함 (Username은 유니크하여 중복 생성불가능함 -> 아래 테스트에서 생성된 username은 테스트 종료시 삭제되어야 다음 테스트 가능
    public void login_succeed() throws Exception{

        Account user = createUser();
        mockMvc.perform(formLogin().user(user.getUsername()).password("123")) // 패스워드는 직접 입력해줘야함 (로그인시에는 평문값으로)
                .andDo(print())
                .andExpect(authenticated());
    }

    @Test
    @Transactional // 각각의 테스트가 종료되면 롤백이 되도록 설정함 (Username은 유니크하여 중복 생성불가능함 -> 아래 테스트에서 생성된 username은 테스트 종료시 삭제되어야 다음 테스트 가능
    public void login_unauthentificated() throws Exception{

        Account user = createUser();
        mockMvc.perform(formLogin().user(user.getUsername()).password("12")) // 패스워드는 직접 입력해줘야함 (로그인시에는 평문값으로)
                .andDo(print())
                .andExpect(unauthenticated());
    }
}

 

9. 스프링 시큐리티 테스트 2부

ㅇ 폼 로그인 / 로그아웃 테스트
 - perform(formLogin())
 - perform(formLogin().user("admin").password("pass"))
 - perform(logout())

ㅇ 응답 유형 확인
 - authenticated()
 - unauthenticated()

ㅇ 해결한 문제 : 스프링 시큐리티 테스트 작성 가능

ㅇ SignUpController

package com.h232ch.demospringsecurityh232ch.account;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/signup")
public class SignUpController {


    @Autowired
    AccountService accountService;

    @GetMapping
    public String signupFrom(Model model){
        model.addAttribute("account", new Account());
        return "signup";
    }

    @PostMapping
    public String processSignup(@ModelAttribute Account account) {
        account.setRole("USER");
        accountService.createNew(account);
        return "redirect:/";
    }
}

ㅇ AccountService

@Service
public class AccountService implements UserDetailsService {
    // UserDetailsService는 SpringSecurity에서 Authentication을 관리할 때
    // DAO(Data Access Object)인터페이스를 통해서 저장소에 저장되어있는 유저 정보를 읽어오는 인터페이스이다.
    // 이 인터페이스를 implements 하는 클래스를 빈으로등록만 하면 SpringSecurity가 자동으로 해당 Service를 UserDetailService로 사용함

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    AccountRepository accountRepository; // 만약 JPA를 사용하지 않는다면 여기에 DB를 연결하는 DAO 구현체가 와야함

    @Override // 스프링 시큐리티로 유저 정보를 제공하는 역할 -> 실제 인증을 하는 인터페이스는 AuthentificationManager가 수행
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { // username을 받아와서 username이 해당하는 user정보를 가져와서 userDetails 타입으로 리턴하는 역할
        Account account = accountRepository.findByUsername(s); // 여기서 User를 꺼내오겠다.
        if(account == null){
            throw new UsernameNotFoundException(s);
            
        }
        
            return User.builder() // SpringSecurity에서 제공하는 User빌더 (Account형의 User정보를 UserDetails형에 맞도록 User Builder를 통해 생성하여 리턴 (Builder를 사용한다는건 Lombok을 쓴다는것)
                .username(account.getUsername())
                .password(account.getPassword())
                .roles(account.getRole()) // , "USER") 로 권한을 추가할 수도 있음 파트 16
                .build();
                
    public Account createNew(Account account) {

//        account.encodePassword(); // 입력받은 패스워드 앞에 Prefix를 설정하고 리턴함 
        account.encodePassword(passwordEncoder); // Password Encoder 사용시 passwordEncoder를 매개변수로 받아야 함
        return this.accountRepository.save(account);
    }
}

ㅇ Account

package com.h232ch.demospringsecurityh232ch.account;

import org.springframework.security.crypto.password.PasswordEncoder;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Account {

    @Id @GeneratedValue // DB에 들어갈떄 자동으로 값이 생성
    private Integer id;

    @Column(unique = true) // username은 유일한 값이 들어가야 함
    private String username;

    private String password;
    private String role;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    public void encodePassword(PasswordEncoder passwordEncoder) {
//        this.password = "{noop}"+this.password; // Spring Security는 Prefix가 필요하므로 Prefix를 입력패스워드 앞에 붙여줘야 함
        this.password = passwordEncoder.encode(this.password); // PasswordEncoder를 사용할 경우 prefix 불필요 // 기본적으로 Bcrypt 사용
    }
}

ㅇ SignUpControllerTest

package com.h232ch.demospringsecurityh232ch.account;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;


@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SignUpControllerTest {

    @Autowired
    MockMvc mockMvc;


    @Test
    public void signUpForm() throws Exception {
        mockMvc.perform(get("/signup"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string(containsString("_csrf")));
    }

    @Test
    public void processSignup() throws Exception{
        mockMvc.perform(post("/signup")
        .param("username", "sh")
        .param("password", "123")
        .with(csrf())) // csrf 토큰을 추가시켜주는 설정
                .andDo(print())
                .andExpect(status().is3xxRedirection());
    }

}

 

 

 

2부 스프링 시큐리티 : 아키텍처

10. SecurityContextHolder와 Authentication

https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#core-components

 

Spring Security Reference

The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them

docs.spring.io

ㅇ SecurityContextHolder (Authentication 객체를 담고 있으며 ThreadLocal을 사용하고 있음)
 - SecurityContext 제공, 기본 전략으로 ThreadLocal을 사용함
 - ThreadLocal : 한 쓰레드 내에서 쉐어하는 저장소 (사용하는 객체 및 코드는 서비스에도 있고 레파시토리, 컨트롤러에도 존재하며 개발시 유기저긍로 객체를 주고 받음 쓰레드 로컬을 사용하면 객체를 주고 받는게 아니라 쉐어드된 저장소에서 객체를 넣고 빼어 쓸 수 있음 (Authentication의 경우 ThreadLocal이 사용되어 전체 코드에서 사용 가능)


ㅇ SecurityContext : Authentication(Principal:계정정보, GrantAuthority:권한) 제공

- SecurityContextHolder 내부에 (SecurityContext를 사용하는)getContext()를 통해 Authentication 정보를 가져옴

ㅇ Principal
 - 누구에 해당하는 정보
 - UserDetailsService에서 리턴한 그 객체(UserDetailsService 중요)
 - 객체는 UserDetails 타입

ㅇ GrantAuthority
 - "ROLE_USER", "ROLE_ADMIN" 등 Principal이 가지고 있는 "권한"을 나타낸다.
 - 인증 이후, 인가 및 권한 확인할 때 이 정보를 참조한다.

ㅇ UserDetails
 - 애플리케이션이 가지고 있는 유저 정보와 스프링 시큐리티가 사용하는 Authentication 객체 사이의 어댑터

ㅇ UserDetailsService
 - 유저 정보를 UserDetails 타입으로 가져오는 DAO(Data Access Object) 인터페이스

ㅇ SimpleController

@GetMapping("/dashboard2")
    public String dashboard2(Model model, Principal principal){ // principal 객체를 받아서 로그인된 사용자의 정보를 알 수 있다.
        sampleService.dashboard(); // 서비스에 principal 정보를 넘겨주지 않더라도 해당 서비스 안에서 Authentication 정보를 활용가능 (ThreadLocal 방식이기 때문에 가능)
        model.addAttribute("message", "Hello, " + principal.getName() ); // Key Value 형태로 모델에 String 객체를 넣어줌
        return "dashboard";
    }

ㅇ SimpleService

@Service // 서비스에 principal 정보를 넘겨주지 않더라도 해당 서비스 안에서 Authentication 정보를 활용가능 (ThreadLocal 방식이기 때문에 가능)
public class SampleService { // 파트 10

public void dashboard() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // SecurityContextHolder에는 인증된 객체의 정보만 들어감
        Object principal = authentication.getPrincipal(); // 인증 사용자를 나타냄 (getPrincipal) 누구냐? (UserDetailsService에서 리턴한 User 객체)
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); // 인증사용자가 가지고 있는 권한을 나타냄 (getAuthorities)
        // (UserDetailsService에서 리턴한 User 객체)
        Object credentials = authentication.getCredentials(); // 크리덴셜
        boolean authenticated = authentication.isAuthenticated(); // 인증된 사용자냐? (로그인 상태라면 True)
        // 권한정보는 AccountService에 기재된 UserDetailsService를 기반으로 작성됨
        // ThreadLocal을 사용하는 SpringContextHolder는 최종적으로 Authentication 담고있으며 Application 어디에서든 사용 가능하도록 해줌
        // Authority에는 Princial, GrantAuthority 두개의 정보가 들어있음


    }

SecurityContextHolder에 존재하는 Authentication 객체를 가져옴
Authentication의 Authorities 정보를 가져옴
Authentication의 Authenticated 여부를 가져옴

 

11. AuthenticationManager와 Authentication

ㅇ 스프링 시큐리티에서 인증(Authentication)은 AuthenticationManager가 한다.
 - AuthenticationManager의 구현체인 ProviderManager의 authenticate 정보

Authentication authenticate(Authentication authentication) throws AuthenticationException;

 - 인자로 받은 (우리가입력한 값을 갖고있는)Authentication이 유요한 인증인지 확인하고 (SecurityContextHolder)Authentication객체를 리턴한다.
 - 인증을 확인하는 과정에서 비활성 계정, 잘못된 비밀번호, 잠김 계정 등의 에러를 던질 수 있음
 - AuthenticationManager 인증을 수행하는 인터페이스이다.
 - 즉 상속받은 ProviderManager는 인증을 수행하므로 디버깅을 통해 어떻게 동작하는지 확인할 수 있음

ㅇ 인자로 받은 Authentication
 - 사용자가 입력한 인증에 필요한 정보(username, passowrd)로 만든 객체 (폼 인증인 경우 해당)
 - Authentication : principal("sh") Credential("123")

ㅇ 유효한 인증인지 확인
 - 사용자가 입력한 Password가 UserDetailsService를 통해 읽어온(Retrieve) UserDetails 객체에 들어있는 password와 일치하는지 확인
 - 해당 사용자 계정이 잠겨 있진 않는지, 비활성 계정은 아닌지 등 확인

ㅇ Authentication 객체를 리턴(ThreadLocal에 존재하는 즉 SecurityContextHolder에 저장되어있는)
 - Authentication : Principal : UserDetailsService에서 리턴한 그 객체 (User)
 - Credentials
 - GrantedAuthories

인증을 수행하는 ProviderManager의 authenticate를 통해 인증 수행 (아래 authentication은 우리가 입력한 ID/PW 정보이다.) SecurityContextHolder에서 호출하는 Authentication과 명칭만 동일하고 다른 객체다.

 

처음 AnonymousAuthenticationProvider를 호출하는데 이 Provider는 Authentication을 처리할 수 없다

 

Parent를 호출하고 다시 for 문으로 진입함

 

DaoAuthenticationProvider를 사용하여 Authentication 처리

 

AbstractUserDetailsAuthenticationProvider의 UsernamePasswordAUthenticationToken으로 authentication을 처리

 

Result 값으로 provider.authenticate(authentication) 값을 받아옴

 

UserDetailsService를 사용하여 사용자 인증을 수행하는 AbstractUserDetailsAuthenticationProvider에서 retrieveUser에 우리가 입력한 Authentication username을 파라메터로 전달함)

 

UserDetails형으로 this.getUserDetailsService().loadUserByUsername(username)을 호출

 

UserDetailsService 구현체인 AccountService에서 구현한 loadUserByUsername이 호출되면서 커스텀하게 작성한 Account가 생성되며 이를 UserDetails 형으로 리턴한다.

 

리턴받은 값을 확인해보면 user라는 객체가 생성되어 있는데 해당 객체가 SecurityContextHolder에 저장되어 있는 Authentication 객체이다.

 

authenticaton : 우리가 입력한 id/pw 정보, result : SecurityContextHolder에 저장되어있는 Authentication 정보

 

12. ThreadLocal

ㅇ Java.lang 패키지에서 제공하는 쓰레드 범위 변수. 즉 쓰레드 수준의 데이터 저장소
ㅇ 같은 쓰레드 내에서만 공유
ㅇ 따라서 같은 쓰레드라면 해당 데이터를 메소드 매개 변수로 넘겨줄 필요 없음
ㅇ SecurityContextHolder의 기본 전략
ㅇ AccountContext

public class AccountContext {

    // 인증을 위해 Authentication을 이용함 실제 인증은 AuthenticationManager 인터페이스의 구현제인 ProviderManager에서 구현되며
    // Authentication은 SecurityContext를 통해 사용가능하며 이는 SecurityContextHolder가 제공한다.

    // 인증정보는 SpringSecurityHolder를 통해 가져오고
    // SecurityContextHolder의 기본 전략은 ThreadLocal이다. -> 우리의 인증정보는 ThreadLocal 형태로 가져오는 것이다!
    // 즉 Authentication 객체 ThreadLocal을 통해 넣어주는 것
    // 이후 SecurityContextHolder에 들어가서 애플리케이션 전반에 걸처 우리가 사용할 수 있도록 함

    // ThreadLocal은 프로그램에서 변수를 전역으로 사용가능하게 한다.
    // ThreadLocal은 아래 예제와 같이 구현되어 있다!

    private static final ThreadLocal<Account> ACCOUNT_THREAD_LOCAL
            = new ThreadLocal<>();

    public static void setAccount(Account account) {
        ACCOUNT_THREAD_LOCAL.set(account);
    }

    public static Account getAccount() {
        return ACCOUNT_THREAD_LOCAL.get();
    }

}

ㅇ AccountController

@RestController
public class AccountController { //파트 12 쓰레드

    @Autowired
    private AccountService accountService; // 이변수는 이 클래스 내부에서 사용 가능한 스콥을 가지고 있음
    // 쓰레드로컬이라는건 변수를 쓰레드스콥으로 지정하고 한 쓰레드 내에서는 그 변수를 공유하는것
    // 프로그램단위의 쓰레드로컬이 존재하는 경우 해당 쓰레드에 변수를 지정하고 가져다 쓰면됨

    @GetMapping("/account/{role}/{username}/{password}") // Url Path에 들어있는 값 각각 Model에 넣어줌
    public Account createAccount(@ModelAttribute Account account){
        return accountService.createNew(account);
    } // SecurityConfig에서 Account/** Url은 인증이 불필요하도록 설정해야함
    // 위 내용은 계정을 생성하는 임시 로직이다. (실환경에서는 이런식으로 사용하면 안됨)
}

 

13. Authentication과 SecurityContextHolder

ㅇ AuthenticationManager가 인증을 마친 뒤 리턴 받은 Authentication 객체의 행방은?
ㅇ UsernamePasswordAuthentidationFilter (AuthenticatonManager를 사용하는 필터)
 - 폼 인증을 처리하는 시큐리티 필터
 - 인증된 Authentication 객체를 SecurityContextHodler에 넣어주는 필터
 - SecurityContextHolder에 Authentication 값을 넣는다. (Authentication을 불러와서 SecurityContextHolder에 넣어줌)
 - SecurityContextHolder.getContext().setAuthentication(authentication)

ㅇ 정리
 - AuthenticationManager를 사용하는 UsernamePasswordAuthenticationFilter가 authentication을 SecurityContextHolder에 넣는다.
 - 로그인이 성공적으로 수행되면 SecurityContextPersisenceFilter는 인증 정보를 저장한다.
 - 로그인 요청이 성공적으로 종료되고 로그인이 필요한 오브젝트에 접근할 때
 - SecurityContextPersistenceFilter는 로그인 성공 이후 저장해 놓은 인증정보(SecurityContext)를 복원하여 SecurityContextHolder에 세팅하고 로그인된 상태로 해당 오브젝트에 접근하게 함
 - 이후 요청이 정상적으로 종료되면 SecurityContextHolder에 세팅된 인증정보를 비워줌

ㅇ SecurityContextPersisenceFilter (스테이트풀하게 세션을 이용하는 경우 사용하는 필터)
ㅇ SecurityContextPersisenceFilter.java : 로그인 후 세션을 유지하여 계속 로그인 상태로 만들어주는 현상을 디버깅 모드로 관찰

로그인 처리를 위해 ProviderManager authenticatie 수행
인증을 최종적으로 수행하는 UsernamePassowrdAuthenticationFilter로 보냄
Authenticate를 수행하고 그 결과를 Parent인 AbtractAuthenticationProcessingFilter로 리턴함 
doFilter가 수행됨
doFilter의 일부분으로 attemptAuthentication이 수행됨 (authResult는 SecurityContextHodler에 저장되는 Authentication이다)
successfuleAuthentication을 통해 들어가면
SecurityContextHolder에 Authentication을 세팅하는 부분이 나옴 (여기까지 로그인 부분)
인증이 된 다음 SecurityContextPersistenceFilter에서 인증정보를 저장하고 이후 로그인이 필요한 오브젝트 접근시
기존에 저장한 인증정보가 있는지 여부를 확인하여 있다면 SecurityContext에 인증정보를 넣어준다.
인증정보(context)가 HttpSession에 저장되는 화면
SecurityContextHolder에 Context를 저장하고 오브젝트에서 해당 정보를 이용하여 작업을 처리함
작업 처리가 완료된 이후 SecurityContextHodler를 비워줌

 

SecurityContextHolder Authentication 정보가 화면에 표기됨

 

14. 스프링 시큐리티 Filter와 FilterChainProxy

ㅇ 스프링 시큐리티가 제공하는 필터들
 - WebAsyncManagerIntergrationFilter
 - SecurityContextPersistenceFilter
 - HeaderWriteFilter
 - CsrfFilter
 - LogoutFilter
 - UsernamePaaswordAuthenticationFilter
 - DefaultLoginPageGeneratingFilter
 - BasicAuthenticationFilter
 - RequeestCacheAwareFilter
 - SecurityContextHolderAwareRequestFilter
 - AnonymouseAuthenticationFilter
 - SessionManagementFilter
 - ExeptionTranslationFilter
 - FilterSecurityInterceptor

ㅇ 필터는 모두 서블릿 필터로 구성되어 있음

ㅇ 이 모든 필터는 FilterChainProxy가 호출함

ㅇ FilterChainProxy 디버깅

FilterChainProxy가 여러가지 Filter 목록을 호출하고 있음

ㅇ 여러개의 WebSecurityConfigurer 사용시 우선순위를 지정할 수 있음 (비권장 Matcher로 조정하는것 권장)

@Configuration
@Order(Ordered.LOWEST_PRECEDENCE - 50) // 숫자가 낮을수록 우선순위가 높음
//@EnableWebSecurity // 부트가 자동으로 설정해주기 때문에 빼도됨
public class SecurityConfig extends WebSecurityConfigurerAdapter {
......


@Configuration
@Order(Ordered.LOWEST_PRECEDENCE - 100) // SecurityConfig 우선순위 정해주기 파트 14
//@EnableWebSecurity // 부트가 자동으로 설정해주기 때문에 빼도됨
public class AnotherSecurityConfig extends WebSecurityConfigurerAdapter {


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/acount/**") // 옵션에 따라 FilterChainProxy가 필요한 필터를 선택함
                .authorizeRequests()
                .anyRequest().permitAll(); // 전부다 인증을 요구하겠다.
    }

}

 

15. DelegatingFilterProxy와 FilterChainProxy

ㅇ DelegatingFilterProxy (FilterChainProxy로 가기위한 전처리)
 - 일반적인 서블릿 필터
 - 서블릿 필터 처리를 스프링에 들어있는 빈으로 위임하고 싶을 때 사용하는 서블릿 필터
 - 타겟 빈 이름을 설정한다.
 - 스프링 부트 없이 스프링 시큐리티를 설정할 때는 AbstractSecurityWebApplicationInitializer를 사용해서 등록
 - 스프링 부트를 사용할 때는 자동으로 등록된다. (SecurityFilterAutoConfiguration)

ㅇ FilterChainProxy
 - 보통 "springSecurityFilterChain"이라는 이름의 빈으로 등록됨

 

16. AccessDecisionManager 1부

ㅇ Access Control 결정을 내리는 인터페이스로 구현체 3가지를 기본으로 제공함
 - AffirmativeBased : 여러 Voter중 한명이라도 허용ㅎ면 허용.
 - ConsenssusBased : 다수결
 - UnanimouseBased : 만장일치

ㅇ AccessDecisionVoter
 - 해당 Authentication이 특정한 Object에 접글할 때 필요한 ConfigAttributes를 만족하는지 확인한다.
 - WebExpressionVoter: 웹 시큐리티에서 사용하는 기본 구현체, ROLE_Xxxx가 매치하는지 확인
 - RoleHierachyVoter : 계층현 ROLE 지원. ADMIN > MANAGER > USER

17. AccessDecisionManager 2부

ㅇ AccessDecisionManager 또는 Voter를 커스터마이징 하는 방법
ㅇ 계층형 ROLE 설정

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    public AccessDecisionManager accessDecisionManager(){ // 계층구조의 롤을 설정하기 위함, 롤히어라키 세팅을  configure에서 지원안함
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");

        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setRoleHierarchy(roleHierarchy);

        WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
        webExpressionVoter.setExpressionHandler(handler);

        List<AccessDecisionVoter<? extends Object>> voters = Arrays.asList(webExpressionVoter);
        return new AffirmativeBased(voters);

        // WebDecisionManager는 인가를 담당하고 있으며 내부에 Access Control 결정을 내리는 AffimativeBased와
        // 해당 Authentication이 특정한 Obejct에 접근할 때 필요한 ConfigAttributes를 만족하는지 확인하는 AccessDecitinVoter를 사용한다.
        // 우리는 WebExpressionVoter가 사용하는 handler에 SetRoleHierachy를 해주기 위해 AccessDecisionManager를 직접 구현한 것임
        // congifure에서 해당 설정이 없다고 함

    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**") 
                .authorizeRequests() 
                .mvcMatchers("/","info","/account/**", "signup").permitAll() 
                .mvcMatchers("/admin").hasRole("ADMIN")
                .mvcMatchers("/user").hasRole("USER") 
                .anyRequest().authenticated() 
                .accessDecisionManager(accessDecisionManager()) // 우리가 설정한 AccessDicisionManager를 사용하게 함

 

18. FilterSecurityInterceptor

AccessDecisionManager를 사용하여 AccessControl 토는 예외처리 하는 필터
 - 대부분의 경우 FilterChainProxy에 제일 마지막 필터로 들어있다.
 - 인증을 거친 뒤 마지막에 최종적으로 접근이 가능한지 확인하는 필터

현재 오브젝트 요청 세션에 Anonymouse 권한이 존재하고 오브젝트 허용 권한은 ROLE_USER로 확인되기 때문에 해당 접속은 거부되어 Login 페이지로 이동됨

19. ExceptionTranslationFilter

ㅇ 필터 체인에서 발생하는 AccessDeniedException과 AuthenticationException을 처리하는 필터
ㅇ AuthenticationException 발생시 : 인증예외 (인증이 될때까지 인증시도)
 - AUthenticationEntryPoint 실행
 - AbstrctSecurityInterceptor 하위 클래스(예, FilterSecurityInterceptor)에서 발생하는 예외만 처리
 - 그렇다면 UserPasswordAuthenticationFilter에서 발생한 인증 에러는?
UsernamePassowrdAUthenticationFilter의 상위 클래스인 AbstractAuthenticationProcessingFilter에서 unsuccessfultAuthentication이 실행되며 saveException을 통해 세션에 에러 메세지를 담아두며 해당 메세지를 기반으로 에러 페이지를 보여줌

ㅇ AccessDeniedException 발생시 (인증에러가 아닌 익명사용자인지 판단하여 AuthenticationEntryPoint를 실행하여 인증 유도, 인증된 사용자라면 AccessDeniedHandler에 위임
 - 익명사용자라면 AuthenticationEntryPoint 실행
 - 아니라면 AccessDeniedHandler에게 위임

 

20. 스프링 시큐리티 아키텍처 정리

1. 최초 스프링 부트가 DelegatingFilterProxy를 빈으로 등록함
2. FilterChainProxy(Bean이름 : springSeucirtyFilterChain)에 필터 처리를 위임함
3. 필터들은 체ㅐ인형태로 구성됨 (시큐리티 필터 목록)
4. WebSecurity, HttpSecurity를 가지고 필터 체인을 구성함(예제에서는 WebSecurityConfigurerAdapter를 상속받아 스프링 시큐리티 환경을 설정)
5. 인증과 관련해서는 AuthenticationManager를 사용
6. AuthenticationManager 구현체인 ProviderManager는 다양한 Provider를 사용하여 인증을 수행하는데
7. 그중 DAOAuthenticationProvider가 UserDetailsSerivce의 정보를 읽어와서 인증을 수행함
8. 데이터에 존재하는 사용자 정보와 사용자가 입력한 정보가 동일한지 확인 (UsernamePasswordAuthenticationFilter)
9. 인증이 확인되면 UsernamePasswordAuthenticationFilter 부모 객체인 AbtractAuthenticationProcessingFilter에서 Authentication 객체를 SecurityContextHodler에 저장함
10. Authentication 객체에 저장되어있는 정보는 principal과 grantedAuthorities가 존재함
11. 이렇게 저장된 Authentication은 SecurityContextPersistenceFilter가 인증정보를 저장하고 이후 로그인된 상태를 유지하도록 해줌
12. 인가와 관련해서는 AccessDecisionManager를 사용하여 Object에 접근시 현재 SecurityContextHolder에 있는 Authentication 정보를 기반으로 접근통제를 수행
13. AccessDecisionManager은 기본적으로 AffirmativeBased 구현체를 사용하여 접근권한 확인을 수행
14. WebExpressionVoter는 SecurityExpressionHandler를 사용하여 익스프레션을 처리하는데 예제에서는 계층화된(하이라키) ROLE(ADMIN>USER)를 처리하기 위해 커스터마이징을 수행했다 (WebSecurityConfigurer)

https://spring.io/guides/topicals/spring-security-architecture

 

Spring Security Architecture

this topical is designed to be read and comprehended in under an hour, it provides broad coverage of a topic that is possibly nuanced or requires deeper understanding than you would get from a getting started guide

spring.io

https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#overall-architecture

 

Spring Security Reference

The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them

docs.spring.io

3부 웹 애플리케이션 시큐리티

21. 스프링 시큐리티 ignoring() 1부

WebSecurity의 ignoring()을 사용해서 시큐리티 필터 적용을 제외할 요청을 설정할 수 있다.
스프링 부트가 제공하는 PathRequest를 사용해서 정적 자원 요청을 스프링 시큐리티 필터를 적용하지 않도록 설정.

 

22. 스프링 시큐리티 ignoring() 2부

ㅇ 동적 리소스는 http.authorizeRequests()에서 처리하는 것을 권장합니다
ㅇ 정적 리소스는 WebSecurity.ignore()를 권장하며 예외적인 정적 자원 (인증이 필요한 정적자원이 있는 경우)는 http.authorizeRequests()를 사용할 수 있습니다.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    http.antMatcher("/**")
        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // 파트 22 Static 리소스의 스프링 시큐리티 미적용 두번째 방법

    @Override
    public void configure(WebSecurity web) throws Exception { // 파트 21 특정 요청에 대한 Spring Security 설정을 거부하고 싶을때
//        web.ignoring().mvcMatchers("/favicon.ico"); // 파비콘 요청을 무시하겠다. 하지만 매번 스태틱 리소스를 명시하는건 귀찮은일
        web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations()); // 이와 같이 설정하면 스태틱 리소스에 대해 Spring Security 예외 처리함
        // ignoring을 적용할 경우 Spring Security Filter 자체를 0으로 만듦 (FilterProxy에서 Filter를 가져오지 않음)
        // atCommonLocation 뒤에 추가로 기재하여 구체적인 통제가 가능하다.
        // 방법은 다양함
    }

 

23. Async 웹 MVC를 지원하는 필터: WebAsyncManagerIntegrationFilter

스프링 MVC의 Async 기능(핸들러에서 Callable을 리턴할 수 있는 기능)을 사용할 때에도 SecurityContext를 공유하도록 도와주는 필터.
 - PreProcess: SecurityContext를 설정한다.
 -
Callable: 비록 다른 쓰레드지만 그 안에서는 동일한 SecurityContext를 참조할 수 있다.
 - PostProcess: SecurityContext를 정리(clean up)한다.

Async란 병렬식으로 동작함
ThreadLocal은 Callable을 통해 Return 값을 받을 수 있다. 테스트 코드에서는 Callalbe로 만든 Thread와 Tocat에서 생성한 Thread 간의 SecurityContext 공유가 가능한 것을 보여준다.

PreProcess는 서로다른 Thread에 Spring Security를 적용하도록 SecurityContext를 설정하고 Callable은 SecurityContext를 참조하며 PostProcess는 사용종료 시점에 SecurityContext를 정리하는 역할을 한다.

@Controller
public class SampleController {

    @GetMapping("/async-handler") // 파트 23 (WebAsyncManagerIntegrationFilter)
    // WebMVC의 Async 기능을 사용할 때에도 Spring Security를 적용
    @ResponseBody
    public Callable<String> asyncHanlder(){

        SecurityLogger.log("MVC"); // Tomcat이 할당한 NIO 쓰레드 (Static 메서드 사용)

        return new Callable<String>() {
            @Override // 먼저 핸들러에 요청한 쓰레드의 응답을 내보내고 Callable 내의 프로세스가 완료되면 Return을 보냄
            public String call() throws Exception {
                SecurityLogger.log("Callable");
                return "Async Handlder"; // 별도의 쓰레드에서 동작 그러나 Spring Principal은 동일한것을 확인하기 위한 코드
            }
        };
    }
package com.h232ch.demospringsecurityh232ch.common;

import org.springframework.security.core.context.SecurityContextHolder;

public class SecurityLogger {

    public static void log(String message){
        System.out.println(message);
        Thread thread = Thread.currentThread();
        System.out.println("Thread : "+thread.getName());
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        System.out.println("principal : "+principal);
    }
}

 

24. 스프링 시큐리티와 @Async

@Async를 사용한 서비스를 호출하는 경우 : Callable과 다르게 @Async를 사용한 서비스를 호출하는 경우 별도의 Async 설정을 해줘야 함

 

@Controller
public class SampleController {

@GetMapping("async-service") // 파트 24
    @ResponseBody
    public String asyncService(){ // 비동기 처리에서는 순서가 보장되지 않음
        SecurityLogger.log("MVC Before Async Service");
        sampleService.asyncService(); // 비동기 처리 (해당 메서드의 결과를 기다리지 않고 다른 기능이 실행됨)
        SecurityLogger.log("MVC After Async Service");
        return "Async Service";
        // Async를 프로젝트에 적용하기 위해서는 메인메서드에 @EnableAsync를 적용해줘야함
    }
@SpringBootApplication
@EnableAsync // 파트 24
public class DemoSpringSecurityH232chApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoSpringSecurityH232chApplication.class, args);
    }

}
@Service 
public class SampleService {

@Async // 특정 Bean의 메소드를 호출할때 별도의 쓰레드를 만들어서 비동기적으로 호출해줌
    // Async만 사용한 경우 SecurityContextHolder의 Authentication이 공유가 안됨으로 설정이 필요함
    public void asyncService() { // 파트 23
        SecurityLogger.log("Async Service");
        System.out.println("Async Service is called");
    }
@Controller
public class SecurityConfig extends WebSecurityConfigurerAdapter {

SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); // 파트 24
        // 톰캣에서 기본으로 생성하는 NIO 쓰레드 하위 쓰레드(Task)에 SecurityContextHolder를 사용할 수 있도록 하는 옵션

25. SecurityContext 영속화 필터: SecurityContextPersistenceFilter

SecurityContextRepository를 사용해서 기존의 SecurityContext를 읽어오거나 초기화 한다.
 - 기본으로 사용하는 전략은 HTTP Session을 사용한다.
 - Spring-Session과 연동하여 세션 클러스터를 구현할 수 있다. (이 강좌에서는 다루지 않습니다.)

 

26. 시큐리티 관련 헤더 추가하는 필터: HeaderWriterFilter

ㅇ 응답 헤더에 시큐리티 관련 헤더를 추가해주는 필터
 -
XContentTypeOptionsHeaderWriter: 마임 타입 스니핑 방어.
 -
XXssProtectionHeaderWriter: 브라우저에 내장된 XSS 필터 적용.
 -
CacheControlHeadersWriter: 캐시 히스토리 취약점 방어.
 -
HstsHeaderWriter: HTTPS로만 소통하도록 강제.
 -
XFrameOptionsHeaderWriter: clickjacking 방어.

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Language: en-US
Content-Type: text/html;charset=UTF-8
Date: Sun, 04 Aug 2019 16:25:10 GMT
Expires: 0
Pragma: no-cache
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

 

27. CSRF 어택 방지 필터: CsrfFilter

인증된 유저의 계정을 사용해 악의적인 변경 요청을 만들어 보내는 기법.
CORS를 사용할 때 특히 주의 해야 함. (Cross Origin Resource Sharing)

 - 타 도메인에서 보내오는 요청을 허용하기 떄문

의도한 사용자만 리소스를 변경할 수 있도록 허용하는 필터

 

28. CSRF 토큰 사용 예제

ㅇ JSP에서 스프링 MVC가 제공하는 <form:form> 태그 또는 타임리프 2.1+ 버전을 사용할 때 폼에 CRSF 히든 필드가 기본으로 생성 됨.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>SignUp</title>
</head>
<body>
    <h1>Sign Up</h1>
    <form action="/signup" th:action="@{/signup}" th:object="${account}" method="post">
        <p>Username: <input type="text" th:field="*{username}" /></p>
        <p>Password: <input type="text" th:field="*{password}" /></p>
        <p><input type="submit" value="Submit" /></p>
    </form>
</body>
</html>
package me.whiteship.demospringsecurityform.account;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class SignUpController {

    @Autowired
    AccountService accountService;

    @GetMapping("/signup")
    public String signUpForm(Model model) {
        model.addAttribute("account", new Account());
        return "signup";
    }

    @PostMapping("/signup")
    public String processSignUp(@ModelAttribute Account account) {
        account.setRole("USER");
        accountService.createNew(account);
        return "redirect:/";
    }
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SignUpControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void signUpForm() throws Exception {
        mockMvc.perform(get("/signup"))
                .andDo(print())
                .andExpect(content().string(containsString("_csrf")));
    }

    @Test
    public void procesSignUp() throws Exception {
        mockMvc.perform(post("/signup")
            .param("username", "sh")
            .param("password", "123")
            .with(csrf()))
                .andExpect(status().is3xxRedirection());
    }
}

 

29. 로그아웃 처리 필터: LogoutFilter

ㅇ 여러 LogoutHandler를 사용하여 로그아웃시 필요한 처리를 하며 이후에는 LogoutSuccessHandler를 사용하여 로그아웃 후처리를 한다.

ㅇ LogoutHandler
 - CsrfLogoutHandler
 - SecurityContextLogoutHandler

ㅇ LogoutSeuccessHandler
 - SimpleUrlLogoutSuccessHandler

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

http.logout()
                .logoutUrl("/logout") // 파트 32
                .logoutSuccessUrl("/"); // 파트 29 로그아웃시 리다이렉션 페이지 커스텀
//                .invalidateHttpSession(true) // 로그아웃 후 HttpSession을 삭제 (기본값이 True라서 별도의 커스텀 불필요
//                .logoutSuccessHandler() // 로그아웃 완료시 별도의 핸들러 작업 수행
//                .addLogoutHandler() // 로그아웃시 부과적인 작업 수행
//                .deleteCookies() // 쿠키 기반의 로그인 사용시 로그아웃시 쿠키 삭제

 

30. 폼 인증 처리 필터 : UsernamePaaswordAuthenticationFilter

ㅇ 폼 로그인을 처리하는 인증 필터
 - 사용자가 폼에 입력한 username과 apssword로 Authentication을 만들고 AUthenticationManager를 사용하여 인증을 시도한다.
 - AuthenticationManager(ProviderManager)는 여러 AUthenticationProvider를 사용하여 인증을 시도하는데 그 중 DAOAUthenticationProvider는 UserDetailsService를 사용하여 UserDetails 정보를 가져오고 UsernamePasswordAuthenticationFilter는 Authentcation 정보와 실제 사용자 정보를 비교하여 인증을 처리한다.

 

31. DefaultLoginPageGeneratingFilter

ㅇ 기본 로그인 폼 페이지를 생성해주는 필터
 - GET /login 요청을 처리하는 필터

ㅇ 로그인 폼 커스터마이징

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

http.formLogin() // 파트 31, 32
            .loginPage("/login") // 로그인 페이지를 별도로 만들겠다. 이런경우 DefaultLoginPageGenerateFilter + LogoutFilter가 적용되지 않음
            .permitAll(); // 로그인 페이제에 PermitAll 설정이 필수적으로 필요함 (안할 경우 페이지가 안보임)
//                Form 로긴의 경우 기본적으로 세션유지(Stateful)하게 사용함 (BasicAuthentication을 사용할 경우 http Basic 요청을 수행 (Authorization)
//                .usernameParameter("my-username"); // 파라메터 이름을 커스텀할 수 있는데 이렇게 하면 프론트 단에서도 동일하게 맍춰줘야 함
//                .usernameParameter("my-sh") // username 파라메터명을 변경함
//                .passwordParameter("my-password"); // password 파라메터명을 변경함

 

32. 로그인/로그아웃 폼 커스터마이징

https://docs.spring.io/spring-security/site/docs/current/reference/html5/#jc-form

 

Spring Security Reference

In Spring Security 3.0, the codebase was sub-divided into separate jars which more clearly separate different functionality areas and third-party dependencies. If you use Maven to build your project, these are the modules you should add to your pom.xml. Ev

docs.spring.io

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>SignIn</title>
</head>
<body>
    <h1>Sign In</h1>
    <div th:if="${param.error}">
        <div class="alert alert-danger">
            Invalid username or password.
        </div>
    </div>
    <form action="/signin" th:action="@{/signin}" method="post">
        <p>Username: <input type="text" name="username" /></p>
        <p>Password: <input type="password" name="password" /></p>
        <p><input type="submit" value="SignIn" /></p>
    </form>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>SignIn</title>
</head>
<body>
    <h1>Logout</h1>
    <form action="/logout" th:action="@{/logout}" method="post">
        <p><input type="submit" value="Logout" /></p>
    </form>
</body>
</html>
http.formLogin()
            .loginPage("/signin")
            .permitAll();

- Security 설정

 

33. Basic 인증 처리 필터: BasicAuthenticationFilter

Basic 인증이란?
 - 요청 헤더에 username와 password를 실어 보내면 브라우저 또는 서버가 그 값을 읽어서 인증하는 방식. 예) Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l (sh:123 을 BASE 64)
 - 보통, 브라우저 기반 요청이 클라이언트의 요청을 처리할 때 자주 사용.
 - 보안에 취약하기 때문에 반드시 HTTPS를 사용할 것을 권장.

 

34. 요청 캐시 필터: RequestCacheAwareFilter

ㅇ 현재 요청과 관련있는 캐시된 요청이 있는지 찾아서 적용하는 필터
 - 캐시된 요청이 없다면 현재 요청을 처리
 - 캐시된 요청이 있다면 해당 캐시된 요청 처리

ㅇ 만약 로그인이 필요한 페이지 /dashboard 를 호출한 경우 로그인이 안되어 있을 경우 /login 페이지로 이동을 시키는 데 RequestChacheAwareFilter는 /dashboard 요청을 캐시에 담아두었다가 Login이 정상 처리되면 해당 페이지로 연결해주는 역할을 함

 

35. 시큐리티 관련 서블릿 스팩 구현 필터: SecurityContextHolderAwareRequestFilter

시큐리티 관련 서블릿 API를 구현해주는 필터
 -
HttpServletRequest#authenticate(HttpServletResponse)
 -
HttpServletRequest#login(String, String)
 -
HttpServletRequest#logout()
 -
AsyncContext#start(Runnable)

ㅇ Spring Security 관련 기능을 서블릿3 스펙으로 지원하는 기능

 

36. 익명 인증 필터: AnonymousAuthenticationFilter

https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#anonymous

 

Spring Security Reference

The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them

docs.spring.io

현재 SecurityContext에 Authentication이 null이면 “익명 Authentication”을 만들어 넣어주고, null이 아니면 아무일도 하지 않는다.

기본으로 만들어 사용할 “익명 Authentication” 객체를 설정할 수 있다

http.anonymous().principal("annoymouse_user"); // 파트 36 기본값은 annonymouseUser, Role값 등 변경 가능 굳이 쓸 필요가 없다
// 사용자 로그인 정보가 없다면 annonymouse token을 생성해서 넣어줌 Null Object Pattern( Null을 대변하는 객체를 넣어서 사용하는 패턴 )

https://en.wikipedia.org/wiki/Null_object_pattern

 

Null object pattern - Wikipedia

"Null object" redirects here. For the concept in category theory, see Initial object. In object-oriented computer programming, a null object is an object with no referenced value or with defined neutral ("null") behavior. The null object design pattern des

en.wikipedia.org

 

37. 세션 관리 필터: SessionManagementFilter

https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#session-mgmt

 

Spring Security Reference

The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them

docs.spring.io

세션 변조 방지 전략 설정: sessionFixation
 - 세션변조 : https://www.owasp.org/index.php/Session_fixation

 

Session fixation Software Attack | OWASP Foundation

Session fixation on the main website for The OWASP Foundation. OWASP is a nonprofit foundation that works to improve the security of software.

owasp.org

 - none
 - newSession
 - migrateSession (서블릿 3.0 컨테이너 사용시 기본값)
 - changeSessionId (서블릿 3.1 컨테이너 사용시 기본값)
 - https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#nsa-session-management-attributes

 

Spring Security Reference

The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them

docs.spring.io

ㅇ 유효하지 않은 세션을 리다이렉트 시킬 URL 설정
 - invalidSessionUrl

ㅇ 동시성 제어 : MaximumSession
 - 추가로 로그인을 막을지 여부 설정 (기본값 False)
 - https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#nsa-concurrency-control

 

Spring Security Reference

The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them

docs.spring.io

세션 생성 전략: sessionCreationPolicy
 - IF_REQURIED
 - NEVER
 - STATELESS
 - ALWAYS

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

//        http.sessionManagement() // 파트 37
//                .sessionFixation() // 세션방지 전략 설정 기본값은 changeSessionId 이다.
//                .changeSessionId() // 굳이 바꿀필요가 없다 이유 migrateSessionId보다 ChnageSessionId가 효율적이다.
//        .invalidSessionUrl("/login") // 유효하지 않은 세션이 접근했을때 어디로 보낼것인가?
//        .maximumSessions(1) // 1개의 계정에 대해 1개의 세션만 유지하겠다 (동시접속 불가)
//        .expiredUrl("/login") // 만약 1개의 계정에 2개의 세션이 생성되면 기존 세션은 expired되고 expiredURL으로 리디렉션됨
//        .maxSessionsPreventsLogin(true); // 기본값은 False, 1개의 계정에 2개의 세션이 생성되면 새로운 새션의 로그인을 막는다.

//        http.sessionManagement() // 파트 37
//                .sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 세션을 사용하지 않음 (로그인 후 인증이 유지안됨)
        // 여러 서버간의 세션공유를 위한 프로젝트 spring session (추후 찾아보기)

 

38. 인증/인가 예외 처리 필터: ExceptionTranslationFilter

https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#exception-translation-filter

 

Spring Security Reference

The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them

docs.spring.io

인증, 인가 에러 처리를 담당하는 필터
  - AuthenticationEntryPoint
 
- AccessDeniedHandler

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Access Denied</title>
</head>
<body>
    <h1><span th:text="${name}">Name</span>, you can't access to the resource.</h1>
</body>
</html>
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

        http.exceptionHandling() // 파트 38
//                .accessDeniedPage("/access-denied"); // User 친화적인 access-denied 페이지 작성후 access denied 발생시 해당 페이지로 이동
                    .accessDeniedHandler(new AccessDeniedHandler() {
                        @Override
                        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                            UserDetails principal = (UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                            String username = principal.getUsername();
                            System.out.println(username + " is denied to access " + request.getRequestURI());
                            response.sendRedirect("/access-denied");
                        }
                    });
@Controller
public class AccessDeniedController { // 파트 38

    @GetMapping("/access-denied")
    public String accessDenied(Principal principal, Model model){
        model.addAttribute("name", principal.getName());
        return "access-denied";
    }
}

 

39. 인가 처리 필터: FilterSecurityInterceptor

https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#filter-security-interceptor

 

Spring Security Reference

The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them

docs.spring.io

HTTP 리소스 시큐리티 처리를 담당하는 필터. AccessDecisionManager를 사용하여 인가를 처리한다.
HTTP 리소스 시큐리티 설정

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
    protected void configure(HttpSecurity http) throws Exception {


        http.antMatcher("/**") // Oder보다 AntMatcher를 사용하여 적절히 설정하는게 좋음
                .authorizeRequests() // 이 설정이 파트 39의 FilterSecurityInterceptor 이다.
                .mvcMatchers("/","info","/account/**", "signup").permitAll() // /, info 정보는 누구나 볼수있음
                .mvcMatchers("/admin").hasRole("ADMIN") // admin 페이지 접속시 ADMIN Role을 가지고 있어야함
                .mvcMatchers("/user").hasRole("USER") // 파트 16 USER 권한을 갖는 사용자가 USER 페이지 접속
                .anyRequest().authenticated() // 기타 다른 요청은 모두 로그인 필요
//                .anyRequest().rememberMe() // RememberMe를 사용하여 인증한 사용자에게 접근권한을 주겠다. (내 로그인을 기억)
//                .anyRequest().fullyAuthenticated() // RememberMe를 사용하여 인증한 사용자도 다시한번 로그인을 요청함 (중요 URL)
//                .anyRequest().anonymous() // 인증을 수행하고 접근하면 접근이 안됨 (anonymous만 접근가능)
                .accessDecisionManager(accessDecisionManager()) // 우리가 설정한 AccessDicisionManager를 사용하게 함
    //                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // 파트 22 Static 리소스의 스프링 시큐리티 미적용 두번째 방법
                // 위 방법은 추천하지 않음 -> 스태틱 리소스의 필터 15개를 다 태움 -> 성능상의 문제
                .and()// and를 굳이 쓰지 않아도 됨
                .formLogin()
                .and()
                .httpBasic(); // and 뒤에 오는것은 아래와 같이 별도로 사용가능

 

40. 토큰 기반 인증 필터 : RememberMeAuthenticationFilter

세션이 사라지거나 만료가 되더라도 쿠키 또는 DB를 사용하여 저장된 토큰 기반으로 인증을 지원하는 필터
RememberMe 설정

http.rememberMe()
        .userDetailsService(accountService)
        .key("remember-me-sample");

 

 

41. 커스텀 필터 추가하기

package com.h232ch.demospringsecurityh232ch.common;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StopWatch;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class LoggingFilter extends GenericFilterBean {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override // 파트 41 커스텀 필터 (GenericFilterBean) : 로깅 필터를 만들어서 적용해보기 (서블릿 필터를 적용하는 것과 마찬가지다)
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        StopWatch stopWatch = new StopWatch();
        stopWatch.start(((HttpServletRequest)servletRequest).getRequestURI());
        filterChain.doFilter(servletRequest, servletResponse); // 다음 필터로 넘겨줘야 함
        stopWatch.stop();
        logger.info(stopWatch.prettyPrint());

    }
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {

 http.addFilterBefore(new LoggingFilter(), WebAsyncManagerIntegrationFilter.class); // 파트 41 커스텀 필터

 

 

https://github.com/thymeleaf/thymeleaf-extras-springsecurity/blob/3.0-master/README.markdown

 

thymeleaf/thymeleaf-extras-springsecurity

Thymeleaf "extras" integration module for Spring Security 3.x and 4.x - thymeleaf/thymeleaf-extras-springsecurity

github.com

 

42. 타임리프 스프링 시큐리티 확장팩

ㅇ 의존성 추가

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

ㅇ Authentication과 Authorization 참조

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>

<h1 th:text="${message}">Hello</h1>
<!--화면의 모델객체를 표현할 때는 ${}를 사용, message가 존재하지 않는경우 Hello를 보여줌-->

<!--thymeleaf-extras-springsecurity5에서 제공하는 기능(authorization, authentication-->
<div th:if="${#authorization.expr('isAuthenticated()')}">
    <h2 th:text="${#authentication.name}">Name</h2>
    <a href="/logout" th:href="@{/logout}">Logout</a>
</div>
<div th:unless="${#authorization.expr('isAuthenticated()')}">
    <a href="/login" th:href="@{/login}">Login</a>
</div>
<!-- 파트 42 -->
<!-- isAuthenticated되어 있다면 authentication.name을 가져와서 화면에 보여주고
isAuthenticated가 아니면(unless) login 태그를 화면에 뿌려줌-->

</body>
</html>

 

44. sec 네임스페이스

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<!-- 파트 43 sec 방식으로 설정 -->
<h1 th:text="${message}">Hello</h1>

<div sec:authorize-expr="isAuthenticated()">
    <h2 sec:authentication="name">Name</h2>
    <a href="/logout" th:href="@{/logout}">Logout</a>
</div>
<div sec:authorize-expr="!isAuthenticated()">
    <a href="/login" th:href="@{/login}">Login</a>
</div>


</body>
</html>

 

45. @AuthenticationPrincipal


https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#mvc-authentication-principal

 

Spring Security Reference

The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them

docs.spring.io

웹 MVC 핸들러 아규먼트로 Principal 객체를 받을 수 있다.

package com.h232ch.demospringsecurityh232ch.account;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;
import java.util.List;

public class UserAccount extends User { // 파트 45 스프링 시큐리티가 제공하는 User를 상속받고 생성자를 상속받아 사용

    private Account account;
    // principal에 UserAccount가 생성되고 UserAccount 내에 account 값이 생성됨

    public Account getAccount() {
        return account;
    }

    public UserAccount(Account account) {
        super(account.getUsername(), account.getPassword(),  List.of(new SimpleGrantedAuthority("ROLE_"+account.getRole())));
        // User 생석자를 통해 id, username, pssword, role 이 UserAccount 객체에 기록됨
        this.account=account;
        //
    }
}
@Controller
public class SampleController {


    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private SampleService sampleService; // 파트 10

    @Autowired
    private BookRepository bookRepository;

public String index(Model model, @CurrentUser Account account){ // @CurrentUser 애노테이션을 사용하여 Account 정보를 가져옴

//        if(principal == null){
//            model.addAttribute("message", "Hello Spring Security"); // Key Value 형태로 모델에 String 객체를 넣어줌
//        } else {
//            model.addAttribute("message", "Hello, " + principal.getName());
//        }

        if(account == null){
            model.addAttribute("message", "Hello Spring Security"); // 파트 45 Spring Security Princial
        } else {
            model.addAttribute("message", "Hello, " + account.getUsername());
        }
        return "index";
    }
@Service
public class AccountService implements UserDetailsService {
    // UserDetailsService는 SpringSecurity에서 Authentication을 관리할 때
    // DAO(Data Access Object)인터페이스를 통해서 저장소에 저장되어있는 유저 정보를 읽어오는 인터페이스이다.
    // 이 인터페이스를 implements 하는 클래스를 빈으로등록만 하면 SpringSecurity가 자동으로 해당 Service를 UserDetailService로 사용함

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    AccountRepository accountRepository; // 만약 JPA를 사용하지 않는다면 여기에 DB를 연결하는 DAO 구현체가 와야함

@Override // 스프링 시큐리티로 유저 정보를 제공하는 역할 -> 실제 인증을 하는 인터페이스는 AuthentificationManager가 수행
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { // username을 받아와서 username이 해당하는 user정보를 가져와서 userDetails 타입으로 리턴하는 역할
        Account account = accountRepository.findByUsername(s); // 여기서 User를 꺼내오겠다.
        if(account == null){
            throw new UsernameNotFoundException(s);
            
        }
//        return User.builder() // SpringSecurity에서 제공하는 User빌더 (Account형의 User정보를 UserDetails형에 맞도록 User Builder를 통해 생성하여 리턴 (Builder를 사용한다는건 Lombok을 쓴다는것)
//                .username(account.getUsername())
//                .password(account.getPassword())
//                .roles(account.getRole()) // , "USER") 로 권한을 추가할 수도 있음 파트 16
//                .build();

        return new UserAccount(account); // 파트 45 build를 사용하지 않고 UserAccount로 객체를 만들어 쓰기
        // 위의 값은 SecurityContextHolder에 principal로 저장 이 principal을 @AuthenticationPrincipal로 받아와서 사용함
    }
package com.h232ch.demospringsecurityh232ch.common;

import org.springframework.security.core.annotation.AuthenticationPrincipal;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : account")
// AuthenticationPrincipal은 SecurityContextHolder에 저장되어 있는 Principal 값을 가져옴
// Principal은 User 생성 아답터 역할을 하는 UserAccount로 리턴을 받아 Principal에 자정함
// expression을 사용하면 UserAccount에 존재하는 account를 Principal로 변환해줌
// 결국 Principal인 UserAccount를 UserAccount내에 존재하는 account로 변환하여 최종적으로 account가 Principal의 값이되며
// 이 값은 java에서 애트리뷰트 리졸버로 사용되는 principal 값이 아닌 SecurityContextHolder에 저장되어있는 Principal으로서
// Account의 모든 기능을 자유롭게 사용할 수 있다.
public @interface CurrentUser {
}

 

46. 스프링 데이터 연동

https://docs.spring.io/spring-security/site/docs/current/reference/html5/#data

 

Spring Security Reference

In Spring Security 3.0, the codebase was sub-divided into separate jars which more clearly separate different functionality areas and third-party dependencies. If you use Maven to build your project, these are the modules you should add to your pom.xml. Ev

docs.spring.io

@Query 애노테이션에서 SpEL로 principal 참조할 수 있는 기능 제공.

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-data</artifactId>
    <version>${spring-security.version}</version>
</dependency>

@Query에서 principal 사용하기

@Controller
public class SampleController {


    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private SampleService sampleService; // 파트 10

    @Autowired
    private BookRepository bookRepository;


@GetMapping("/user") // 파트 16
    public String user(Model model, Principal principal){
        model.addAttribute("message", "hello User, " + principal.getName());
        model.addAttribute("books", bookRepository.findCurrentUserBooks()); // 파트 46
        return "user";
    }
    
    
package com.h232ch.demospringsecurityh232ch.book;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface BookRepository extends JpaRepository<Book, Integer> {

    @Query("select b from Book b where b.author.id = ?#{ principal.account.id }") // SecurityContextHolder에 존재하는 account.id를 가져오는것
            // AccountService의 loadUserByUsername에 리턴값(UserAccount)에 존재하는 account.id를 가져옮
//    @Query("select b from Book b where b.author.username = ?#{ principal.username }") // Username으로 구현
    List<Book> findCurrentUserBooks();
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>user</title>
</head>
<body>

<!--파트 16-->

<h1 th:text="${message}">Hello</h1>
<!--화면의 모델객체를 표현할 때는 ${}를 사용, message가 존재하지 않는경우 Hello를 보여줌-->

<tr th:each="book : ${books}">
    <td><span th:text="${book.title}"> Title </span></td>
</tr>


</body>
</html>

 

댓글