Introduction:
Spring Security is a framework that provides authentication, authorization, and protection against common attacks. With first class support for securing both imperative and reactive applications, it is the de-facto standard for securing Spring-based applications.
Consuming Authentication objects:
Spring security provides Authentication.
- Authentication (authn): represent the users.
- Principal: (name, email etc)
- GrantedAuthorities: roles.
- Authorization (authz): are the users allowed to perform.
Image from Spring Security, demystified
Demos:
In this post I integrate Custom Login Form, OAuth2 Login with Google and Basic Auth.
Technology
- Spring Boot 3.0.6
- Java 17
- OAuth2
- Maven
- IntelliJ IDEA
Security Configuration:
SecurityConfig.java: In this class, I configure Custom Login, OAuth2 Login with Google and Basic Auth. sometimes we need apply some filter before, or some case we need test with user admin in this case I create the class AdminAuthenticateProvider without password.
package com.henry.springsecurity.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeRequests( authorizeConfig -> {
authorizeConfig.requestMatchers("/").permitAll();
authorizeConfig.requestMatchers("/login/**").permitAll();
authorizeConfig.requestMatchers("/error").permitAll();
authorizeConfig.requestMatchers("/favicon.ico").permitAll();
authorizeConfig.anyRequest().authenticated();
})
.formLogin( login -> {
login.loginPage("/login").permitAll();
login.defaultSuccessUrl("/private");
login.failureUrl("/login?error=true").permitAll();
}) //Normal Login
.logout(logout -> {
logout.logoutSuccessUrl("/login?logout=true").permitAll();
logout.invalidateHttpSession(true).permitAll();
logout.deleteCookies("JSESSIONID").permitAll();
})
.httpBasic(Customizer.withDefaults()) // support basic auth
.oauth2Login(oauth -> {
oauth.loginPage("/login").permitAll();
oauth.defaultSuccessUrl("/private");
oauth.failureUrl("/login?error=true").permitAll();
}) // OpenID Connect with google
.addFilterBefore(new CustFilter(), UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(new AdminAuthenticateProvider())
.csrf()
.disable()
.build();
}
@Bean
public UserDetailsService userDetailsService(){
return new InMemoryUserDetailsManager(
User.builder()
.username("henry")
.password("{noop}password")
.authorities("ROLE_USER")
.build()
);
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring() .requestMatchers("/resources/**", "/static/**", "/css/**");
}
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeRequests( authorizeConfig -> {
authorizeConfig.requestMatchers("/").permitAll();
authorizeConfig.requestMatchers("/login/**").permitAll();
authorizeConfig.requestMatchers("/error").permitAll();
authorizeConfig.requestMatchers("/favicon.ico").permitAll();
authorizeConfig.anyRequest().authenticated();
})
.formLogin( login -> {
login.loginPage("/login").permitAll();
login.defaultSuccessUrl("/private");
login.failureUrl("/login?error=true").permitAll();
}) //Normal Login
.logout(logout -> {
logout.logoutSuccessUrl("/login?logout=true").permitAll();
logout.invalidateHttpSession(true).permitAll();
logout.deleteCookies("JSESSIONID").permitAll();
})
.httpBasic(Customizer.withDefaults()) // support basic auth
.oauth2Login(oauth -> {
oauth.loginPage("/login").permitAll();
oauth.defaultSuccessUrl("/private");
oauth.failureUrl("/login?error=true").permitAll();
}) // OpenID Connect with google
.addFilterBefore(new CustFilter(), UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(new AdminAuthenticateProvider())
.csrf()
.disable()
.build();
}
@Bean
public UserDetailsService userDetailsService(){
return new InMemoryUserDetailsManager(
User.builder()
.username("henry")
.password("{noop}password")
.authorities("ROLE_USER")
.build()
);
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring() .requestMatchers("/resources/**", "/static/**", "/css/**");
}
}
AdminAuthenticateProvider.java
package com.henry.springsecurity.config;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
public class AdminAuthenticateProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
var username = authentication.getName();
if("admin".equalsIgnoreCase(username)){
return UsernamePasswordAuthenticationToken.authenticated(
"admin",
null,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")
);
}
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
public class AdminAuthenticateProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
var username = authentication.getName();
if("admin".equalsIgnoreCase(username)){
return UsernamePasswordAuthenticationToken.authenticated(
"admin",
null,
AuthorityUtils.createAuthorityList("ROLE_ADMIN")
);
}
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
CustFilter.java
package com.henry.springsecurity.config;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class CustFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
System.out.println("Hello filter");
filterChain.doFilter(request, response);
}
}
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class CustFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
System.out.println("Hello filter");
filterChain.doFilter(request, response);
}
}
OAuth2 Login with Google Configuration:
Go to Console Cloud Google
- Create new project or choose existing project.
- Go to APIs & Services or Type oauth in search bar
- Choose Credentials
- Click on CREATE CREDENTIALS -> OAuth client ID
- Application Type -> Web application
- Name -> your app name
- Authorized redirect URIs: http://localhost:8080/login/oauth2/code/google
- CREATE
View
Create a login page our own.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Please Log In</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" >
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" >
<link rel="stylesheet" href="/css/login.css" type="text/css"/>
</head>
<body>
<section class="vh-100">
<div class="container-fluid h-custom">
<div class="row d-flex justify-content-center align-items-center h-100">
<div class="col-md-8 col-lg-6 col-xl-4 offset-xl-1">
<div
class="d-flex flex-row align-items-center justify-content-center justify-content-lg-start">
<p class="lead fw-normal mb-0 me-3">Login with OAuth 2.0</p>
</div>
<div class="d-flex flex-row align-items-center justify-content-center justify-content-lg-start">
<table class="table table-striped">
<tbody>
<tr>
<td><a href="/oauth2/authorization/google">Google</a></td>
</tr>
</tbody>
</table>
</div>
<form th:action="@{/login}" method="post">
<div class="divider d-flex align-items-center my-4">
<p class="text-center fw-bold mx-3 mb-0">Or</p>
</div>
<!-- Username input -->
<div class="form-outline mb-4">
<input type="text" name="username" class="form-control form-control-lg"
placeholder="Username" />
</div>
<!-- Password input -->
<div class="form-outline mb-3">
<input type="password" name="password" class="form-control form-control-lg"
placeholder="Password" />
</div>
<div class="text-center text-lg-start mt-4 pt-2">
<button type="submit" class="btn btn-primary btn-lg"
style="padding-left: 2.5rem; padding-right: 2.5rem;">Login</button>
</div>
</form>
</div>
</div>
</div>
</section>
</body>
</html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Please Log In</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" >
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" >
<link rel="stylesheet" href="/css/login.css" type="text/css"/>
</head>
<body>
<section class="vh-100">
<div class="container-fluid h-custom">
<div class="row d-flex justify-content-center align-items-center h-100">
<div class="col-md-8 col-lg-6 col-xl-4 offset-xl-1">
<div
class="d-flex flex-row align-items-center justify-content-center justify-content-lg-start">
<p class="lead fw-normal mb-0 me-3">Login with OAuth 2.0</p>
</div>
<div class="d-flex flex-row align-items-center justify-content-center justify-content-lg-start">
<table class="table table-striped">
<tbody>
<tr>
<td><a href="/oauth2/authorization/google">Google</a></td>
</tr>
</tbody>
</table>
</div>
<form th:action="@{/login}" method="post">
<div class="divider d-flex align-items-center my-4">
<p class="text-center fw-bold mx-3 mb-0">Or</p>
</div>
<!-- Username input -->
<div class="form-outline mb-4">
<input type="text" name="username" class="form-control form-control-lg"
placeholder="Username" />
</div>
<!-- Password input -->
<div class="form-outline mb-3">
<input type="password" name="password" class="form-control form-control-lg"
placeholder="Password" />
</div>
<div class="text-center text-lg-start mt-4 pt-2">
<button type="submit" class="btn btn-primary btn-lg"
style="padding-left: 2.5rem; padding-right: 2.5rem;">Login</button>
</div>
</form>
</div>
</div>
</div>
</section>
</body>
</html>
Controller:
LoginController.java
package com.henry.springsecurity.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
}
package com.henry.springsecurity.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
}
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
}
WebController.java
package com.henry.springsecurity.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RestController
public class WebController {
@GetMapping("/")
public String publicPage(){
return "Hello Henry";
}
@GetMapping("/private")
public String privatePage(Authentication authentication){
return "Welcome " + getName(authentication);
}
private String getName(Authentication authentication) {
return Optional.of(authentication.getPrincipal())
.filter(OidcUser.class::isInstance)
.map(OidcUser.class::cast)
.map(OidcUser::getEmail)
.orElseGet(authentication::getName);
}
}
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Optional;
@RestController
public class WebController {
@GetMapping("/")
public String publicPage(){
return "Hello Henry";
}
@GetMapping("/private")
public String privatePage(Authentication authentication){
return "Welcome " + getName(authentication);
}
private String getName(Authentication authentication) {
return Optional.of(authentication.getPrincipal())
.filter(OidcUser.class::isInstance)
.map(OidcUser.class::cast)
.map(OidcUser::getEmail)
.orElseGet(authentication::getName);
}
}
Project 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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>spring-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security</name>
<description>Demo project for Spring Boot Security</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</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>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.henry</groupId>
<artifactId>spring-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security</name>
<description>Demo project for Spring Boot Security</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</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>
application.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: YOUR_CLIENT_ID
client-secret: YOUR_CLIENT_SECRET
security:
oauth2:
client:
registration:
google:
client-id: YOUR_CLIENT_ID
client-secret: YOUR_CLIENT_SECRET
Run & Test
Run Spring Boot application with command: mvn spring-boot:run. by console, IntelliJ etc.
- Without Security go to: http://localhost:8080/ result this:
- With Security go to: http://localhost:8080/private result this:
Type:
User: henry Password: password
Note: In the case, we will be test with admin user, you can typing anything password, because I created the AdminAuthenticateProvider class and configure in Spring Security config authenticationProvider. for more details check in the end the references.
- Without Security go to: http://localhost:8080/ result this:
- With Security go to: http://localhost:8080/private result this:
Type:
User: henry
Password: password
Note: In the case, we will be test with admin user, you can typing anything password, because I created the AdminAuthenticateProvider class and configure in Spring Security config authenticationProvider. for more details check in the end the references.
https://docs.spring.io/spring-security/reference/index.html
https://docs.spring.io/spring-security/reference/features/authentication/index.html