스프링 시큐리티의 인증 구현

  • 우리는 사용자 관리를 위해서 UserDetailsServiceUserDetailsManager 인터페이스를 이용합니다.
  • UserDetailsService
    • 사용자의 이름으로 사용자를 검색하는 역할을 함
  • UserDetailsManager
    • 대부분의 애플리케이션에 필요한 사용자 추가, 수정, 삭제 작업을 함

→ 이렇게 인터페이스를 분리해서 유연성을 향상시켰음

사용자 기술하기

UserDetails 인터페이스

  • UserDetails 인터페이스는 다음과 같은 메서드로 이루어져있다. 우리는 이를 구현할 예정이다.
    • getPassword() : 사용자의 패스워드를 반환한다.
    • getUsername() : 사용자의 이름을 반환한다.
    • getAuthorities() : 사용자에게 부여된 권한의 그룹을 반환한다.
    • 나머지들은 사용자가 애플리케이션의 리소스에 접근할 수 있도록 권한을 부여하는 메서드이다.
      • 계정 만료
      • 계정 잠금
      • 자격 증명 만료
      • 계정 비활성화
      • 우리는 이들을 이용해서 true 를 반환하게 재정의 하여, 사용자에 대한 제한을 둘 수 있습니다.
        • 기능을 구현할 필요가 없다면 모두 true 를 반환하게 하면 됩니다.

GrantedAuthority 인터페이스

  • 스프링 시큐리티에서는 GrantedAthority 를 통해서 인터페이스로 권한을 나타냅니다.

  • 권한을 만들기 위해서는 해당 이용 권리의 이름을 찾으면 됩니다.
  • 우리는 부여한 이름을 바탕으로 권한 부여 규칙을 작성합니다.
  • 보통은 람다식을 통해서 이러한 권한을 만듭니다 혹은 SimpleGrantedAuthority 클래스를 이용합니다.
    • 이런 람다식을 사용할때는 @FunctionalInterface 어노테이션으로 인터페이스가 함수형인것을 지정하는것이 좋습니다.
GrantedAuthority g1 = () -> "READ";
GrantedAuthority g2 = new SimpleGrantedAuthority("READ");

각각의 구현을 작성

  • 첫 번째로는 UserDetails 에 대한 구현을 작성해보도록 하겠습니다.
package com.example.example1;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

public class DummyUser implements UserDetails {
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(() -> "READ");
    }

    @Override
    public String getPassword() {
        return "12345";
    }

    @Override
    public String getUsername() {
        return "haechan";
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • getAuthorities() 에서는 람다식을 통해 READ 를 반환하도록 만듭니다.
  • getUsername()getPassword() 에는 원하는 값을 반환하게 만듭니다.
    • 하지만, 실질적인 User는 다음과 같이 생성자를 통해서 값을 받고 반환하도록 만들어야합니다.

  • 나머지는 모두 true 를 반환하도록 합니다.
  • 이러한 것을 통해서 UserDetail 인스턴스를 생성할 수 있습니다.
User.UserBuilder builder1 = User.withUsername("haechan");
        UserDetails u1 = builder1.password("12345")
                .authorities("read", "write")
                .passwordEncoder(p -> encode(p))
                .accountExpired(false)
                .disabled(true)
                .build();
  • 우리는 이러한 User 를 Entity를 통해서 저장한다고 한다. 그러면 어떻게 해야할까?
    • Entity에 UserDetails 를 구현하는 방법이있다. → 이는 잘못된 사례다 Entity에 여러가지 관계도 추가되오 인터페이스의 메서드도 구현해야하기 떄문에 많이 복잡해진다.
    • 깔끔하게 책임을 가지는것은 Entity를 생성하고 UserDetails 를 구현하는 새로운 클래스에 맴버변수로 엔티티를 가지는것이 가장 좋은 코드라고 볼 수 있다.
    • ublic class DummyUser implements UserDetails { private final User user; public DummyUser(User user) { this.user = user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(() -> user.getAuthority()); } @Override public String getPassword() { return this.user.getPassword(); } @Override public String getUsername() { return this.user.getUsername(); } // 나머지 값들 }

스프링 시큐리티가 사용자를 관리하는 방법 지정

  • 스프링 시큐리티는 새 사용자를 추가하거나, 사용자를 관리 할때, UserDetailsService 라는 특정 구성 요소로 인증 프로세스가 사용자의 관리를 위임합니다. 그래서 우리는 이를 구현하는 다양한 방법을 사용할 것 입니다.
  • 그중 JdbcUserDetailsManager 를 이용해서 예제 프로젝트를 작성할 것이다.

UserDetailsService 인터페이스

  • 인증구현은 loadUserByUsername 메서드를 호출해서 주어진 사용자 이름을 가진 사용자의 세부정보를 얻을 수 있다.
    • 만약, 존재하지 않는다면 UsernameNotFoundException 이 발생하게 된다.
    • 이는 RuntimeException 이다.

 

UserDetailsService의 계약 구현

  • 이제 UserDatailsServiceInMememoryDetailsManager 를 사용해서 구현해보도록 하겠습니다.
package com.example.example1;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

public class User implements UserDetails {
    private final String username;
    private final String password;
    private final String authority;

    public User(String username, String password, String authority) {
        this.username = username;
        this.password = password;
        this.authority = authority;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(() -> authority);
    }

    @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 를 정의를 하고 이제 인터페이스를 구현해봅시다.
package com.example.example1;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.List;

public class InMemoryUserDetailsService implements UserDetailsService {

    private final List<UserDetails> users;

    public InMemoryUserDetailsService(List<UserDetails> users) {
        this.users = users;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return users.stream()
                .filter(user -> user.getUsername().equals(username))
                .findFirst()
                .orElseThrow(
                        () -> new UsernameNotFoundException("유저를 찾을 수 없습니다.")
                );
    }
}
  • 이제 구성 클래스를 빈으로 추가하고 사용자를 등록해보도록 하겠습니다
package com.example.example1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.List;

@Configuration
public class ProjectConfig {

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = new User("haechan", "12345", "read");
        List<UserDetails> users = List.of(user);
        return new InMemoryUserDetailsService(users);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}
  • 이후 실행을 해보면 제대로 나오는 것을 알 수 있다.

UserDetailsManager 인터페이스 구현

  • 이제 UserDetailsManager 라는 유저의 수정, 삭제, 추가 등을 관리하는 인터페이스를 구현하는 방법을 살펴보겠습니다.

  • 우리가 위에서 사용한 InMemoryUserDetailsManagerUserDetailsManager 를 구현한 것 입니다. 이제 우리는 JdbcUserDetailsManager를 이용하도록 하겠습니다.

JdbcUserDetailsManager

  • 이는 SQL 데이터베이스에 저장된 사용자를 관리하고, JDBC를 이용해서 데이터베이스에 직접 연결을합니다.

  • 우선 테이블가 데이터를 넣습니다.
    • 외래키 관련은 테스트를 위함으로 뺴서 작성을 하였습니다.
create table if not exists users(
    id int not null auto_increment,
    username varchar(45) not null,
    password varchar(45) not null,
    enabled int not null,
    primary key(id)
);

create table if not exists authorities(
    id int not null auto_increment,
    username varchar(45) not null,
    authority varchar(45) not null,
    primary key (id)
);

insert ignore into authorities values(NULL, 'haechan', 'write');
insert ignore into users values(NULL, 'haechan', '12345', '1');
  • 이후 사용할 의존성을 추가해준다.
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
            <scope>runtime</scope>
        </dependency>
    </dependencies>
  • 의존성을 설치했으면, db정보를 넣는다.
spring.datasource.username=root
spring.datasource.url=jdbc:mysql://localhost/security?useUnicode=true&serverTimezone=Asia/Seoul
spring.datasource.password=root
spring.sql.init.mode=always
  • mysql 8.0 이상부터는 serverTimezone 설정을 깜박하면, 에러가생긴다.
  • 이제 JdbcUserDetailsManager 를 등록한다.
package com.example.example1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;

import javax.sql.DataSource;
import java.util.List;

@Configuration
public class ProjectConfig {

    @Bean
    public UserDetailsService userDetailsService(DataSource dataSource) {
        return new JdbcUserDetailsManager(dataSource);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

  • 이후 실행하면 제대로 된 값이 나오는 것을 알 수 있다.
  • 우리는 이에대한, query 값도 변경이 가능하다. 사실 실생활에서는 username으로 판단할 수도 있지만, email로 판단할수도 있고 여러가지 경우가 존재한다.
@Bean
    public UserDetailsService userDetailsService(DataSource dataSource) {

        String userByUsernameQuery = "SELECT * from users where username = ?";
        String authsByUserQuery = "SELECT * from authorities where username = ?";

        JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource);
        userDetailsManager.setUsersByUsernameQuery(userByUsernameQuery);
        userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);

        return userDetailsManager;
    }
  • 다음처럼, 명시적으로 setter 등을 통해서 query를 변경할 수 있다.

Reference

  • 스프링 시큐리티 인 액션 - 로렌티우 스필카

+ Recent posts