SpringSecurity 이용하기 - 1 : https://galid1.tistory.com/688 (custom login form 구현)
SpringSecurity 이용하기 - 2 : https://galid1.tistory.com/698 (login Success Handle)
SpringSecurity 이용하기 - 3 : https://galid1.tistory.com/699 (logout 기능 추가하기)
SpringSecurity 이용하기 - 4 : 현재 포스트 (database를 이용한 로그인 구현)
이번 포스팅에서는, Spring Security 로그인시, 직접 구축한 Database의 User 정보를 이용해 로그인하는 방법을 알아보도록 하겠습니다.
Core Concepts : https://docs.spring.io/spring-security/site/docs/3.0.x/reference/technical-overview.html
Architecture : https://spring.io/guides/topicals/spring-security-architecture
이번 포스팅을 시작하기 앞서 위글을 읽고 Spring Security가 동작하는 전반적인 개념을 숙지하신 뒤에 진행하신다면 더욱 이해가 쉽습니다.
SpringSecurity 사용자 인증 개념
SpringSecurity 이용하기 관련글을 4개나 포스팅하면서, 이제야 SpringSecurity의 Concept에 대해 알아보려 합니다. SpringSecurity를 이용함으로써 얻는 편의를 알기도 전에, 이러한 내부의 개념들을 익혀나간다면, 기반지식이 없기 때문에 당연히 이해가 더 어려울 것으로 판단되어, 우선은 간단한 기능들을 사용해보고, 알려드리려 했습니다.
1. SecurityContextHolder, Authentication
SecurityContextHolder
는 Application과 상호작용하는 보안주체의 정보가 저장되는 공간입니다. SpringSecurity에서는 이러한 보안주체를 Authentication
을 이용하여 나타냅니다.
현재 인증된 사용자의 정보 가져오기
xObject principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
위의 코드를 이용하여 우리는 현재 인증된 사용자의 정보를 가져올 수 있습니다. SecurityContextHolder.getContext()
는 SecurityContext
를 반환하며, SecurityContext
는 현재 스레드와 관련된 보안정보를 정의합니다.
2. UserDetailsService
2.1 UserDetails
Authentication으로부터 Principal을 가져올 수 있습니다. 다시 Principal은 UserDetails
로 캐스팅이 가능합니다. UserDetails
는 우리의 Application Database와 Spring Security의 SecurityContextHolder 사이의 일종의 어댑터입니다.
UserDetails -> Spring Security
예를들어 Spring Security에서 인증에 성공시 SecurityContextHolder
에 Authentication
을 저장이 되는데요, UserDetails
는 이 Authentication
객체를 만드는데에 사용이 됩니다.
UserDetails -> Application Database
UserDetails
는 또한 User Database를 나타내는 역할을 합니다. 따라서 비즈니스 모델 객체로 캐스팅하여 사용하기도 합니다.
2.2 UserDetailsService
그렇다면 위와 같은 UserDetails
를 어떻게 제공하고, 언제 제공해야할까요?
Spring Security에서는 사용자가 로그인을 시도할시, 해당 로그인 정보와 Backend(직접 구축한 서버)
에 존재하는 비교대상을 찾는 방법을 구현하기위해, UserDetailsService interface
를 제공합니다.
UserDetails를 Spring Security에 제공하는 방법은 UserDetailsService
를 구현하여, loadUserByUsername(String username);
를 오버라이딩 하면 됩니다. loadUserByUsername()
메소드는 Spring Security에서 사용자정보를 로드하는 일반적인 방법입니다.
인증에 성공한다면, UserDetails
는 SecurityContextHolder에 저장될Authentication
객체를 만드는데 됩니다.
3. GrantedAuthority
Authentication
으로부터 제공되는 또다른 메소드 중 getAuthorites()
란 것이 있습니다. 이 메소드는 GrantedAuthority
라는 객체의 배열을 반환합니다.
이 GrantedAuthority
는 객체의 이름에서 쉽게 알 수 있듯이, 부여된 권한을 나타내는 객체입니다.
Spring Security Database User Login(with JPA) 구현
전체적인 그림을 먼저 인지하고 진행하시는 것이 도움이 될것같아 우선, 전체적인 파일 구조를 보여드립니다.
1. jdbc 설정
실습에 사용될 database는 mysql
입니다. mysql을 운영체제에 맡게 우선 설치를 했다고 가정한뒤, 진행하겠습니다.
1.1 database 생성 및 table생성
먼저 database를 생성하기위해 mysql을 실행합니다. mysql workbench를 사용하면 시각적으로 database를 볼 수 있어 더욱 좋습니다.
testdb를 생성합니다.
xxxxxxxxxx
use testdb; #testdb를 사용함을 지정합니다.
create table user (
user_id int primary key auto_increment,
name varchar(255),
password varchar(255),
role varchar(255)); # Spring security에서 사용될 user의 role입니다.
위의 쿼리를 통해 user 테이블을 생성합니다.
1.2 mysql connector 의존성 추가
jdbc를 사용하기위해, build.gradle
을 열어 다음과같이 의존성을 추가합니다.
(최신 mysql connector 의존성은 https://mvnrepository.com/artifact/mysql/mysql-connector-java 에서 구하실 수 있습니다.)
1.3 database configuration class 생성
xxxxxxxxxx
public class DatabaseConfig {
public DataSource dataSource() {
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
dataSourceBuilder.driverClassName("com.mysql.cj.jdbc.Driver");
dataSourceBuilder.username("root");
dataSourceBuilder.password("1234");
dataSourceBuilder.url("jdbc:mysql://localhost:3306/testdb?useSSL=false&serverTimezone=UTC");
return dataSourceBuilder.build();
}
}
driverClassName : com.mysql.cj.jdbc.Driver
mysql 5.x
에서 8.x
로 버전이 바뀌면서, jdbc에 필요한 설정 몇가지가 변경되었습니다. 원래는 com.mysql.jdbc.Driver
였던 driverClassName이 위처럼 변경이 되었습니다.
url: jdbc:mysql://DOMAIN:3306/DATABASE_NAME?serverTimezone=UTC
기존과 동일하지만 serverTimezone설정을 해주지 않는다면 에러가 발생합니다.
1.4 Database 연결 테스트
연결에 성공하면 콘솔창에 위와같이 나타납니다.
2. JPA 설정
database 프로그래밍을 위해서, 저는 JPA를 사용하도록 하겠습니다.
2.1 의존성 설정
의존성 설정
JPA를 이용하기 위해 의존성을 위 그림과같이 추가합니다.
https://galid1.tistory.com/531
추가적으로, 가독성 향상 및 코드 수정을 편리하게 할수있도록 도와주는 lombok
을 추가해줍니다. lombok에 대한 설정방법과 설명은 위 링크를 참조해주세요.
2.2 JPA관련 설정
x
server
port80
################ 이 부분을 주목하세요.######################
spring
jpa
show-sqltrue
hibernate
ddl-auto update
properties
dialect
org.hibernate.dialect.MySQL5InnoDBDialect
##########################################################
handlebars
suffix .html
expose-session-attributestrue
application.yml
을 열고 위의 설정을 추가합니다.
hibernate: ddl-auto: update
필요한 table들을 생성하기위해, 자동으로 ddl(Data Definition Language)
쿼리를 만들어 database에 요청합니다.
show-sql
jpa에의해서 자동으로 생성되는 sql을 콘솔에 출력해주는 설정입니다.
properties: dialect: org.hibernate.dialect.MySQL5InnoDBDialect
jpa 구현체에서 사용할 방언을 지정합니다. database별로 sql이 달라지기 때문에(예를들면, pagination 쿼리 등..)이를 위해 특정 방언을 사용하도록 지정합니다.
2.3 User Entity 생성
xxxxxxxxxx
name = "user") (
access = AccessLevel.PROTECTED) (
public class UserEntity {
strategy = GenerationType.AUTO) (
private long userId;
private String name;
private String password;
private String role;
public UserEntity(String name, String password, String role) {
this.name = name;
this.password = password;
this.role = role;
}
}
우리가 만들 Server에서 User를 표현하기위해 User Entity를 생성합니다. 이 UserEntity는 Spring Security
의 필요에 따라서, Authentication
으로 변환되기도, Application에서 사용되기도 합니다. 한줄한줄 살펴보겠습니다.
JPA
아래의 어노테이션들은 jpa에 관련한 코드들입니다.
@Entity
해당 클래스가 Entity임을 알려줍니다. 즉 이 클래스는 Database의 table
에 맵핑됩니다. 이 클래스에 존재하는 field들이 table의 column에 맵핑됩니다.
@Table(name = "user")
database에 저장될 테이블의 이름을 user
로 설정합니다.
lombok
아래의 어노테이션들은 lombok에 관련한 코드들입니다.
@Getter
내부의 field들에 대한 getter() 메소드를 자동으로 생성해줍니다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
아무런 파라미터가 없는 기본 생성자를 생성해줍니다. JPA에서는 프록시 생성을 위해서 기본생성자
를 필요로하기 때문에, 이 어노테이션을부여합니다.
access는 이 기본생성자에 대한 접근제어설정을 합니다. default는 PUBLIC
인데요, 기본생성자의 경우, 엔티티에 필수적으로 생성해야하는 값을 생성하지 않기 때문에, 외부에 공개한다면 위험할 수 있습니다. 따라서 PROTECTED
로 지정합니다.
@Builder
생성자에 @Builder
를 부여하는 경우, 생성자에 필요한 파라미터들을 포함하는 builder를 자동으로 생성해줍니다.
@Id
이 어노테이션이 부여된 field를 Database table의 primary key
로 지정합니다.
@GeneratedValue(strategy = GenerationType.AUTO)
@Id
어노테이션과 같이 사용되며, primary key
의 생성 전략을 정의합니다. GenerationType.AUTO
는 새로운 row가 추가됨에 따라 자동으로 값이 증가하며 부여되도록합니다. 즉 Database의 auto_increment
옵션이 부여되었다고 생각하면됩니다.
2.4 User Repository 생성
xxxxxxxxxx
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByName(String name);
}
spring data jpa
에서는 java에서 database 프로그래밍시 반복되는 코드들을 줄여주는 편리한 기능을 제공합니다. 위와 같이 interface를 생성한뒤, JpaRepository<Entity, PrimaryKey>
를 상속받으면, 기본적인 CRUD(CREATE, READ, UPDATE, DELETE)
를 위한 메소드가 자동으로 생성됩니다.
Optional
Spring Data Jpa
는 또한, 메소드의 이름과 return Type을 이용하여, 자동으로 Database 관련한 기능이 구현된 메소드를 생성해줍니다.
예를들어 위와 같이 Optional<UserEntity> findByName(String name)
로 작성된 메소드의 이름을 추론하여, select from user where name = "파라미터"
라는 쿼리를 Database에 요청하며, 이를 return Type인 Optional<UserEntity>
로 반환하는 메소드를 자동으로 생성해줍니다.
이 메소드는 사용자가 form Login
시 요청한 id(name)를 이용해 Spring Security가 인증을 위해 Database에 존재하는 User정보를 찾을 때 사용됩니다.
2.5 user table 확인
SpringBoot Application을 실행한뒤, mysql workbench에서 database를 확인합니다.
우리가 작성한, UserEntity의 내용대로 table이 생성된 것을 볼 수 있습니다.
3. 로그인을 위한 사용자 추가
이제 Database에 사용자를 추가 해보도록 하겠습니다.
3.1 Controller 수정
이전 포스팅에서 작성했던 Controller에 /signUp
에 대한 요청을 처리하는 핸들러를 추가합니다. 이 핸들러에서는 회원가입 페이지를 작성하기 귀찮기 때문에, 코드상에서 사용자를 생성하도록합니다.
x
public class MainController {
private UserRepository userRepository;
private PasswordEncoder passwordEncoder;
"/signUp") (
public String signUp() {
UserEntity user = UserEntity.builder()
.name("galid")
.password(passwordEncoder.encode("1234"))
.role("user")
.build();
userRepository.save(user);
return "redirect:/login";
}
... // 이전 포스팅을 참조하시길 바랍니다.
}
먼저 UserRepository, PasswordEncoder를 주입받습니다.
그다음, UserEntity를 생성한뒤, userRepository를 이용해 entity를 저장합니다. 이렇게되면, table에 user row가 추가됩니다. password 저장시, passwordEncoder를 통해, 인코딩된 암호가 database에 저장되도록 합니다.
마지막으로, signUp요청을 한뒤에는, login페이지로 리다이렉트 시킵니다.
3.2 Spring Security 설정 추가
x
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.antMatchers("/login", "/signUp")
.permitAll()
... // 이전 포스팅을 참고하세요.
}
...
}
사용자가 /signUp
에 접근할 수 있도록 antMathcers()에 /signUp
엔드포인트를 추가합니다.
3.3 Database 확인
어플리케이션을 실행한뒤, /signUp
으로 요청합니다. 우리가 생성한 코드대로 user가 생성된뒤, login페이지로 리다이렉트 되어집니다.
user가 추가된것을 확인할 수 있습니다. 이제 위에 추가한 user를 이용해 로그인을 할 수 있도록 해보겠습니다.
4. Spring Security에서 Database 정보를 이용하도록 만들기
xxxxxxxxxx
public class BackedLoginService implements UserDetailsService {
private UserRepository userRepository;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity user = userRepository.findByName(username).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저입니다."));
return new User(user.getName(), user.getPassword(), Arrays.asList(new SimpleGrantedAuthority(user.getRole())));
}
}
방법은 간단합니다. UserDetailsService
를 구현하는 클래스를 만들고, 이를 Bean으로 등록하면 준비는 끝입니다.
loadUserByUsername(String username)
이 메소드는 spring security에서 사용자가 로그인할때 인증을 위해 사용하는 메소드입니다. username
파라미터에는 사용자가 로그인시 form의 id parameter에 담긴 내용이 담기게됩니다.
따라서 이 id를 이용해 database에서 사용자를 가져오고, database에서 가져온 정보를 이용해 UserDetails
를 만들어 return하면 됩니다.
5. 최종 Test
어플리케이션을 실행한뒤, 로그인페이지로 접근하여, 생성한 user정보를 이용해 로그인을 시도합니다.
성공적으로 로그인이 이루어지는것을 볼 수 있습니다.
'FrameWork > Spring Security' 카테고리의 다른 글
Spring Security - Spring MVC Test하기(@WithMockUser, form login 리다이렉션 해결) (0) | 2020.08.10 |
---|---|
Spring Security - SpringSecurity 이용하기 3 (logout 기능 추가하기) (0) | 2020.02.23 |
Spring Security - SpringSecurity 이용하기 2 (login Success Handle) (0) | 2020.02.22 |
Spring Security - SpringSecurity 이용하기 1 (custom login form 구현) (2) | 2020.02.12 |
SpringSecurity - Kakao OAuth2 Client 사용하기 (8) | 2019.07.06 |