springboot:spring_simple_username-password_authentication_with_h2

Spring Simple Username-Password Authentication with H2

Basically, we have 6 steps:

  1. Setup the project dependencies
  2. Create User Role entities. User entity implement UserDetails
  3. Create Your own UserDetailsService
  4. Create WebSecurityConfig extends WebSecurityConfigurerAdapter
  5. Create BCryptPasswordEncoder bean
  6. Create some fake account in main class, and a controller for testing

Add those in your build.gradle. We need JPA, security, thymeleaf templates engine, web MVC, and h2 database.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'com.h2database:h2'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.springframework.security:spring-security-test'
}

Create a User entity that implements UserDetails. To make it simple, we return true for all UserDetails methods, except getAuthorities(). We also tells JPA to create a table that link user and role. We need to (fetch = FetchType.EAGER) for user and role relationship.

@Entity
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name = "users_roles",
            joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    private Collection<Role> roles;

    public User() {}
    public User(String username, String password, Collection<Role> roles) {
        this.username = username;
        this.password = password;
        this.setRoles(roles);
    }

    public Long getId() {return id;}
    public void setId(Long id) {this.id = id;}
    public String getUsername() {return username;}
    public void setUsername(String username) {this.username = username;}
    public String getPassword() {return password;}
    public void setPassword(String password) {this.password = password;}
    public Collection<Role> getRoles() {return roles;}
    public void setRoles(Collection<Role> roles) {this.roles = roles;}

    @Override public boolean isAccountNonExpired() {return true;}
    @Override public boolean isAccountNonLocked() {return true;}
    @Override public boolean isCredentialsNonExpired() {return true;}
    @Override public boolean isEnabled() {return true;}
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> grantedAuthorities = new HashSet<>();
        for (Role role : getRoles()) {
            grantedAuthorities.add(new SimpleGrantedAuthority(role.getRole()));
        }
        return grantedAuthorities;
    }
}

Create the role entity.

@Entity
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false, unique = true)
    private String role;

    @ManyToMany(mappedBy = "roles")
    private List<User> users;
    public Long getId() {return id;}
    public void setId(Long id) {this.id = id;}
    public String getRole() {return role;}
    public void setRole(String role) {this.role = role;}
    public List<User> getUsers() {return users;}
    public void setUsers(List<User> users) {this.users = users;}
}

Also need to create the crud repository for accessing the data in DB.

public interface UserRepository extends CrudRepository<User, Long> {
    User findUserById(Long id);
    User findUserByUsername(String username);
}

public interface RoleRepository extends CrudRepository<Role, Long> {
    Role findRoleByRole(String role);
}

We will use this for getting user in auth config.

@Service
public class H2UserDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findUserByUsername(username);
        if (user != null) {
            return user;
        }
        throw new UsernameNotFoundException("User not found with username: " + username);
    }
}

For debugging, we let the path /h2-console to be accessed without authentication. To make H2 console works, we do .frameOptions().disable() here.

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @Autowired
    private UserDetailsService h2UserDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().headers()
                .frameOptions().disable()
                .and()
                .authorizeRequests()
                .antMatchers("h2-console/**").permitAll()
                .anyRequest().hasRole("ADMIN")
                .and()
                .formLogin().permitAll()
                .and()
                .logout().permitAll();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(h2UserDetailsService).passwordEncoder(bCryptPasswordEncoder);
    }
}

Simply add the following code inside the main application class.

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

Creating fake account on run. One way to do it is to have the main class to implements CommandLineRunner.

   @Override
    public void run(String... args) throws Exception {
        Role roleUser = new Role();
        roleUser.setRole("ROLE_USER");
        Role roleAdmin = new Role();
        roleAdmin.setRole("ROLE_ADMIN");
        roleRepository.save(roleUser);
        roleRepository.save(roleAdmin);
        Set<Role> allRoles = new HashSet<>();
        allRoles.add(roleAdmin);
        allRoles.add(roleUser);
        Set<Role> userRoles = new HashSet<>();
        userRoles.add(roleUser);
        Set<Role> adminRole = new HashSet<>();
        adminRole.add(roleAdmin);
        userRepository.save(new User("adminuser", bCryptPasswordEncoder.encode("password"), allRoles));
        userRepository.save(new User("user", bCryptPasswordEncoder.encode("password"), userRoles));
        userRepository.save(new User("admin", bCryptPasswordEncoder.encode("password"), adminRole));
    }

A testing controller can be as simple as

@Controller
public class HomeController {
    @GetMapping("/")
    public String home() {
        return "home/index";
    }
}

With an html file in resources/templates/

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Home</title>
</head>
<body>
<div>
    Hello!
</div>
</body>
</html>

Go to http://127.0.0.1:8080/, and it will redirect you to the /login site. Type admin:password to login. The system will return you back to the root page, and you will see Hello!. If you use the user:password, you will see a 403 error because we set .anyRequest().hasRole(“ADMIN”) in the WebSecurityConfig.

  • springboot/spring_simple_username-password_authentication_with_h2.txt
  • Last modified: 2020/06/09 16:30
  • by chongtin