Spring Security with JDBC(UserDetailsService)

By | 2021년 7월 17일
Table of Contents

Spring Security with JDBC(UserDetailsService)

전체소스

전체소스는 여기 에 공개되어 있습니다.

프로젝트 생성

웹서비스에 보안 적용 을 기초로 시작합니다.

의존성 추가

build.gradle 에 jdbc, jpa, mysql-connector 를 추가해 줍니다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    runtimeOnly 'mysql:mysql-connector-java'

    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    implementation 'org.springframework.security:spring-security-test'
}

application.properties 설정

# spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/db_example
spring.datasource.username=springuser
spring.datasource.password=ThePassword
spring.datasource.driver-class-name =com.mysql.jdbc.Driver
# spring.jpa.show-sql: true

User 추가

User.java

@Getter
@Setter
@Entity
@Table(name = "tbl_user", catalog = "db_example")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @OneToMany(cascade=CascadeType.ALL,fetch = FetchType.EAGER)
    @JoinColumn(name="uid")
    private List<UserRole> roles;
}

UserRepository.java

public interface UserRepository extends JpaRepository<User, Long> {

    User findByUsername(String username);
}

UserRole.java

@Getter
@Setter
@Entity
@Table(name = "tbl_userrole", catalog = "db_example")
public class UserRole {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    private Long uid;

    @Column(nullable = false)
    private String rolename;
}

UserRoleRepository.java

public interface UserRoleRepository extends JpaRepository<UserRole, Long> {
}

UserDetailsService 추가

MyUserDetailsService.java

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        return new MyUserDetails(user);
    }
}

MyUserDetails.java

public class MyUserDetails implements UserDetails {
    private final User user;

    public MyUserDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> roles = new ArrayList<GrantedAuthority>();

        for (UserRole role : this.user.getRoles()) {
            roles.add(new SimpleGrantedAuthority(role.getRolename()));
        }

        return roles;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
CREATE TABLE `tbl_user` (
    `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `username` VARCHAR(255) NOT NULL,
    `password` VARCHAR(255) NOT NULL
)
COLLATE='utf8_general_ci'
ENGINE=INNODB;

INSERT INTO tbl_user(username, PASSWORD)
VALUES('111', '{noop}222');

CREATE TABLE `tbl_userrole` (
    `id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `uid` INT(11) NOT NULL,
    `rolename` VARCHAR(255) NOT NULL
)
COLLATE='utf8_general_ci'
ENGINE=INNODB;

INSERT INTO tbl_userrole(uid, rolename)
VALUES(1, 'ROLE_ADMIN');

INSERT INTO tbl_userrole(uid, rolename)
VALUES(1, 'ROLE_USER');

WebSecurityConfig 수정

기존에 userDetailsService() 를 제거합니다.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    .antMatchers("/", "/home").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .and()
                .logout()
                    .permitAll();
    }

//    @Bean
//    @Override
//    public UserDetailsService userDetailsService() {
//        UserDetails user =
//                User.withDefaultPasswordEncoder()
//                        .username("user")
//                        .password("password")
//                        .roles("USER")
//                        .build();
//
//        return new InMemoryUserDetailsManager(user);
//    }
}

프로젝트 실행

프로젝트를 실행하면, 데이타베이스에 저장한 아이디/비밀번호를 이용해 로그인할 수 있습니다.

비밀번호 암호화

현재는 비밀번호가 데이타베이스 상에 평문으로 저장됩니다.

보안상 취약하므로 암호화된 비밀번호가 데이타베이스에 저장되도록 방식을 변경합니다.

암호화 비밀번호 생성

public class MakeBCryptPassword {

    public static void main(String[] args) {

        String password = "222";

        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String hashedPassword = passwordEncoder.encode(password);
        System.out.println(hashedPassword);
        System.out.println(passwordEncoder.matches("222", hashedPassword));
    }
}

위 파일을 실행하고, 로그에 표시되는 암호화 비밀번호를 복사해 데이타베이스에 저장합니다.

UPDATE tbl_user
SET PASSWORD = '$2a$10$7yXyY8.uGeP6PWs0P.yw9Ojvf94IfG9eyx75AKvTK9IDo0f3pZle6'
WHERE id = 1;

MyUserDetailsService 수정

MyUserDetailsService.java

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Override
    public UserDetails loadUserByUsername(String username) {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException(username);
        }
        return new MyUserDetails(user);
    }

    public PasswordEncoder passwordEncoder() {
        return this.passwordEncoder;
    }
}

WebSecurityConfig.java

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MyUserDetailsService userService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    .antMatchers("/", "/home").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .and()
                .logout()
                    .permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService)
                .passwordEncoder(userService.passwordEncoder())
        ;
    }
}

프로젝트 재실행

프로젝트를 재실행하면 암호화된 비밀번호화 입력된 비밀번호를 비교하게 됩니다.

One thought on “Spring Security with JDBC(UserDetailsService)

  1. Pingback: Spring Boot 시작하기 – 상구리의 기술 블로그

답글 남기기