Building a REST API with Spring Boot
Module 2: Developing a Secure App
Lesson: Get List
Send pageable request to Spring Data.
interface CashCardRepository extends CrudRepository<CashCard, Long>, PagingAndSortingRepository<CashCard, Long> {
}
private final CashCardRepository cashCardRepository;
@GetMapping
private ResponseEntity<List<CashCard>> findAll(Pageable pageable) {
Page<CashCard> page = cashCardRepository.findAll(
PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
pageable.getSortOr(Sort.by(Sort.Direction.ASC, "amount"))
));
return ResponseEntity.ok(page.getContent());
}
Lesson: Simple Spring Security
Authentication
-
Principal is a synonym for user that can be a person or another program.
-
Authentication is the act of a Principal providing its identity to the system, ex. providing credentials (username and password). Upon providing proper credentials, the Principal is authenticated, i.e. successfully logged in.
-
Authentication Session is created once a Principal gets authenticated and can be implemented in many ways, ex. which can be implemented via Session Token placed in a Cookie.
-
Cookie is a set of data stored in a web client (browser) and associated with a specific URI. They are sent automatically to the server with every request and is persistent for a certain amount of time.
Spring Security implements Authentication via Filter Chain (org.springframework.security.web.SecurityFilterChain
) which by default returns 401 UNAUTHORIZED if the Principal is not authenticated.
Authorization
Spring Security provides Authorization via Role-Based Access Control (RBAC), which means Principal has a number of Roles. Each Resource (or operation) specifies which Roles a Principal must have in order to perform actions.
Same Origin Policy
-
Same Origin Policy (SOP) is the most basic mechanism of protection that a server implements. Only scripts which are contained in a web page are allowed to send requests to the origin (URI) of the web page.
-
Cross-Origin Resource Sharing is a way servers can cooperate to relax the SOP, by explicitly allowing a lost of "allowed origins". This is handy if a system consists of services running on machines with different URIs (Microservices). Spring Seciruty provides the
@CrossOrigin
annotation allowing to specify a lost of allowed sites and allowing by default all origins.
Common Web Exploits
-
Cross-Site Request Forgery (CSRF) happens when a malicious piece of code sends a request to a server where a user is authenticated. CSRF Token is an unique token generated on each request making it harder for an outside actor to insert itself into the conversation.
-
Cross-Site Scripting (XSS) is more dangerous and occurs when an attacker tricks a victim application into executing arbitrary code, ex. saving a string into the DB containing a malicious
<script>
tag. Unlike CSRF, XSS attacks don’t depend on Authentication. The attacks can be mitigated by properly escaping the special HTML characters.
Lab
-
Add a Spring Security dependency.
dependencies { ... implementation 'org.springframework.boot:spring-boot-starter-security' ... }
Upon adding the dependency, Spring Security is enabled. By default, all endpoints require authentication.
-
If accessed via a browser, unauthenticated requests may redirect to a login page (which might not exist if no view is configured).
-
If accessed via a REST client, a
401 UNAUTHORIZED
response is returned. -
Application errors still result in
500 INTERNAL_SERVER_ERROR
;403 FORBIDDEN
only occurs when the user is authenticated but lacks necessary authorization.
-
-
Add a minimum configuration of Filter Chain
@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); }
Upon adding the configuration, all endpoints still require authentication, and unauthenticated requests will return
401 UNAUTHORIZED
. No endpoints become accessible yet. -
Configure basic authentication.
@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(request -> request // All HTTP requests to cashcards/ require to be authenticated using HTTP Basic Authentication security (username and password). .requestMatchers("/cashcards/**").authenticated() ) .httpBasic(Customizer.withDefaults()) // Do not require CSRF security. .csrf(csrf -> csrf.disable()); return http.build(); }
Upon adjusting the configuration, the rest endpoints return
401 UNAUTHORIZED
instead as the requests must supply a username and password. -
Add an in-memory user details.
@Bean UserDetailsService testOnlyUsers(PasswordEncoder passwordEncoder) { User.UserBuilder users = User.builder(); UserDetails sarah = users .username("sarah1") .password(passwordEncoder.encode("abc123")) .roles() // No roles for now .build(); return new InMemoryUserDetailsManager(sarah); }
Upon adding, the endpoints protected by authentication become accessible using HTTP Basic credentials (e.g.,
sarah1:abc123
). -
Adjust the in-memory user details with roles and enable role-based security.
@Bean UserDetailsService testOnlyUsers(PasswordEncoder passwordEncoder) { User.UserBuilder users = User.builder(); UserDetails sarah = users .username("sarah1") .password(passwordEncoder.encode("abc123")) .roles("CARD-OWNER") // new role .build(); UserDetails hankOwnsNoCards = users .username("hank-owns-no-cards") .password(passwordEncoder.encode("qrs456")) .roles("NON-OWNER") // new role .build(); return new InMemoryUserDetailsManager(sarah, hankOwnsNoCards); }
@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(request -> request // enable RBAC: Replace the .authenticated() call with the hasRole(...) call. .requestMatchers("/cashcards/**").hasRole("CARD-OWNER")) .httpBasic(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()); return http.build(); }
Upon adjusting the configuration, the endpoints are accessible for the users with the correct role and
403 FORBIDDEN
for the remaining users. -
Restrict resources ownership.
interface CashCardRepository extends CrudRepository<CashCard, Long>, PagingAndSortingRepository<CashCard, Long> { CashCard findByIdAndOwner(Long id, String owner); Page<CashCard> findByOwner(String owner, PageRequest pageRequest); }
@GetMapping("/{requestedId}") private ResponseEntity<CashCard> findById(@PathVariable Long requestedId, Principal principal) { Optional<CashCard> cashCardOptional = Optional.ofNullable(cashCardRepository.findByIdAndOwner(requestedId, principal.getName())); if (cashCardOptional.isPresent()) { return ResponseEntity.ok(cashCardOptional.get()); } else { return ResponseEntity.notFound().build(); } }
Spring Security issued a guidance regarding non-browser clients.
When should you use CSRF protection? Our recommendation is to use CSRF protection for any request that could be processed by a browser by normal users. If you are only creating a service that is used by non-browser clients, you will likely want to disable CSRF protection.
Course: Building a REST API with Spring Boot Module 2: Developing a Secure App Lesson: https://spring.academy/courses/building-a-rest-api-with-spring-boot/lessons/implementing-put
If you need the server to return the Location
header of the created resource, then you must use POST
.
Alternatively, when the resource URI is known at creation time (for example Invoice API), you can use PUT
.
HTTP Method | Operation | Definition of Resource URI | What does it do? | Response Status Code | Response Body |
---|---|---|---|---|---|
POST |
Create |
Server generates and returns the URI |
Creates a sub-resource ("under" or "within" the passed URI) |
201 CREATED |
The created resource |
PUT |
Create |
Client supplies the URI |
Creates a resource (at the Request URI) |
201 CREATED |
The created resource |
PUT |
Update |
Client supplies the URI |
Replaces the resource: The entire record is replaced by the object in the Request |
204 NO CONTENT |
(empty) |
PATCH |
Update |
Client supplies the URI |
Partial Update: modify only fields included in the request on the existing record |
200 OK |
The updated resource |
Lesson: Implementing Delete
Delete Options
-
Hard Delete is a simple option and to delete the record from the database. With a hard delete, it’s gone forever.
-
Soft Delete is an alternative which works by marking records as "deleted" in the database, so they are retained, but marked as deleted, ex. via boolean
IS_DELETED
or timestampDELETED_DATE
.
Audit Trail and Archiving
If we use Hard Delete it is recommended to store additional data to know when and who deleted a record:
-
Archive the deleted data into a different location.
-
Add audit fields to the record itself, for example
DELETED_DATE
orDELETED_BY_USER
. This is not limited to Delete operations, but Create and Update also.