Hello Steemer!!!
Let's see about the spring boot security
1. Spring security Overview
Spring security is the highly customizable authentication and access-control framework. This is the security module for securing spring applications. But, this can also be used for non-spring based application with few extra configurations to enable the security features. Spring Security is a framework that focuses on providing both authentication and authorization to Java applications.
2. Spring Security ModulesIn Spring Security 3.0, the codebase has been sub-divided into separate jars which more clearly separate different functionality areas and third-party dependencies. The following are the list of modules currently shipped by spring security framework.
3. Getting Spring SecurityYou can get hold of Spring Security in several ways.3.1. Usage with MavenA minimal Spring Security Maven set of dependencies typically looks like the following:Pom.xml
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
</dependencies>
3.2. Usage with GradleA minimal Spring Security Gradle set of dependencies typically looks like the following:build.gradle
dependencies {
compile 'org.springframework.security:spring-security-web:4.1.2.RELEASE'
compile 'org.springframework.security:spring-security-config:4.1.2.RELEASE'
}
4. Core ComponentsCore Components represent the building blocks of spring security and what are the core components that are actually used while user is authenticating to your application. So if you ever need to go beyond a simple namespace configuration then it’s important that you understand what they are, even if you don’t actually need to interact with them directly.4.1 SecurityContextAs the name implies, this interface is the corner stone of storing all the security related details for your application. When you enable spring security for your application, a SecurityContext will enable for each application and stores the details of authenticated user, etc. It uses Authentication object for storing the details related to authentications.4.2 SecurityContextHolderThe most fundamental object is SecurityContextHolder. This class is important for accessing any value from the SecurityContext. This is where we store details of the present security context of the application, which includes details of the principal currently using the application. By default the SecurityContextHolder uses a ThreadLocal to store these details, which means that the security context is always available to methods in the same thread of execution, even if the security context is not explicitly passed around as an argument to those methods.
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}
4.3 AuthenticationLet’s consider a standard authentication scenario that everyone is familiar with.
The following are the steps to achieve the authentication:
4.4 UserDetailsServiceUserDetails is a core interface in Spring Security. It represents a principal, but in an extensible and application-specific way. Think of UserDetails as the adapter between your own user database and what Spring Security needs inside the SecurityContextHolder. UserDetailsService is a core interface in spring security to load user specific data. This interface is considered as user DAO and will be implemented by specific DAO implementations. For example, for a basic in memory authentication, there is an InMemoryUserDetailsManager. This interface declares only one method loadUserByUsername (String username) which simplifies the implementation classes to write other specific methods.Suppose you want to use your existing DAO classes to load the user details from the database, just implement the UserDetailsService and override the method loadUserByUsername(String username). An example of this implementation would look like this:
@Service
public class CurrentUserDetailsService implements UserDetailsService {
private final UserService userService;
@Autowired
public CurrentUserDetailsService(UserService userService) {
this.userService = userService;
}
public CurrentUser loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.getUserByUsername(username);
return new CurrentUser(user);
}
}
In the above code, the model CurrentUser must of of type org.springframework.security.userdetails.UserDetails. In the below example application, I have extended the org.springframework.security.userdetails.UserDetails class and written the custom user class. This will have the advantage of not exposing our domain class.4.5 GrantedAuthorityApart from authenticating to the application, another important component is to get the list of granted authorities for the logged in user. This comes as part of the authorization process. This is retrieved by calling the getAuthorities() in Authentication object. This returns the list of GrantedAuthority which denotes roles for the users. Such authorities are usually “roles”, such as ROLE_ADMINISTRATOR orROLE_HR_SUPERVISOR. These roles are later on configured for web authorization, method authorization and domain object authorization.SummaryThe major building blocks of Spring Security that we’ve seen so far are:
5. Spring Security Example ApplicationThis section walks you through the process of creating a simple web application in Spring Boot with resources that are protected by Spring Security.What you’ll buildYou’ll build a Spring MVC application that secures the page with a login form backed by a fixed list of users. Let’s start with the use cases for every common web application with some basic access restrictions. So, the requirements of such an app could be:
Let’s create Web Project with Spring Boot Web Initialzr with Spring Web, Spring Data JPA, Spring Security and H2 Database. So following project structure we have after creating from web interface and adding necessary files for this example.Let’s discuss files of this example.Dependencies pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dineshonjava.sbsecurity</groupId>
<artifactId>SpringBootSecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>SpringBootSecurity</name>
<description>SpringBootSecurity project for Spring Boot and Spring Security</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
WebSecurityConfigurerAdapter
This is the Java configuration class for writing the web based security configurations. You can override the methods in this class to configure the following things:
SecurityConfig.java
/**
*
*/
package com.dineshonjava.sbsecurity.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* @author Dinesh.Rajput
*
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home", "/public/**").permitAll()
.antMatchers("/users/**").hasAuthority("ADMIN")
.anyRequest().fullyAuthenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.usernameParameter("email")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.permitAll();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
}
Domain Classes
User.java
/**
*
*/
package com.dineshonjava.sbsecurity.model;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import com.dineshonjava.sbsecurity.role.Role;
/**
* @author Dinesh.Rajput
*
*/
@Entity
@Table(name = "user")
public class User implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "userid", nullable = false, updatable = false)
Long userid;
@Column(name = "username", nullable = false)
String username;
@Column(name = "email", nullable = false, unique = true)
String email;
@Column(name = "password", nullable = false)
String password;
@Column(name = "role", nullable = false)
@Enumerated(EnumType.STRING)
Role role;
public Long getUserid() {
return userid;
}
public void setUserid(Long userid) {
this.userid = userid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
@Override
public String toString() {
return "User [userid=" + userid + ", username=" + username + ", email="
+ email + ", password=" + password + ", role=" + role + "]";
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((email == null) ? 0 : email.hashCode());
result = prime * result
+ ((password == null) ? 0 : password.hashCode());
result = prime * result + ((role == null) ? 0 : role.hashCode());
result = prime * result + ((userid == null) ? 0 : userid.hashCode());
result = prime * result
+ ((username == null) ? 0 : username.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (email == null) {
if (other.email != null)
return false;
} else if (!email.equals(other.email))
return false;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (role != other.role)
return false;
if (userid == null) {
if (other.userid != null)
return false;
} else if (!userid.equals(other.userid))
return false;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
return true;
}
}
Role.java
/**
*
*/
package com.dineshonjava.sbsecurity.role;
/**
* @author Dinesh.Rajput
*
*/
public enum Role {
USER, ADMIN
}
Besides that, a form for creating a new user will be nice to have:
UserBean.java
/**
*
*/
package com.dineshonjava.sbsecurity.bean;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotEmpty;
import com.dineshonjava.sbsecurity.role.Role;
/**
* @author Dinesh.Rajput
*
*/
public class UserBean {
@NotEmpty
private String username = "";
@NotEmpty
private String email = "";
@NotEmpty
private String password = "";
@NotEmpty
private String passwordRepeated = "";
@NotNull
private Role role = Role.USER;
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPasswordRepeated() {
return passwordRepeated;
}
public void setPasswordRepeated(String passwordRepeated) {
this.passwordRepeated = passwordRepeated;
}
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
This will function as a data transfer object (DTO) between the web layer and service layer. It’s annotated by Hibernate Validator validation constraints and sets some sane defaults.CurrentUser.java
/**
*
*/
package com.dineshonjava.sbsecurity.bean;
import org.springframework.security.core.authority.AuthorityUtils;
import com.dineshonjava.sbsecurity.model.User;
import com.dineshonjava.sbsecurity.role.Role;
/**
* @author Dinesh.Rajput
*
*/
public class CurrentUser extends org.springframework.security.core.userdetails.User {
/**
*
*/
private static final long serialVersionUID = 1L;
private User user;
public CurrentUser(User user) {
super(user.getEmail(), user.getPassword(), AuthorityUtils.createAuthorityList(user.getRole().toString()));
this.user = user;
}
public User getUser() {
return user;
}
public Long getId() {
return user.getUserid();
}
public Role getRole() {
return user.getRole();
}
@Override
public String toString() {
return "CurrentUser{" +
"user=" + user +
"} " + super.toString();
}
}
Service Layer
In service layer, where the business logic should, we’d need something to retrieve the User by his id, email, list all the users and create a new one.
UserService .java
/**
*
*/
package com.dineshonjava.sbsecurity.service;
import java.util.Collection;
import com.dineshonjava.sbsecurity.bean.UserBean;
import com.dineshonjava.sbsecurity.model.User;
/**
* @author Dinesh.Rajput
*
*/
public interface UserService {
User getUserById(long id);
User getUserByEmail(String email);
Collection<User> getAllUsers();
User create(UserBean userBean);
}
UserServiceImpl .java
The implementation of the service:
/**
*
*/
package com.dineshonjava.sbsecurity.service;
import java.util.Collection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import com.dineshonjava.sbsecurity.bean.UserBean;
import com.dineshonjava.sbsecurity.model.User;
import com.dineshonjava.sbsecurity.model.UserRepository;
/**
* @author Dinesh.Rajput
*
*/
@Service
public class UserServiceImpl implements UserService {
private static final Logger LOGGER = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
UserRepository userRepository;
@Override
public User getUserById(long id) {
LOGGER.debug("Getting user={}", id);
return userRepository.findOne(id);
}
@Override
public User getUserByEmail(String email) {
LOGGER.debug("Getting user by email={}", email.replaceFirst("@.*", "@***"));
return userRepository.findOneByEmail(email);
}
@Override
public Collection<User> getAllUsers() {
LOGGER.debug("Getting all users");
return (Collection<User>) userRepository.findAll();
}
@Override
public User create(UserBean userBean) {
User user = new User();
user.setUsername(userBean.getUsername());
user.setEmail(userBean.getEmail());
user.setPassword(new BCryptPasswordEncoder().encode(userBean.getPassword()));
user.setRole(userBean.getRole());
return userRepository.save(user);
}
}
Spring Data Repository
This section explains the service classes and spring data repository implementation.
UserRepository .java
/**
*
*/
package com.dineshonjava.sbsecurity.model;
import org.springframework.data.repository.CrudRepository;
/**
* @author Dinesh.Rajput
*
*/
public interface UserRepository extends CrudRepository<User, Long>{
User findOneByEmail(String email);
}
CurrentUserDetailsService.java
/**
*
*/
package com.dineshonjava.sbsecurity.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.dineshonjava.sbsecurity.bean.CurrentUser;
import com.dineshonjava.sbsecurity.model.User;
/**
* @author Dinesh.Rajput
*
*/
@Service
public class CurrentUserDetailsService implements UserDetailsService {
private static final Logger LOGGER = LoggerFactory.getLogger(CurrentUserDetailsService.class);
@Autowired
UserService userService;
@Override
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
LOGGER.debug("Authenticating user with email={}", email.replaceFirst("@.*", "@***"));
User user = userService.getUserByEmail(email);
return new CurrentUser(user);
}
}
Spring MVC Configurations Web Layer
Home Page
/**
*
*/
package com.dineshonjava.sbsecurity.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author Dinesh.Rajput
*
*/
@Controller
public class HomeController {
private static final Logger LOGGER = LoggerFactory.getLogger(HomeController.class);
@RequestMapping("/")
public String getHomePage() {
LOGGER.debug("Getting home page");
return "home";
}
}
Login Page
/**
*
*/
package com.dineshonjava.sbsecurity.controller;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
/**
* @author Dinesh.Rajput
*
*/
@Controller
public class LoginController {
private static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class);
@RequestMapping(value = "/login", method = RequestMethod.GET)
public ModelAndView getLoginPage(@RequestParam Optional<String> error) {
LOGGER.debug("Getting login page, error={}", error);
return new ModelAndView("login", "error", error);
}
}
User Page
/**
*
*/
package com.dineshonjava.sbsecurity.controller;
import javax.validation.Valid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import com.dineshonjava.sbsecurity.bean.UserBean;
import com.dineshonjava.sbsecurity.bean.validator.UserBeanValidator;
import com.dineshonjava.sbsecurity.service.UserService;
/**
* @author Dinesh.Rajput
*
*/
@Controller
public class UserController {
private static final Logger LOGGER = LoggerFactory.getLogger(UserController.class);
@Autowired
UserService userService;
@Autowired
UserBeanValidator userBeanValidator;
@InitBinder("form")
public void initBinder(WebDataBinder binder) {
binder.addValidators(userBeanValidator);
}
@RequestMapping("/users")
public ModelAndView getUsersPage() {
LOGGER.debug("Getting users page");
return new ModelAndView("users", "users", userService.getAllUsers());
}
@PreAuthorize("@currentUserServiceImpl.canAccessUser(principal, #id)")
@RequestMapping("/user/{id}")
public ModelAndView getUserPage(@PathVariable Long id) {
LOGGER.debug("Getting user page for user={}", id);
return new ModelAndView("user", "user", userService.getUserById(id));
}
@PreAuthorize("hasAuthority('ADMIN')")
@RequestMapping(value = "/user/create", method = RequestMethod.GET)
public ModelAndView getUserCreatePage() {
LOGGER.debug("Getting user create form");
return new ModelAndView("user_create", "form", new UserBean());
}
@PreAuthorize("hasAuthority('ADMIN')")
@RequestMapping(value = "/user/create", method = RequestMethod.POST)
public String handleUserCreateForm(@Valid @ModelAttribute("form") UserBean form, BindingResult bindingResult) {
LOGGER.debug("Processing user create form={}, bindingResult={}", form, bindingResult);
if (bindingResult.hasErrors()) {
return "user_create";
}
try {
userService.create(form);
} catch (DataIntegrityViolationException e) {
LOGGER.warn("Exception occurred when trying to save the user, assuming duplicate email", e);
bindingResult.reject("email.exists", "Email already exists");
return "user_create";
}
return "redirect:/users";
}
}
CurrentUserServiceImpl.java
/**
*
*/
package com.dineshonjava.sbsecurity.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import com.dineshonjava.sbsecurity.bean.CurrentUser;
import com.dineshonjava.sbsecurity.role.Role;
/**
* @author Dinesh.Rajput
*
*/
@Service
public class CurrentUserServiceImpl implements CurrentUserService {
private static final Logger LOGGER = LoggerFactory.getLogger(CurrentUserDetailsService.class);
@Override
public boolean canAccessUser(CurrentUser currentUser, Long userId) {
LOGGER.debug("Checking if user={} has access to user={}", currentUser, userId);
return currentUser != null
&& (currentUser.getRole() == Role.ADMIN || currentUser.getId().equals(userId));
}
}
application.properties
#security.user.name=root
#security.user.password=111
#security.user.role=ADMIN
logging.level.org.springframework=WARN
logging.level.org.hibernate=WARN
logging.level.com.dineshonjava=DEBUG
spring.freemarker.template-loader-path=/WEB-INF/ftl
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-spring-macro-helpers=true
Here we are using freemarker template for views layers.You can access this whole running project from below link.
https://github.com/DOJ-SoftwareConsultant/SpringBootSecurity
Let's see the spring boot security with jwt in the next post.