스프링 시큐리티의 인증 구현
- 우리는 사용자 관리를 위해서
UserDetailsService
및UserDetailsManager
인터페이스를 이용합니다. 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(); } // 나머지 값들 }
- Entity에
스프링 시큐리티가 사용자를 관리하는 방법 지정
- 스프링 시큐리티는 새 사용자를 추가하거나, 사용자를 관리 할때,
UserDetailsService
라는 특정 구성 요소로 인증 프로세스가 사용자의 관리를 위임합니다. 그래서 우리는 이를 구현하는 다양한 방법을 사용할 것 입니다. - 그중
JdbcUserDetailsManager
를 이용해서 예제 프로젝트를 작성할 것이다.
UserDetailsService 인터페이스
- 인증구현은
loadUserByUsername
메서드를 호출해서 주어진 사용자 이름을 가진 사용자의 세부정보를 얻을 수 있다.- 만약, 존재하지 않는다면
UsernameNotFoundException
이 발생하게 된다. - 이는
RuntimeException
이다.
- 만약, 존재하지 않는다면
UserDetailsService의 계약 구현
- 이제
UserDatailsService
를InMememoryDetailsManager
를 사용해서 구현해보도록 하겠습니다.
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
라는 유저의 수정, 삭제, 추가 등을 관리하는 인터페이스를 구현하는 방법을 살펴보겠습니다.
- 우리가 위에서 사용한
InMemoryUserDetailsManager
는UserDetailsManager
를 구현한 것 입니다. 이제 우리는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
- 스프링 시큐리티 인 액션 - 로렌티우 스필카
'Spring > 스프링 시큐리티' 카테고리의 다른 글
[스프링 시큐리티] 소신대로 Spring Security JWT 방식 구현하기 (1) (0) | 2023.03.27 |
---|---|
스프링 시큐리티 인 액션 (1) (0) | 2023.01.31 |