====== Spring Simple Username-Password Authentication with H2 ======
Basically, we have 6 steps:
- Setup the project dependencies
- Create User Role entities. User entity implement UserDetails
- Create Your own UserDetailsService
- Create WebSecurityConfig extends WebSecurityConfigurerAdapter
- Create BCryptPasswordEncoder bean
- Create some fake account in main class, and a controller for testing
===== Setup the project dependencies =====
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 User Role entities. User entity implement UserDetails =====
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 roles;
public User() {}
public User(String username, String password, Collection 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 getRoles() {return roles;}
public void setRoles(Collection 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 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 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 getUsers() {return users;}
public void setUsers(List users) {this.users = users;}
}
Also need to create the crud repository for accessing the data in DB.
public interface UserRepository extends CrudRepository {
User findUserById(Long id);
User findUserByUsername(String username);
}
public interface RoleRepository extends CrudRepository {
Role findRoleByRole(String role);
}
===== Create Your own UserDetailsService =====
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);
}
}
===== Create WebSecurityConfig extends WebSecurityConfigurerAdapter =====
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);
}
}
===== Create BCryptPasswordEncoder Bean =====
Simply add the following code inside the main application class.
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
===== Create some fake account in main class, and a controller for testing =====
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 allRoles = new HashSet<>();
allRoles.add(roleAdmin);
allRoles.add(roleUser);
Set userRoles = new HashSet<>();
userRoles.add(roleUser);
Set 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/
Home
Hello!
===== To Test =====
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.