Basically, we have 6 steps:
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.