1. Spring security Overview
- “Authentication” is the process of establishing a principal is who they claim to be (a “principal” generally means a user, device or some other system which can perform an action in your application).
- “Authorization” refers to the process of deciding whether a principal is allowed to perform an action within your application. To arrive at the point where an authorization decision is needed, the identity of the principal has already been established by the authentication process. These concepts are common, and not at all specific to Spring Security.
2. Spring Security Modules
- Core (spring-security-core.jar) – This module contains the APIs for basic authentication and access-control related mechanism. This is mandatory for ant spring security applications.
- Remoting (spring-security-remoting.jar) – This module provides integration to the Spring Remoting. You don’t need to include this module unless you are writing remote client applications.
- Web (spring-security-web.jar) – This module contains APIs for servlet filters and any web based authentication like access restriction for URLs. Any web application would require this module.
- Config (spring-security-config.jar) – Contains the security namespace parsing code & Java configuration code. You need it if you are using the Spring Security XML namespace for configuration. If you are not using XML configurations, you can ignore this module.
- LDAP (spring-security-ldap.jar)– Required if you need to use LDAP authentication or manage LDAP user entries.
- ACL (spring-security-acl.jar) – Specialized domain object ACL implementation. Used to apply security to specific domain object instances within your application.
- CAS (spring-security-cas.jar) – Spring Security’s CAS client integration. If you want to use Spring Security web authentication with a CAS single sign-on server.
- OpenID (pring-security-openid.jar) – OpenID web authentication support. Used to authenticate users against an external OpenID server.
- Test (spring-security-test.jar)– Support for testing with Spring Security.
3. Getting Spring Security
<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>
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 Components
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
- A user is prompted to log in with a username and password.
- The system verifies that the password is correct for the username.
- The context information for that user is obtained their list of roles and so on.
- A security context is established for the user
- The user proceeds, potentially to perform some operation which is potentially protected by an access control mechanism which checks the required permissions for the operation against the current security context information.
- Authentication is an interface which has several implementations for different authentication models. For a simple user name and password authentication, spring security would use UsernamePasswordAuthenticationToken. When user enters username and password, system creates a new instance of UsernamePasswordAuthenticationToken.
- The token is passed to an instance of AuthenticationManager for validation. Internally what AuthenticationManager will do is to iterate the list of configured AuthenticationProvider to validate the request. There should be at least one provider to be configured for the valid authentication.
- The AuthenticationManager returns a fully populated Authentication instance on successful authentication.
- The final step is to establish a security context by invoking SecurityContextHolder.getContext().setAuthentication(), passing in the returned authentication object.
@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); } }
- SecurityContextHolder, to provide access to the SecurityContext.
- SecurityContext, to hold the Authentication and possibly request-specific security information.
- Authentication, to represent the principal in a Spring Security-specific manner.
- GrantedAuthority, to reflect the application-wide permissions granted to a principal.
- UserDetails, to provide the necessary information to build an Authentication object from your application’s DAOs or other source of security data.
- UserDetailsService, to create a UserDetails when passed in a String-based username (or certificate ID or the like).
- The app will have users, each with role Admin or User
- They log in by their emails and passwords
- Non-admin users can view their info, but cannot peek at other users
- Admin users can list and view all the users, and create new ones as well
- Customized form for login
- “Remember me” authentication for laziest
- Possibility to logout
- Home page will be available for everyone, authenticated or not
<?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:
- Enforce the user to be authenticated prior to accessing any URL in your application
- Create a user with the username user , password, and role of ROLE_USER
- Enables HTTP Basic and Form based authentication
- Spring Security will automatically render a login page and logout success page for you
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
Happy Spring Boot Learning!!!:)
Spring Boot Related Topics
- Introduction to Spring Boot
- Essentials and Key Components of Spring Boot
- Spring Boot CLI Installation and Hello World Example
- Spring Boot Initializr Web Interface
- Spring Boot Initializr With IDEs
- Spring Boot Initializr With Spring Boot CLI
- Installing Spring Boot
- Developing your first Spring Boot application
- External Configurations for Spring Boot Applications
- Logging Configuration in Spring Boot
- Spring Boot and Spring MVC
- Working with SQL Databases and Spring Boot
- MySQL Configurations
- Spring Data JPA using Spring Boot Application
- Spring Boot with NoSQL technologies
- Spring Cache Tutorial
- Spring Security Tutorial with Spring Boot
- Spring Boot and MongoDB in REST Application
- Complete Guide for Spring Boot Actuator
- Microservices with Spring Boot
Can you please provide a Example like, Rest and Basic Authentication component both are individual instances ?