How to Develop Full Stack Spring Boot Application on Google App Engine + Cloud SQL - Step by Step Guide

Web Application =
Spring Boot + Cloud SQL + JPA + Spring Security + Thymeleaf + Bootstrap + Google App Engine
Ver. 1.0.0, 2019-08-11



Table of Contents



Preface


There are couple tutorials available how to develop and deploy Spring Boot application on Google App Engine. This tutorial adds more layers (persistence, security, views) to create useful template which can be used as a starting point for web application.



Scope of this Guide

  1. Configure and deploy basic version of Spring Boot application to Google App Engine
  2. Configure Cloud SQL instance
  3. Configure JPA and create entities
  4. Use Thymeleaf or JSP templates to create web pages
  5. Secure application using spring-security


This guide also shows all instructions step by step, to help to understand each part of the application.
This guide can be a foundation for further development of your application - you can fetch code from GitHub (https://github.com/maciej-arkit/Spring-Boot-with-Google-App-Engine), and extend it.
This guide does not aim to be a comprehensive guide on web application development - its goal is to address the most important subjects related to Spring-Boot and Google App Engine development.

Why Google App Engine?

Google App Engine is a Platform as a Service, which allows to build scalable applications.
It is part of Google Cloud Platform, and it is only one of the options you can use to build applications on top of Google Cloud Platform.
Google App Engine is quite cost effective - it actually allows to run small applications for free, as long as they stay below certain traffic threshold, which should be enough for small applications.
Hence it can be a good alternative for everyone who is looking for scalable platform for prototyping and building multiple startup applications without a need to pay a lot.
Please remember that this guide uses also Cloud SQL. Cloud SQL do not provide free quotas, hence running application using Cloud SQL will always result in charges.
This guide uses cheapest db-f1-micro instance, which should cost ~$0.0105 per hour (see https://cloud.google.com/sql/pricing#2nd-gen-pricing for exact pricing).


Google App Engine has many benefits, but it has also some limitations.
It may not always be the most convenient for development due to its nature - i.e. as Platform as a Service, it introduces native mechanisms and limitations (ex. threads management) which may require additional effort to adjust your application, but thankfully it is quite rare.
Google provides Dev Server, which replicates GAE environment on localhost. As much as it is very helpful in majority of cases, it does not always reflects behavior of a real GAE environment 1:1.

Migrating to other providers

It is relatively easy to migrate from GAE to other environment - ex. to standard virtual machine provided by Amazon, Google or MS. Of course, the effort may be bigger if your application will start to use many services specific for GAE only - ex. tasks, queues, etc.

Prerequisites

To complete this guide, following applications are required:
  1. Java SDK 1.8
  2. Maven 3.5+
  3. Google Cloud CLI (“gcloud”)
  4. Access to terminal - preferred Linux or MacOS.
  5. Installed mysql client - required if you would like to connect directly from your machine to Google Cloud MySQL.

Google Cloud - Prices

You should be able to create and run sample application without setting up billing for your GCP account, especially if you just created new one.
Sometimes it may be required. It depends on free quotas and free tokens your account may have received from Google. Total cost of running this guide should rather not exceed 1$. If you decide to configure billing for your account, please ensure to configure billing limits so you can control  expenses. Please remember to remove resources created on GCP if you do not plan to use them after finishing this guide, to avoid unnecessary charges. For more information please refer to https://cloud.google.com/products/calculator

GitHub Sources

Sources for each exercise are available in GitHub https://github.com/maciej-arkit/Spring-Boot-with-Google-App-Engine
For each step there is related branch containing full working solution:


You can create whole project step by step, from scratch, or you can fetch sources from github and navigate between branches to follow code changes.

Step 1. Initial version of Spring Boot application

Related branch: 1-spring-boot-initial


In this section, we will configure Google App Engine account, create initial version of Spring Boot application and deploy it to Google App Engine.
NOTE: All steps executed in this chapter using gcloud CLI, can be also done manually using Google Cloud Console (https://console.cloud.google.com)


1/ Use Spring Initializr to create initial version of Spring Boot application
Send request to Spring Initializr from command line to download initial version of project:
$ curl https://start.spring.io/starter.tgz -d packaging=war \
  -d dependencies=web,cloud-gcp -d baseDir=spring-boot-and-gae-template | tar -xzvf -


Alternatively, you can go directly to https://start.spring.io/, fill out all required fields (including “Packaging: War”
“Java: 8”) and select Spring Web Starter and  GCP Support as dependencies. and click “Generate the project”


2/ Initialize Google Cloud project
From command line invoke:
gcloud init



This command will start project initialization and will take you through the setup project.
Please follow the instructions on the screen.
You will be asked couple questions:
  1. You need to login to your Google account
  2. If you have any existing configurations you will be asked to pick one or create configuration from scratch.
    For this guide, please create new configuration.
  3. Select which Google Account do you want to use
  4. Create new project and provide name for it


3/ Create Google App Engine application
Invoke following command to create Google App Engine application
$ gcloud app create



4/ Update application code
Update pom.xml - add appengine-maven-plugin:
<plugins>
...
<plugin>
  <groupId>com.google.cloud.tools</groupId>
  <artifactId>appengine-maven-plugin</artifactId>
  <version>2.0.0</version>
</plugin>
...
<plugins>


Add src/main/webapp/WEB-INF/appengine-web.xml with following content
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
   <version>1</version>
   <threadsafe>true</threadsafe>
   <runtime>java8</runtime>
</appengine-web-app>


Create new class named ShopController to render simple “Hello World!” text:
package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ShopsController {

   @GetMapping("/")
   public String hello() {
       return "Spring Boot and Google App Engine - Hello World!";
   }
}


7/ Verify that application is working
Run appengine on localhost:
$ mvn appengine.:run
Go to http://localhost:8080 and verify that sample web page is shown.
If application works properly on localhost, you can now deploy application to Google App Engine (replace <PROJECT_NAME> with name of the project created with gcloud init, replace version with any number - for this step “1” is a good choice):
$ mvn appengine:deploy -Dapp.deploy.projectId=<YOUR_PROJECT_NAME> -Dapp.deploy.version=<VERSION_NUMBER>


Go to Google Cloud Platform - App Engine dashboard (https://console.cloud.google.com/appengine) and verify that your application is deployed and is serving the traffic at https://<PROJECT_ID>.appspot.com/




FINAL NOTE: If you experience any problems, you can fetch working code from https://github.com/maciej-arkit/Spring-Boot-with-Google-App-Engine and switch to branch 1-spring-boot-initial

Step 2. Spring Boot + Cloud SQL (MySQL)

Prerequisites:
  1. Ensure that Cloud SQL API and Cloud SQL Admin API for your project are enabled:
    1. Go to https://console.developers.google.com/apis/api/sqladmin.googleapis.com/overview, ensure that your project is selected in a drop down in navigation bar, and click “Enable” button


NOTE: At the time, using PostgreSQL instance can be troublesome. At least at db-f1-micro instance, some errors and some signals of problems with connections were observed.


1/ Create Cloud SQL instance
🕒 This process may take few minutes
Please note that for the purpose of this guide we use db-f1-micro instance, which is the cheapest option.
For more details on pricing, please see: https://cloud.google.com/sql/pricing
By default gcloud creates MySQL instance. If you would like to create different, you can provide “--database-version” parameter.
# NOTE: You can change region to your preferred one (https://cloud.google.com/sql/docs/mysql/locations)
$ gcloud sql instances create [INSTANCE_NAME] --tier db-f1-micro --region us-central1



2/ Create database
Invoke command to create database:
$ gcloud sql databases create [DATABASE_NAME] --instance [INSTANCE_NAME]


Set password for default “root” user:
$ gcloud sql users set-password root --instance=[INSTANCE_NAME] --host=% --prompt-for-password


3/ Update application configuration
In this step we will need to update application configuration so it can connect to Cloud SQL database.


3.1/ Add spring-cloud-gcp-starter-sql-mysql dependency to pom.xml:
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-gcp-starter-sql-mysql</artifactId>
</dependency>


3.2/ Update application.properties:


Update application.properties as follows:
spring.datasource.username=root
spring.datasource.password=[database-user-password]
spring.cloud.gcp.sql.database-name=[database-name]
spring.cloud.gcp.sql.instance-connection-name=[instance-connection-name]


To get value for “spring.cloud.gcp.sql.instance-connection-name” property in application.properties, please invoke following command:
$ gcloud sql instances describe <INSTANCE_NAME> | grep connectionName



4/ Update ShopsController - add endpoint to test JDBC connectivity
Add jdbcTemplate and “/test” endpoint to ShopsController
ShopsController.java
@RestController
public class ShopsController {

   private final JdbcTemplate jdbcTemplate;

   public ShopsController(JdbcTemplate jdbcTemplate) {
       this.jdbcTemplate = jdbcTemplate;
   }

   @GetMapping("/")
   public String hello() {
       return "Spring Boot and Google App Engine - Hello World!";
   }

   @GetMapping("/test")
   public String test() {
       String currentTimestamp = jdbcTemplate.queryForObject("SELECT CURRENT_TIMESTAMP()", String.class);
       return "CURRENT TIMESTAMP: " + currentTimestamp;
   }
}


5/ Run and test application
Before running application, we need to create service account keys, so local application can access Cloud SQL database.
To create and download keys as a JSON file, please invoke following command:
$ gcloud iam service-accounts keys create ./key.json \
  --iam-account [IAM_SERVICE_ACCOUNT]

NOTE: As [IAM_SERVICE_ACCOUNT] you can use predefined default service account which is usually [PROJECT_ID]@appspot.gserviceaccount.com. You can check available service accounts in Google Console: https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts?supportedpurview=project&project&folder&organizationId


When key.json is downloaded, please set GOOGLE_APPLICATION_CREDENTIALS environmental variable to point to this file. This variable is required only on localhost:
$ export GOOGLE_APPLICATION_CREDENTIALS=[PATH]/key.json
Alternatively, you can set a property in application.properties file spring.cloud.gcp.credentials.location, but this property needs to be disabled when deploying to Google Cloud - otherwise application will throw exception (File not found).


Now you can try to run application on localhost:
$ mvn appengine:run


If everything works correctly, and application can connect to Cloud SQL, you should see timestamp value fetched from database:


6/ Deploy application to Google App Engine
Now if everything works on localhost, application can be deployed to Google App Engine.
Please invoke following command to deploy application to GAE:
$ mvn clean; mvn package; mvn appengine:deploy -Dapp.deploy.projectId=[PROJECT_ID] -Dapp.deploy.version=[VERSION_NUMBER]


When application is deployed it can be tested at https://[project_id].appspot.com

Additional steps

Try to connect to Cloud SQL database from command line using gcloud- this requires pre installed mysql client on your machine:
$ gcloud sql connect [INSTANCE_NAME] --user=root


FINAL NOTE: If you experience any problems, you can fetch working code from https://github.com/maciej-arkit/Spring-Boot-with-Google-App-Engine and switch to branch 2-spring-boot+cloud-sql-mysql

References



Step 3. Spring Boot + JPA



In this step we will add support for JPA.
JPA adds abstraction layer on top of JDBC and creates a link between relational database and Java objects.


1/ Add dependency to pom.xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>


2/ Create “Shop” entity
@Entity
public class Shop {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private long id;
   private String name;
   private String address;

   
}


3/ Create “ShopRepository”
package com.example.demo;

import org.springframework.data.repository.CrudRepository;

public interface ShopRepository extends CrudRepository<Shop, Long> {
}


4/ Update application.properties / create import.sql
Application needs information about SQL dialect to allow JPA to works properly.
We also would like to recreate database upon application startup and display SQL statements in the logs.
Please add following lines at the end of application.properties:
spring.jpa.database-platform=org.hibernate.dialect.MySQL57Dialect
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create


Please add src/main/resources/import.sql file with following content:
INSERT INTO shop (name, address) VALUES ('Shop-1', 'Test Street, New York');
INSERT INTO shop (name, address) VALUES ('Shop-2', 'Test Avenue, New York');
This script will be executed during application startup to create test records.


5/ Update “ShopsController” to use JPA interface
Please update ShopsController as follows. Please notice that at this point we will not use JdbcTemplate anymore.
@RestController
public class ShopsController {

   private final ShopRepository shopRepository;

   public ShopsController(ShopRepository shopRepository) {
       this.shopRepository = shopRepository;
   }

   @GetMapping("/")
   public String hello() {
       return "Spring Boot and Google App Engine - Hello World!";
   }

   @GetMapping("/shops")
   public List<Shop> getShops() {
       List<Shop> shopsList = new ArrayList<>();
       this.shopRepository.findAll().forEach(shopsList::add);
       return shopsList;
   }
}


6/ Build and test application
Now we should build and test our application on localhost and then deploy it to GAE
Run application locally and verify that endpoint http://localhost:8080/shops works properly. It should display list of shops:

References

Step 4. Add web pages (with Thymeleaf)



NOTE: In this part of the guide, we will create layout and web pages using Thymeleaf template engine.
If you do not know Thymeleaf, at this point, Thymeleaf just as a templating engine used for purpose of this guide - it is not the main subject or critical part. You don’t have to worry too much if you do not know it in and out.


1/ Add Thymeleaf and Bootstrap dependencies.
  • spring-boot-starter-thymeleaf will be used to render dynamic pages
  • thymeleaf-layout-dialect will be used to create simple pages layout (header, content, footer)
  • bootstrap will help to create nice looking pages
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
  <groupId>nz.net.ultraq.thymeleaf</groupId>
  <artifactId>thymeleaf-layout-dialect</artifactId>
  <version>${thymeleaf-layout-dialect.version}</version>
</dependency>
<dependency>
  <groupId>org.webjars</groupId>
  <artifactId>bootstrap</artifactId>
  <version>4.3.1</version>
</dependency>


2/ Update “ShopsController” to regular controller
Let’s convert ShopsController to a regular controller (change “@RestController” to “@Controller”).
Now our endpoint methods need to return String - name of a corresponding Thymeleaf template.


src/main/java/com/example/demo/ShopsController.java
@Controller
public class ShopsController {

   private final ShopRepository shopRepository;

   public ShopsController(ShopRepository shopRepository) {
       this.shopRepository = shopRepository;
   }

   @GetMapping("/")
   public String hello() {
       return "hello";
   }

   @GetMapping("/shops")
   public String shops(Model model) {
       List<Shop> shopsList = new ArrayList<>();
       this.shopRepository.findAll().forEach(shopsList::add);
       model.addAttribute("shops", shopsList);
       return "shops";
   }
}


3/ Add Thymeleaf templates
Now let’s add Thymeleaf templates, and define simple page layout consisting of header and footer:


We need to add following files:
File
Comment
/src/main/resources/templates/fragments/layout.html
Defines layout for all pages.
It includes header and footer.
/src/main/resources/templates/fragments/header.html
Defines page header
/src/main/resources/templates/fragments/footer.html
Defines page footer
/src/main/resources/templates/hello.html
Simple template for main page “/”
/src/main/resources/templates/shops.html
Simple template to display list of shops


Please find sources for each template below:
/src/main/resources/templates/fragments/layout.html
<!DOCTYPE html>
<html xmlns:layout="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="stylesheet" data-th-href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}">
   <title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">Spring Boot and Google App Engine</title>
</head>
<body>
<div th:replace="fragments/header :: header"></div>
<div class="container">
   <div layout:fragment="content">
       CONTENT
   </div>
</div>
<div th:replace="fragments/footer :: footer">&copy; Footer Text</div>
</body>
</html>


/src/main/resources/templates/fragments/header.html
<!DOCTYPE html>
<html xmlns:layout="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="stylesheet" data-th-href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}">
   <title>Header</title>
</head>
<body>
<div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom shadow-sm" th:fragment="header">
   <h5 class="my-0 mr-md-auto font-weight-normal"><a href="#" th:href="@{/}">Sample Application</a></h5>
   <nav class="my-2 my-md-0 mr-md-3">
       <a class="p-2 text-dark" href="#" th:href="@{/}">Home</a>
       <a class="p-2 text-dark" href="#" th:href="@{/shops}">Shops</a>
   </nav>
   <a class="btn btn-outline-primary" href="#">Logout</a>
</div>
</body>
</html>


/src/main/resources/templates/fragments/footer.html
<!DOCTYPE html>
<html xmlns:layout="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="stylesheet" data-th-href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}">
   <title>Footer</title>
</head>
<body>
<footer class="footer mt-5" th:fragment="footer">
   <div class="container">
       <span class="text-muted">&copy; [[${#dates.year(#dates.createNow())}]] Footer Text</span>
   </div>
</footer>
</body>
</html>


/src/main/resources/templates/hello.html
<!DOCTYPE html>
<html xmlns:layout="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html layout:decorate="~{fragments/layout}">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="stylesheet" data-th-href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}">
   <title>Shops</title>
</head>
<body>
<div layout:fragment="content">
   <div class="mt-3">
       This is a sample Spring Boot application deployable to Google App Engine.<br/>
       Click <a href="#" th:href="@{/shops}">here</a> to pull some shops from database.
   </div>
</div>
</body>
</html>


/src/main/resources/templates/shops.html
<!DOCTYPE html>
<html xmlns:layout="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html layout:decorate="~{fragments/layout}">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="stylesheet" data-th-href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}">
   <title>Shops</title>
</head>
<body>
<div layout:fragment="content">
   <h2 class="mt-2 mb-5">Shops</h2>
   <table class="table">
       <thead>
       <tr>
           <th scope="col">Id</th>
           <th scope="col">Name</th>
           <th scope="col">Address</th>
       </tr>
       </thead>
       <tbody>
       <tr data-th-each="shop : ${shops}">
           <th scope="row">[[${shop.id}]]</th>
           <td>[[${shop.name}]]</td>
           <td>[[${shop.address}]]</td>
       </tr>
       </tbody>
   </table>
</div>
</body>
</html>


6/ Build and test application
As usually, now we should build and test application on localhost and then deploy it to GAE
Run application locally and verify if everything works properly.




FINAL NOTE: If you experience any problems, you can fetch working code from https://github.com/maciej-arkit/Spring-Boot-with-Google-App-Engine and switch to branch 4-spring-boot+cloud-sql-mysql+JPA+Thymeleaf+Layout

Step 5. Secure application (with Spring-Security)



In this step application will be secured using Spring Security. Adding dependency to “spring-boot-starter-security” by default secures all application endpoints. We will also update view templates and application.properties to add simple configuration for user/password. Complete integration so application can use MySQL to store users and passwords will be configured in Step 6. Configure Spring Security to work with JPA on top of MySQL database.


1/ Add spring-boot-starter-security dependency
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>


2/ Add statically configured user and password
Add following lines at the end of application.properties:
spring.security.user.name=user
spring.security.user.password=password


3/ Update appengine-web.xml (enable sessions for Google App Engine)
Spring Security requires session to keep authentication data.
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
   <version>1</version>
   <threadsafe>true</threadsafe>
   <runtime>java8</runtime>
   <sessions-enabled>true</sessions-enabled>
</appengine-web-app>


4/ Update “Logout” button in header.html template
Now as we have configured basic security, we can update “Logout” button to point to “/logout” endpoint, which is by default exposed by Spring Security. Go to “resources/templates/fragments/header.html” and update “Logout” button as follows:
<a class="btn btn-outline-primary" href="#" th:href="@{/logout}">Logout</a>


5/ Add error page
By default Thymeleaf redirects all errors to error template (/src/main/resources/templates/error.html).
Now let’s add error.html
src/main/resources/templates/error.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
     xmlns:th="http://www.thymeleaf.org">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="stylesheet" data-th-href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}">
   <title>[[${status}]]</title>
</head>
<body>
<div class="container text-center">
   <h2>Something went wrong</h2>
   <p>[[${status}]] - [[${error}]]</p>
</div>
</body>
</html>


6/ Build and test application
As usually, now we should build and test application on localhost and then deploy it to GAE
Run application locally and verify if everything works properly.
Now you need to login to access application.


FINAL NOTE: If you experience any problems, you can fetch working code from https://github.com/maciej-arkit/Spring-Boot-with-Google-App-Engine and switch to branch 5-spring-boot+cloud-sql-mysql+JPA+Thymeleaf+spring-security

Step 6. Configure Spring Security to work with JPA on top of MySQL database



In this step we will extend spring security to store users in Cloud SQL database.
Before we create new classes, let’s add Lombok dependency to pom.xml, to keep our classes compact and readable. Lombok automatically generates getters and setters, so you do not need to write additional code.
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.18.8</version>
  <scope>provided</scope>
</dependency>


If you prefer to keep code in a classic fashion, that is okay too - you can skip adding Lombok and add standard getters and setters.


1/ Create Spring Security model classes
We need to create User and Role entities to keep information about users and their roles.
Each user can have 0...N roles.
User and Role entities will be used to provide information to Spring Security framework for purpose of authentication and authorization.


src/main/java/com/example/demo/security/models/User.java
@Entity
@Getter
@Setter
public class User implements Serializable {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private int id;
   private String username;
   private String password;
   private String email;

   @ManyToMany(fetch = FetchType.EAGER)
   private Set<Role> roles;

}



src/main/java/com/example/demo/security/models/Role.java
@Entity
@Getter
@Setter
public class Role implements Serializable {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private int id;
   private String role;
}


2/ Create repositories
Once we have defined entities (User and Role), we need to define corresponding repositories:
src/main/java/com/example/demo/security/models/UserRepository.java
public interface UserRepository extends JpaRepository<User, Integer>{

   User findByUsername(String username);

}


src/main/java/com/example/demo/security/models/RoleRepository.java
public interface RoleRepository extends JpaRepository<Role, Integer>{
}


3/ Create CustomUserDetailsService
Now we need to implement our CustomUserDetailsService which loads user information from database.
It also fetches information about roles assigned to user.


src/main/java/com/example/demo/security/CustomUserDetailsService.java
@Service
public class CustomUserDetailsService implements UserDetailsService {

   @Autowired
   private UserRepository repository;

   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       User user = repository.findByUsername(username);

       if ( user == null ) {
           throw new UsernameNotFoundException("User: [" + username + "] does not exist.");
       }

       Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
       for (Role role : user.getRoles()){
           grantedAuthorities.add(new SimpleGrantedAuthority(role.getRole()));
       }
       return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities);
   }
}


4/ Create WebSecurityConfig
We need to extend WebSecurityConfigurerAdapter to setup simple authorization rules.
Only admin users will be allowed to access “/admin/**” urls. Regular users and admins can access all other urls in the system.
com/example/demo/security/WebSecurityConfig.java
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   private UserDetailsService userDetailsService;

   @Override
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN").and()
               .authorizeRequests().antMatchers("/**").hasAnyRole("USER", "ADMIN").and()
               .formLogin().permitAll();
   }

   @Bean
   public BCryptPasswordEncoder bCryptPasswordEncoder() {
       return new BCryptPasswordEncoder();
   }
}
5/ Prepare SQL script to create initial users
Add following script at the end of “import.sql” to create “user” and “admin” users. Both users will have password “password”.
Please also notice that we assign ROLE_USER to “user” and ROLE_ADMIN to “admin”.


src/main/resources/import.sql
-- Roles
INSERT INTO role (role) values ("ROLE_USER");
INSERT INTO role (role) values ("ROLE_ADMIN");

-- Users
-- Password is "password"
INSERT INTO user (email, password, username) VALUES ('user@example.com', '$2a$10$zY0UtMQiV.8UBcYFxc4Dse/rxAnZeyP6Hioei1pfR9T0uVTpzr3Pm', 'user');
-- Password is "password"
INSERT INTO user (email, password, username) VALUES ('admin@example.com', '$2a$10$zY0UtMQiV.8UBcYFxc4Dse/rxAnZeyP6Hioei1pfR9T0uVTpzr3Pm', 'admin');

-- Assign roles to users
INSERT INTO user_roles (user_id, roles_id) VALUES ((SELECT id FROM user WHERE username='user'), (SELECT id FROM role WHERE role='ROLE_USER'));
INSERT INTO user_roles (user_id, roles_id) VALUES ((SELECT id FROM user WHERE username='admin'), (SELECT id FROM role WHERE role='ROLE_ADMIN'));


6/ Add “AdminController” and simple “Admin” dashboard
At the end, let’s add AdminController and simple Admin dashboard.
This part of the system will be available only for users having ROLE_ADMIN (please see com/example/demo/security/WebSecurityConfig.java).


src/main/java/com/example/demo/security/admin/AdminController.java
@Controller
@RequestMapping("/admin/")
public class AdminController {
   @Autowired
   private UserRepository userRepository;

   @Autowired
   private BCryptPasswordEncoder passwordEncoder;

   @PostMapping("/user/new")
   public String addUser(@RequestBody User user) {
       String pwd = user.getPassword();
       String encryptPwd = passwordEncoder.encode(pwd);
       user.setPassword(encryptPwd);
       userRepository.save(user);
       return "admin/dashboard";
   }

   @GetMapping("/dashboard")
   public String shops(Model model) {
       List<User> users = new ArrayList<>();
       this.userRepository.findAll().forEach(users::add);
       model.addAttribute("users", users);
       return "admin/dashboard";
   }
}


src/main/resources/templates/admin/dashboard.html
<!DOCTYPE html>
<html xmlns:layout="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html layout:decorate="~{fragments/layout}">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="stylesheet" data-th-href="@{/webjars/bootstrap/4.3.1/css/bootstrap.min.css}">
   <title>Shops</title>
</head>
<body>
<div layout:fragment="content">
   <h1>Admin Dashboard</h1>
   <h3 class="mt-2 mb-5">Users</h3>
   <table class="table">
       <thead>
       <tr>
           <th scope="col">Id</th>
           <th scope="col">Username</th>
           <th scope="col">E-mail</th>
       </tr>
       </thead>
       <tbody>
       <tr data-th-each="user : ${users}">
           <th scope="row">[[${user.id}]]</th>
           <td>[[${user.username}]]</td>
           <td>[[${user.email}]]</td>
       </tr>
       </tbody>
   </table>
</div>
</body>
</html>


7/ Build and test application
As usually, now we should build and test application on localhost and then deploy it to GAE
Run application locally and verify if everything works properly.
Test 1: Login as “user” (password: “password”).
Verify that you can access home page and list of shops.
Verify that you cannot access /admin/dashboard - you should see HTTP 403 message (shown below).


Test 2: Login as “admin” (password: “password”)
Verify that you can access all pages which are allowed for regular user (“/”, “/shops”). 
Verify that you can access “/admin/dashboard” - you should see dashboard page as shown below.


FINAL NOTE: If you experience any problems, you can fetch working code from https://github.com/maciej-arkit/Spring-Boot-with-Google-App-Engine and switch to branch 6-spring-boot+cloud-sql-mysql+JPA+Thymeleaf+spring-security-update

References

  1. Spring Security using Spring Data JPA + MySQL + Spring Boot: https://www.youtube.com/watch?v=IyzC1kkHZ-I
  2. Spring Boot + Spring Security + Thymeleaf Form Login Example: https://memorynotfound.com/spring-boot-spring-security-thymeleaf-form-login-example/

Resources clean up

During the training we have created couple resources on Google Cloud Platform.
It is worth to ensure that these resources are removed or disabled after the training. Otherwise your account can be charged.

Troubleshooting

Problem: Application cannot connect to Cloud SQL from localhost
Possible solution: Ensure that GOOGLE_APPLICATION_CREDENTIALS variable is configured (it should refer to credentials json file)

References


Komentarze

Popularne posty z tego bloga

How to start developing Google App Engine applications

How to deploy Ruby on Rails application on Google Compute Engine

Geospatial Queries with Google Cloud Datastore (Java)