spring-project

Spring Security Custom UserDetailsService 구현하기

가는가래 2019. 2. 24. 21:39

Spring Security Custom UserDetailsService(DB) 구현하기 


이전에는 다음과 같이 USER를 메모리에 저장해놓고 Spring Security의 테스트를 하였다. 

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// add our Users for in memory authentication
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}


하지만 실제로는 이렇게 사용하지 못하고 대부분 DB 등의 저장소에 사용자의 정보를 저장해놓고 사용해야 한다. 


여기서는 DB에 저장해놓은 데이터를 JPA를 사용하여 가져오는 것을 구현하고자 한다. 


application.properties

#JPA Configuration
spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none



mysql DB script

CREATE SCHEMA `quickguide` DEFAULT CHARACTER SET utf8mb4 ;

CREATE TABLE `user` (
`seq` int(11) NOT NULL,
`enabled` bit(1) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`username` varchar(255) DEFAULT NULL,
PRIMARY KEY (`seq`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;

CREATE TABLE `authority` (
`seq` int(11) NOT NULL,
`authority` varchar(255) DEFAULT NULL,
`username` varchar(255) DEFAULT NULL,
PRIMARY KEY (`seq`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4;

-- INSERT INTO `quickguide`.`users` (`seq`, `enabled`, `password`, `username`) VALUES ('1', 1, '1234', 'test');
INSERT INTO `quickguide`.`user` (`seq`, `enabled`, `password`, `username`) VALUES ('1', 1, '$2a$10$5rWSd1pL7FIbB1RVmM.2c.hXGowUjz0T/V1I.GlWGY7lVg4AKPxvu', 'test');
INSERT INTO `quickguide`.`authority` (`seq`, `authority`, `username`) VALUES ('1', 'USER', 'test');



JPA Entity

package com.quickguide.backend.authentication.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class User {

@Id
@Column(name = "seq")
@GeneratedValue(strategy = GenerationType.AUTO)
private int seq;

@Column(name = "username")
private String username;

@Column(name = "password")
private String password;

@Column(name = "enabled")
private boolean enabled;

}
package com.quickguide.backend.authentication.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Authority {

@Id
@Column(name = "seq")
@GeneratedValue(strategy = GenerationType.AUTO)
private int seq;

@Column(name = "username")
private String username;

@Column(name = "authority")
private String authority;
}



JPA Repository

package com.quickguide.backend.authentication.repository;

import com.quickguide.backend.authentication.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UsersRepository extends JpaRepository<User, Integer> {
User findByUsername(String username);
}
package com.quickguide.backend.authentication.repository;

import com.quickguide.backend.authentication.entity.Authority;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface AuthoritiesRepository extends JpaRepository<Authority, Integer> {
List<Authority> findByUsername(String username);
}



Spring Security Config 클래스

package com.quickguide.backend.config;

import com.quickguide.backend.authentication.service.QuickGuideUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private QuickGuideUserDetailsService quickGuideUserDetailsService;

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/auth/login")
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.logout()
.permitAll();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// add our Users for in memory authentication
// auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
auth.userDetailsService(quickGuideUserDetailsService);
}

@Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}
}


1) 기존에 사용자 정보를 메모리에 저장하는 부분을 주석처리 하고 DB에서 사용자 정보를 가져오는 클래스로 변경한다.

        auth.userDetailsService(quickGuideUserDetailsService);

2) 패스워드 인코딩 클래스 등록 (사용자가 입력한 패스워드가 DB에 저장되어 있는 패스워드와 동일한지 판단)

    @Bean
public PasswordEncoder encoder() {
return new BCryptPasswordEncoder();
}



사용자 정의 UserDetails 

package com.quickguide.backend.authentication.domain;

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

import java.util.Collection;

@Data
public class QuickGuideUser implements UserDetails {

private String username;
private String password;
private boolean isEnabled;
private boolean isAccountNonExpired;
private boolean isAccountNonLocked;
private boolean isCredentialsNonExpired;
private Collection<? extends GrantedAuthority> authorities;
}



QuickGuideUserDetailsService - 사용자 정의 UserDetailsService

package com.quickguide.backend.authentication.service;

import com.quickguide.backend.authentication.domain.QuickGuideUser;
import com.quickguide.backend.authentication.entity.Authority;
import com.quickguide.backend.authentication.entity.User;
import com.quickguide.backend.authentication.repository.AuthoritiesRepository;
import com.quickguide.backend.authentication.repository.UsersRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

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

@Slf4j
@Service
public class QuickGuideUserDetailsService implements UserDetailsService {

@Autowired
private UsersRepository usersRepository;

@Autowired
private AuthoritiesRepository authoritiesRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = usersRepository.findByUsername(username);

if (user == null) {
throw new UsernameNotFoundException(username + "is not found.");
}

QuickGuideUser quickGuideUser = new QuickGuideUser();
quickGuideUser.setUsername(user.getUsername());
quickGuideUser.setPassword(user.getPassword());
quickGuideUser.setAuthorities(getAuthorities(username));
quickGuideUser.setEnabled(true);
quickGuideUser.setAccountNonExpired(true);
quickGuideUser.setAccountNonLocked(true);
quickGuideUser.setCredentialsNonExpired(true);

return quickGuideUser;
}

public Collection<GrantedAuthority> getAuthorities(String username) {
List<Authority> authList = authoritiesRepository.findByUsername(username);
List<GrantedAuthority> authorities = new ArrayList<>();
for (Authority authority : authList) {
authorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
}
return authorities;
}

}


1) @Slf4j 은 Lombok에서 제공하는 어노테이션으로 log 관련 된 부분이므로 삭제해도 무방하다.

2) 사용자 UserDetailsService 를 구현하기 위해서는 UserDetailsService 인터페이스를 상속받아야 한다.

3) 유저의 상태를 저장하는 부분은 true 로 설정해 놓았다. (해당 부분의 사용에 대한 부분은 나중에 정리) 

- 실제 운영에서는 유용하게 사용할 수 있다. (접속 30일 이상 지난 사용자는 Lock 처리 등)

        quickGuideUser.setEnabled(true);
quickGuideUser.setAccountNonExpired(true);
quickGuideUser.setAccountNonLocked(true);
quickGuideUser.setCredentialsNonExpired(true);


이제 http://localhost:8080/ 입력 후 test / 1234 를 입력하면 로그인이 된다.