Spring Security with Spring Boot and MySQL
In this tutorial I'll show how to use Spring Security with Spring Boot and MySQL. We'll implement a UserDetailsService provided by Spring Security. It is easy to configure Spring Security with Spring Boot and MySQL. You can also check how to run Spring Boot application inside docker container
1. Create a Spring Boot project from start.spring.io with following dependencies in your pom.xml file. You can choose gradle also for project dependencies.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- dev tools for hot reloading -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<!-- database and connectivity -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- spring boot security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2. Edit the application.properties file to connect to the MySQL
database. You must first create a database to connect successfully.
Tables will be created automatically based upon the model class.#######################
# Database
#######################
spring.datasource.url=jdbc:mysql://localhost:3306/spring_security
spring.datasource.username=root
spring.datasource.password=kasturi66
spring.datasource.hikari.driver-class-name=com.mysql.jdbc.Driver
#60 sec
spring.datasource.hikari.connection-timeout=60000
spring.datasource.hikari.maximum-pool-size=5
# Keep the connection alive if idle for a long time (needed in production)
spring.datasource.testWhileIdle = true
spring.datasource.validationQuery = SELECT 1
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048
# Show or not log for each sql query
spring.jpa.show-sql = true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto = update
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
# for hibernate session factory
spring.jpa.properties.hibernate.current_session_context_class=org.springframework.orm.hibernate5.SpringSessionContext
3. Create a class that stores the username, password of the user. Let
us call it AppUser. We can also name it User class but later we need to
user the User class from Spring Security. So, to remove the confusion,
make it simpler. The class looks like thispackage pro.budthapa.domain;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
@Entity(name="User")
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String username;
private String password;
private boolean active;
@ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.ALL)
@JoinTable(joinColumns = @JoinColumn(name = "user_id"),inverseJoinColumns = @JoinColumn(name = "role_id"))
private List<Role> 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 List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
4. Create a Role class that will define the roles for our users. You can put as many types of role as you wantpackage pro.budthapa.domain;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
@Entity
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String role;
public Role() {}
public Role(String role) {
this.role = role;
}
@ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
private List<AppUser> 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<AppUser> getUsers() {
return users;
}
public void setUsers(List<AppUser> users) {
this.users = users;
}
}
5. Now, create the UserRepository interface that extends another
interface called JpaRepository<T,T>. It will take care of our CRUD
operations.package pro.budthapa.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import pro.budthapa.domain.AppUser;
@Repository
public interface UserRepository extends JpaRepository<AppUser, Integer> {
public Optional<AppUser> findByUsername(String name);
}
6. Create a service interface called UserService and define the operations we need to used for interacting with the database.package pro.budthapa.service;
import pro.budthapa.domain.AppUser;
public interface UserService {
public AppUser saveUser(AppUser user);
}
7. Create a class that implements the UserService interface we defined above.package pro.budthapa.service.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import pro.budthapa.domain.AppUser;
import pro.budthapa.repository.UserRepository;
import pro.budthapa.service.UserService;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public AppUser saveUser(AppUser user) {
return userRepository.save(user);
}
}
8. Create a custom user service detail class called
UserDetailsServiceImpl that implements the UserDetailsService from
Spring Security. This class will check if our user is present in the
database and handle the login processpackage pro.budthapa.service.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import pro.budthapa.domain.Role;
import pro.budthapa.domain.AppUser;
import pro.budthapa.repository.UserRepository;
public class UserDetailsServiceImpl implements UserDetailsService{
private UserRepository userRepository;
public UserDetailsServiceImpl(pro.budthapa.repository.UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<AppUser> user = userRepository.findByUsername(username);
if(user.isPresent()) {
return new User(user.get().getUsername(),
user.get().getPassword(), getAuthorities(user.get()));
}else {
throw new UsernameNotFoundException("Invalid user tried to login. User not found exception");
}
}
private List<GrantedAuthority> getAuthorities(AppUser user) {
List<GrantedAuthority> authorities = new ArrayList<>();
for(Role role: user.getRoles()) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRole());
authorities.add(grantedAuthority);
}
return authorities;
}
}
9. Create a security config class that extends
WebSecurityConfigurerAdapter from Spring Security. This class will
handle all the security related configuration. You can define which
resource you want to block for certain users and grant for other users.package pro.budthapa.config;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import pro.budthapa.repository.UserRepository;
import pro.budthapa.security.AuthenticationFailureHandler;
import pro.budthapa.service.impl.UserDetailsServiceImpl;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserRepository userRepository;
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new AuthenticationFailureHandler();
}
/*
* Tell Spring Security to use the custom built UserDetailsServiceImpl class
*
*/
@Override
protected void configure(AuthenticationManagerBuilder authBuilder) throws Exception {
authBuilder.userDetailsService(userDetailsServiceBean()).passwordEncoder(passwordEncoder());
}
@Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return new UserDetailsServiceImpl(userRepository);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//you can either disable this or
//put <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
//inside the login form
.csrf().disable()
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/logout").permitAll()
.antMatchers("/admin/**").hasAuthority("ADMIN")
.antMatchers("/user/**").hasAuthority("USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") //enable this to go to your own custom login page
//.loginProcessingUrl("/login") //enable this to use login page provided by spring security
.failureUrl("/login?error")
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout");
;
}
/*
*
* These resources are available to every users
*/
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/js**")
.antMatchers("/images/**")
.antMatchers("/css/**")
.antMatchers("/templates/**");
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
10. Create a controller class called LoginController, you can name it
anything as you prefer. We'll define routes to our resources in this
classpackage pro.budthapa.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import pro.budthapa.domain.AppUser;
@Controller
public class LoginController {
private Logger log = LoggerFactory.getLogger(LoginController.class);
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/login")
public String login(Model model){
model.addAttribute("user", new AppUser());
return "login";
}
//access only to admin
@PreAuthorize("hasAuthority('ADMIN')")
@GetMapping("/admin/home")
public String adminLandingPage() {
log.info("Accessing admin page");
return "admin"; //this name should match to admin.html inside templates folder
}
//access only for user
@PreAuthorize("hasAuthority('USER')")
@GetMapping("/user/home")
public String userLandingPage() {
log.info("Accessing user page");
return "user"; //this name should match to user.html inside templates folder
}
}
11. Create a template for your login form. You can use BootStrap to make it look good<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Login Page</title>
<link rel="stylesheet"
th:href="@{https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css}" />
</head>
<body>
<div class="container">
<div class="col-sm-4 col-sm-offset-4" style="margin-top: 25px;">
<div class="panel panel-info">
<div class="panel-heading">
<div class="panel-title">
<div>
<span>Sign In</span>
</div>
</div>
</div>
<div class="panel-body" id="login-panel-body">
<div th:if="${param.error}" class="alert alert-danger col-sm-12">
<span>Invalid Email or Password</span>
</div>
<div th:if="${param.logout}" class="alert alert-success col-sm-12">
<span>You've successfully logged out</span>
</div>
<form method="POST" action="/login">
<div class="form-group">
<label for="username">Username:</label>
<input class="form-control" id="username" name="username" placeholder="username" type="text"/>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input class="form-control" id="password" name="password" placeholder="password" type="password"/>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success form-control">Login</button>
</div>
</form>
</div>
<div class="panel-footer">
<a th:href="@{/}">Home Page</a>
</div>
</div>
</div>
</div>
<script th:src="@{https://code.jquery.com/jquery-3.2.1.min.js}"></script>
<script th:src="@{https://code.jquery.com/ui/1.12.1/jquery-ui.min.js}"></script>
<script
th:src="@{https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js}"></script>
</body>
</html>
12. Create admin page, this page is restricted to admin only<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>Admin</title>
</head>
<body>
This is admin page
</body>
</html>
13. Create user page, this page is restricted to user only<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8"/>
<title>User</title>
</head>
<body>
This is user page
</body>
</html>
14. Create your home page for easy navigation to admin page and user page.<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>Spring Boot Security Demo</title>
<link rel="stylesheet"
th:href="@{https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css}" />
</head>
<body>
<div class="container">
<div class="col-sm-6 col-sm-offset-3" style="margin-top: 25px;">
<div class="panel panel-info" style="text-align: center;">
<div class="panel-heading">
<div class="panel-title">
<div>
<span>Home Page</span>
</div>
</div>
</div>
<div class="panel-body" id="login-panel-body">
<div class="col-sm-6">
<a th:href="@{/admin/home}">Admin Landing Page</a>
</div>
<div class="col-sm-6">
<a th:href="@{/user/home}">User Landing Page</a>
</div>
</div>
<div class="panel-footer">
<a th:href="@{/login}">Login Page</a>
</div>
</div>
</div>
</div>
<script th:src="@{https://code.jquery.com/jquery-3.2.1.min.js}"></script>
<script th:src="@{https://code.jquery.com/ui/1.12.1/jquery-ui.min.js}"></script>
<script
th:src="@{https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js}"></script>
</body>
</html>
15. Now, inside the main startup class, insert some dummy data for our users. You can use any other methods to insert data.package pro.budthapa;
import java.util.Arrays;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import pro.budthapa.domain.AppUser;
import pro.budthapa.domain.Role;
import pro.budthapa.service.UserService;
@SpringBootApplication
public class SpringBootSecurityApplication implements CommandLineRunner{
public static void main(String[] args) {
SpringApplication.run(SpringBootSecurityApplication.class, args);
}
@Autowired
private BCryptPasswordEncoder encoder;
@Autowired
private UserService userService;
/*
* This method will run during application startup and execute all the codes inside this method
*
*/
@Override
public void run(String... arg0) throws Exception {
//Remove or comment this part after first execution of application,
//or else duplicate data will be inserted in the database
AppUser admin = new AppUser();
admin.setActive(true);
admin.setPassword(encoder.encode("password"));
admin.setUsername("admin");
admin.setRoles(Arrays.asList(new Role("ADMIN")));
userService.saveUser(admin);
AppUser user = new AppUser();
user.setActive(true);
user.setPassword(encoder.encode("password"));
user.setUsername("user");
user.setRoles(Arrays.asList(new Role("USER")));
userService.saveUser(user);
}
}
16. Finally run you application. Dummy user and their related roles and password are inserted in the database. Hibernate has created tables based upon the entities define in pro.budthapa.model package.
Now, go to localhost:8080 and will see the following screen.
Now, go to localhost:8080/login and will see the login screen.
Put admin in username field and password in the password field. You will be authenticated. Putting wrong credentials will show the same login page again. You can define your own validations and messages when user enters the wrong username and password combinations.
Similarly, you can do the same thing for user also.
After logging with admin credentials, try to go to localhost:8080/user/home you will see access denied page.
After logging with user credentials, try to go to localhost:8080/admin/home you will see access denied page.
If you don't want to see the Whitelabel Error Page, you can define your own access denied page
You can also run Spring Boot application inside docker container using this link.
If you want to control the event on authentication success and failure you can create the handler page aswell. For eg. If you want a user to try invalid login attempt only for 3 times and block after 3 attempts you can create another class to handle such event.
Download full project from github
Comments
Post a Comment