Spring Authentication – UserDetailsService
In my previous tutorials, I have shown few authentications mechanisms, such as, Spring Security Form based Authentication – XML Configuration , Spring Security Form based Authentication – Annotations , Spring Security – JDBC Authentication but in this tutorial I will show you how to authenticate users using Spring JDBC UserDetailsService and Spring MVC web application to secure pages. I will create spring mvc based web application and I will configure Spring Security to protect a page from outside access.
UserDetailsService is used by DaoAuthenticationProvider for retrieving a username, password, and other attributes for authenticating with a username and password. Spring Security provides in-memory and JDBC implementations of UserDetailsService.
Spring Security allows to you to integrate security features with JEE web application easily, it takes care about all incoming HTTP requests via servlet filter, and implements “user defined” security checking.
Prerequisites
Java 1.8+, Tomcat 9, Servlet 4.0.1, Spring 5.7.4, Maven 3.8.5
Project Setup
Create a maven based web project in your favorite IDE or tool. You can also use the following pom.xml file for your project. The minimal dependency artifacts required for Spring Security are spring-security-web
and spring-security-config
.
<?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.roytuts</groupId>
<artifactId>spring-security-jdbc-authentication-userdetailsservice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.23</version>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.7.4</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.7.4</version>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
<!-- jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.23</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
</dependencies>
<build>
<finalName>spring-security-jdbc-authentication</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
In the above pom.xml file you notice additional plugin for failOnMissingWebXml
configuration is required because I am going to write Java code purely based on annotations, so I won’t need the web.xml
file in WEB-INF directory.
Deployment Descriptor
Here the following configuration class which is equivalent to the web.xml file for web applications.
public class MvcWebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[] { WebSecurityConfig.class, WebMvcConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return null;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
The above class declared Spring MVC DispatcherServlet
, that acts as a front controller to handle incoming request and response for the URL pattern "/"
. This is equivalent to declaring DispatcherServlet in web.xml file in my tutorial Spring Security Form based Authentication – XML Configuration
I have also loaded config classes WebSecurityConfig.class and WebMvcConfic.class, which are equivalent to security.xml and controllers.xml configurations in my tutorial Spring Security Form based Authentication – XML Configuration
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
}
The above class is equivalent to add the filter declaration DelegatingFilterProxy
in your web.xml file.
This provides a hook into the Spring Security web infrastructure. DelegatingFilterProxy
is a Spring Framework class which delegates to a filter implementation which is defined as a Spring bean in your application context. In this case, the bean is named springSecurityFilterChain
, which is an internal infrastructure bean created by the namespace to handle web security. Note that you should not use this bean name yourself.
Security Configuration
Create WebSecurityConfig.java that is equivalent to security.xml file under src/main/resources directory in my tutorial Spring Security Form based Authentication – XML Configuration.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Autowired
private DataSource dataSource;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
final String sqlUserName = "select u.user_name, u.user_pass, u.enable from user u where u.user_name = ?";
final String sqlAuthorities = "select ur.user_name, ur.user_role from user_role ur where ur.user_name = ?";
auth.jdbcAuthentication().dataSource(dataSource).usersByUsernameQuery(sqlUserName)
.authoritiesByUsernameQuery(sqlAuthorities).passwordEncoder(new BCryptPasswordEncoder());
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http// ,
.authorizeRequests()// ,
.antMatchers("/admin")// Ensures that request with "/admin" to
// our application requires the user to
// be authenticated
.access("hasRole('ADMIN')")// Any URL that starts with
// "/admin" will
// be restricted to users who have the
// role "ROLE_ADMIN",
.and()// ,
.formLogin()// Allows users to authenticate with form based
// login,
.loginPage("/login")// specifies the location of the log in
// page,
.loginProcessingUrl("/j_spring_security_check")// login
// processing
// URL,
.defaultSuccessUrl("/admin")// default-target-url,
.failureUrl("/login?error")// authentication-failure-url,
.usernameParameter("username")// overrides Spring's default
// j_username with
// username-parameter,
.passwordParameter("password");// overrides Spring's default
// j_password with
// password-parameter
return http.build();
}
}
Spring MVC Configuration
Create WebMvcConfig.java that is equivalent to controllers.xml file under src/main/resources directory in my tutorial Spring Security Form based Authentication – XML Configuration.
@EnableWebMvc
@Configuration
@PropertySource(value = { "classpath:messages.properties", "classpath:jdbc.properties" })
@ComponentScan(basePackages = "com.roytuts.spring.security.jdbc.authentication.controllers")
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private Environment environment;
@Bean
public ViewResolver getViewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver("/views/", ".jsp");
return viewResolver;
}
@Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("/static/");
}
@Bean("messageSource")
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages");
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
@Bean(name = "dataSource")
public DriverManagerDataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(environment.getRequiredProperty("jdbc.driverClassName"));
dataSource.setUrl(environment.getRequiredProperty("jdbc.url"));
dataSource.setUsername(environment.getRequiredProperty("jdbc.username"));
dataSource.setPassword(environment.getRequiredProperty("jdbc.password"));
return dataSource;
}
}
The annotation @EnableWebMvc
is equivalent to <mvc:annotation-driven />
to work with annotations in Spring MVC.
The addResourceHandlers()
acts in the similar way as <mvc:resources location="/static/" mapping="/static/**" />
to load static resources from static directory.
The annotation @ComponentScan
is equivalent to <context:component-scan/>
to load all annotation-driven controllers from the given base package.
I have also declared view resolver bean and message resource for i18n
supports.
Create messages.properties
file with below content and put it under src/main/resources folder
page.title=Spring Security JDBC Authentication
page.home.heading=Home Page
page.login.heading=Login Here
page.admin.heading=Administrator Control Panel
page.admin.message=This page demonstrates how to use Spring security.
page.goto.admin=Go to Administrator page
login.failure.reason=Invalid credentials
welcome.msg=Welcome
logout.text=Logout
logout.msg.success=You have been successfully logged out.
Create below below jdbc.properties
file with below content and put it under src/main/resources
folder.
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost/roytuts
jdbc.username=root
jdbc.password=root
User Auth Service
Create UserAuthService with below source code. The following class implements UserDetailsService from Spring Security API.
@Service
public class UserAuthService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Override
public org.springframework.security.core.userdetails.UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails userDetails = userDao.getUser(username);
if (userDetails == null) {// should have proper handling of Exception
throw new UsernameNotFoundException("User '" + username + "' not found.");
}
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(userDetails.getRole());
org.springframework.security.core.userdetails.UserDetails details = new User(userDetails.getUsername(),
userDetails.getPassword(), Arrays.asList(grantedAuthority));
return details;
}
}
DAO Class
The DAO class is used to access database to retrieve or insert data.
@Repository
@Transactional
public class UserDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public UserDetails getUser(String username) {
try {
final String sql = "select u.user_name user_name, u.user_pass user_pass, ur.user_role user_role from user u, user_role ur where u.user_name = ? and u.user_name = ur.user_name";
UserDetails userDetails = jdbcTemplate.queryForObject(sql, new UserRowMapper(), username);
return userDetails;
} catch (EmptyResultDataAccessException ex) {
return null;// should have proper handling of Exception
}
}
}
Mapper Class
The mapper class is used to map Java object with database table. Create mapper class with below source.
public class UserRowMapper implements RowMapper<UserDetails> {
@Override
public UserDetails mapRow(ResultSet rs, int row) throws SQLException {
UserDetails userDetails = new UserDetails();
userDetails.setUsername(rs.getString("user_name"));
userDetails.setPassword(rs.getString("user_pass"));
userDetails.setRole(rs.getString("user_role"));
return userDetails;
}
}
Model Class
The model that maintains a state of an object in Java application.
public class UserDetails {
private String username;
private String password;
private String role;
//getters and setters
//...
}
Controller Class
Create controller class with below source code. The controller class is responsible for validating and handling requests and responses for the clients or end users.
@Controller
@RequestMapping("/")
public class SpringSecurityController implements MessageSourceAware {
private MessageSource messageSource;
@Override
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
@RequestMapping("/")
public String defaultPage(Model model) {
model.addAttribute("msg", "Welcome to Spring Security");
return "index";
}
@RequestMapping("/login")
public String loginPage(Model model, @RequestParam(value = "error", required = false) String error,
@RequestParam(value = "logout", required = false) String logout) {
if (error != null) {
model.addAttribute("error", messageSource.getMessage("login.failure.reason", null, Locale.US));
}
if (logout != null) {
model.addAttribute("msg", messageSource.getMessage("logout.msg.success", null, Locale.US));
}
return "login";
}
@RequestMapping("/logout")
public String logoutPage(Model model, HttpServletRequest request) {
request.getSession().invalidate();
return "redirect:/login?logout";
}
@RequestMapping("/admin")
public String adminPage(Model model) {
model.addAttribute("title", messageSource.getMessage("page.admin.heading", null, Locale.US));
model.addAttribute("message", messageSource.getMessage("page.admin.message", null, Locale.US));
return "admin";
}
}
Style
I have applied some basic style, so create the below style.css
file and put it under src/main/webapp/static/css directory.
.error {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.msg {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
#login-box {
width: 500px;
padding: 20px;
margin: 50px auto;
background: #fff;
-webkit-border-radius: 2px;
-moz-border-radius: 2px;
border: 1px solid #000;
}
View Files
Below is the index.jsp file and put it under src/main/webapp/views
directory and see how keys are used to fetch corresponding value from messages.properties file. This index.jsp file is not secured and is accessible directly.
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title><fmt:bundle basename="messages">
<fmt:message key="page.title" />
</fmt:bundle></title>
</head>
<body>
<div align="center">
<h1>
<fmt:bundle basename="messages">
<fmt:message key="page.home.heading" />
</fmt:bundle>
</h1>
<a href="${pageContext.request.contextPath}/admin"><fmt:bundle
basename="messages">
<fmt:message key="page.goto.admin" />
</fmt:bundle></a>
</div>
</body>
</html>
Below admin.jsp file in src/main/webapp/views directory is secured and user must login before viewing the content of this file. When you try to access the admin.jsp file then you will automatically be redirected to the login.jsp file.
<%@ page language="java" session="true"
contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title><fmt:bundle basename="messages">
<fmt:message key="page.title" />
</fmt:bundle></title>
</head>
<body>
<div align="center">
<h1>${title}</h1>
<h2>${message}</h2>
<c:if test="${pageContext.request.userPrincipal.name != null}">
<h2>
<fmt:bundle basename="messages">
<fmt:message key="welcome.msg" />
</fmt:bundle>
: ${pageContext.request.userPrincipal.name} | <a
href="<c:url value='logout'/>"><fmt:bundle basename="messages">
<fmt:message key="logout.text" />
</fmt:bundle></a>
</h2>
</c:if>
</div>
</body>
</html>
The content of the login.jsp file under src/main/webapp/views directory.
<%@ page language="java" session="true"
contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt"%>
<html>
<head>
<title><fmt:bundle basename="messages">
<fmt:message key="page.title" />
</fmt:bundle></title>
<link rel="stylesheet" type="text/css"
href="<c:url value="/static/css/style.css"/>" />
</head>
<body>
<div id="login-box">
<h2>
<fmt:bundle basename="messages">
<fmt:message key="page.login.heading" />
</fmt:bundle>
</h2>
<c:if test="${not empty error}">
<div class="error">${error}</div>
</c:if>
<c:if test="${not empty msg}">
<div class="msg">${msg}</div>
</c:if>
<form name='loginForm'
action="<c:url value='j_spring_security_check' />" method='POST'>
<table>
<tr>
<td>User:</td>
<td><input type='text' name='username' value=''></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='password' /></td>
</tr>
<tr>
<td colspan='2'><input name="submit" type="submit"
value="Submit" /></td>
</tr>
</table>
<input type="hidden" name="${_csrf.parameterName}"
value="${_csrf.token}" />
</form>
</div>
</body>
</html>
MySQL Database
Create below tables in MySQL database server. The password is encrypted using Bcrypt password encoder algorithm. The decrypted value of the encrypted password is roy
.
USE `roytuts`;
/*Table structure for table `user` */
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_pass` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`enable` tinyint NOT NULL DEFAULT '1',
PRIMARY KEY (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*Data for the table `user` */
insert into `user`(`user_name`,`user_pass`,`enable`) values ('roy','$2a$12$PN0MjtyNGWW.AjWdIuYKxe4.4Grjs4K7oanuAnt/WSDSvFlUc3eQi',1);
/*Table structure for table `user_role` */
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
`user_role` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL,
KEY `fk_user` (`user_name`),
CONSTRAINT `fk_user` FOREIGN KEY (`user_name`) REFERENCES `user` (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*Data for the table `user_role` */
insert into `user_role`(`user_name`,`user_role`) values ('roy','ROLE_ADMIN');
Testing Spring Security JDBC UserDetailsService Authentication
When you deploy the application and run the application you will see different output in the browser.
When you hit the URL http://localhost:8080/spring-security-jdbc-authentication-userdetailsservice/, you will see the home page as shown below in an image.
Once you login to the application using credentials roy/roy, you will see a page similar tot he following image:
Hope you got an idea how to authenticate user with Spring UserDetailsService interface.