Spring Boot is a popular framework to create web application. With it, we can easy to create stand-alone and production-ready web applications. And there is Spring Initializr tool can help us to create base project which only need choose the libraries which we need.

But, for new developers, it needs to spend your much time to learn and try, not easy to get into the swing of it. Begin from this, I will use some of posts to show how to use it to accomplish our jobs for API server without front-end.

By default, when we create a Spring Boot application with Spring Initializr, it will be a project which contains front-end and backend. You can easy to implement server processes in it, and also can implement web pages with Thymeleaf or other frameworks in same project. But now, the pattern of separating front-end and backend to 2 separated projects is very popular. If we use Spring Boot to do it, there are some different between before.

In this post we will focus on how to use Spring Security to build a project to do the user authentication and authorization, and send the results to front-end with JSON in response.

How does the Spring Security work

In this section, we only discuss how Spring Security work on Servlet based applications.

Looks following picture, Spring Security’s Servlet is based on Servlet Filters. When client send a request, container creates a FilterChain. It contains Filters and Servlet. What Filters will be used is depended on request URI. Spring Security registered the DelegatingFilterProxy at position of Filter 1. The DelegatingFilterProxy contains FilterChainProxy. When a request here, it will delegate to SecurityFilterChain which match the request URI. SecurityFilterChain will do the jobs of authentication and authorization.

To do the jobs, each SecurityFilterChain contains a serials of Security Filters, and they may have different numbers. For example, in above picture, the SecurityFilterChain 0 has n Filters, and the SecurityFilterChain N has m Filters. All these filters will be executed one by one with order.

Below is a comprehensize list of Spring Security Filter ordering.

  • ChannelProcessingFilter
  • ConcurrentSessionFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • CsrfFilter
  • LogoutFilter
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • ConcurrentSessionFilter
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

Now, let’s see how the authentication will be done when we use user’s name and password to login.

When we use username and password to login application, the UsernamePasswordAuthenticationFilter will take the job. It will use UserDetailsService bean to get the original user’s information from database or other storages, and then user PasswordEncorder encode the password in request and compare it with original. If passed, a SecurityContext will be created and filled principal, credentials and authorities and save to SecurityContextHolder. At last, call AuthenticationSuccessHandler to response. Otherwise, call AuthenticationFailureHandler to response failure.

In above picture, I marked some objects to Green. They will be used in this post to accomplish the goal.

Basic implementation with built-in logic

Ok, if you already understood how the Spring Security work, we can start to write our code.

To use Spring Security in Sprint Boot, we just need create project with following 2 dependencies

  • spring-boot-starter-security
  • spring-boot-starter-web

In demo project, we use version ‘2.2.6.RELEASE’ of them.

First, we need provide the UserDetailsService to get user’s original information from database or other storages. In this example, I just store the data in the memory.

In this example, we have 2 users. One is Tom, he has Visitor role and only has View permission to view the book list. Another is Jerry, he take the Admin role, and has full permission to add book, delete book, update book’s information, and view book list.

Permission.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.simplejourney.security.entities;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Permission {
public enum Permissions {
Create,
Delete,
Modify,
View
}

private long id;
private Permissions permission;
}

Role.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.simplejourney.security.entities;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.List;

@Data
@AllArgsConstructor
public class Role {
public enum Roles {
Admin("ROLE_ADMIN"),
Vistor("ROLE_VISITOR");

private String name;
Roles(String name) {
this.name = name;
}
}

private long id;
private Roles role;
private List<Permission> permissions;
}

User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.simplejourney.security.entities;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.List;

@Data
@AllArgsConstructor
public class User {
private long id;
private String name;
private String password;
private List<Role> roles;
}

UserDetailsServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.simplejourney.security.services.impl;

import com.simplejourney.security.entities.Permission;
import com.simplejourney.security.entities.Role;
import com.simplejourney.security.entities.User;
import com.simplejourney.security.services.DemoUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 java.util.Collection;
import java.util.HashSet;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private DemoUserService userServiceImpl;

@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
User user = userServiceImpl.findByName(name);
if (null == user) {
return null;
}

Collection<SimpleGrantedAuthority> authorities = new HashSet<>();
for (Role role : user.getRoles()) {
for (Permission permission : role.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission.getPermission().name()));
}
}

return new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(), authorities);
}
}

DemoUserService.java

1
2
3
4
5
6
7
package com.simplejourney.security.services;

import com.simplejourney.security.entities.User;

public interface DemoUserService {
User findByName(String name);
}

DemoUserServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.simplejourney.security.services.impl;

import com.simplejourney.security.entities.Permission;
import com.simplejourney.security.entities.Role;
import com.simplejourney.security.entities.User;
import com.simplejourney.security.services.DemoUserService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

@Service
public class DemoUserServiceImpl implements DemoUserService {
private Map<String, User> users = new HashMap<String, User>() {{
put("jerry", new User(0, "jerry", "$2a$10$slYQmyNdGzTn7ZLBXBChFOCHQhUkTikWVg2V95lHK7HRj/LPjaZIa",
new ArrayList<Role>() {{
add(new Role(0, Role.Roles.Admin, new ArrayList<Permission>() {{
add(new Permission(0, Permission.Permissions.Create));
add(new Permission(0, Permission.Permissions.Delete));
add(new Permission(0, Permission.Permissions.Modify));
add(new Permission(0, Permission.Permissions.View));
}}));
}}));

put("tom", new User(0, "tom", "$2a$10$slYQmyNdGzTn7ZLBXBChFOCHQhUkTikWVg2V95lHK7HRj/LPjaZIa",
new ArrayList<Role>() {{
add(new Role(0, Role.Roles.Vistor, new ArrayList<Permission>() {{
add(new Permission(0, Permission.Permissions.View));
}}));
}}));
}};

public User findByName(String name) {
return this.users.get(name);
}
}

Then, we should implement some handlers to handle the results of authentication, and send the json response to front-end.

UserLoginSuccessHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.simplejourney.security.config;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class UserLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
Map<String, Object> results = new HashMap<String, Object>() {{
put("code", 200);
put("message", "Login Succeed");
}};
String json = new Gson().toJson(results, new TypeToken<HashMap<String, Object>>(){}.getType());

response.setContentType("json/application;chartset=utf-8");
response.getWriter().write(json);
}
}

UserLoginFailureHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.simplejourney.security.config;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Map<String, Object> results = new HashMap<String, Object>() {{
put("code", HttpServletResponse.SC_UNAUTHORIZED);
put("message", "Login Failed");
put("error", e.getMessage());
}};
String json = new Gson().toJson(results, new TypeToken<HashMap<String, Object>>(){}.getType());

httpServletResponse.setContentType("json/application;chartset=utf-8");
httpServletResponse.getWriter().write(json);
}
}

UserLogoutSuccessHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.simplejourney.security.config;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Service;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Service
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
Map<String, Object> results = new HashMap<String, Object>() {{
put("code", 200);
put("message", "Logout Succeed");
}};
String json = new Gson().toJson(results, new TypeToken<HashMap<String, Object>>(){}.getType());

httpServletResponse.setContentType("json/application;chartset=utf-8");
httpServletResponse.getWriter().write(json);
}
}

To handle the exceptions which thrown during authentication, we need implement AccessDeniedHandler to handle ‘Access Denied’ exception, and AuthenticationEntryPoint for others.

UserAccessDeniedHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.simplejourney.security.config;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class UserAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
Map<String, Object> results = new HashMap<String, Object>() {{
put("code", HttpServletResponse.SC_FORBIDDEN);
put("message", "Access Denied");
}};
String json = new Gson().toJson(results, new TypeToken<HashMap<String, Object>>(){}.getType());

httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.getWriter().write(json);
}
}

DemoAuthenticationEntryPoint.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.simplejourney.security.config;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

@Component
public class DemoAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Map<String, Object> results = new HashMap<String, Object>() {{
put("code", HttpServletResponse.SC_UNAUTHORIZED);
put("message", "Authentication Failed");
put("error", e.getMessage());
}};
String json = new Gson().toJson(results, new TypeToken<HashMap<String, Object>>(){}.getType());

httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
httpServletResponse.setContentType("json/application;chartset=utf-8");
httpServletResponse.getWriter().write(json);
}
}

After above, we need manage the expired strategy of user session. When the session expired, we need notify front-end to show login page or do other actions.

DemoSessionInformationExpiredStrategy.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.simplejourney.security.config;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class DemoSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
Map<String, Object> results = new HashMap<String, Object>() {{
put("code", HttpServletResponse.SC_UNAUTHORIZED);
put("message", "Session Expired");
}};
String json = new Gson().toJson(results, new TypeToken<HashMap<String, Object>>(){}.getType());

HttpServletResponse response = sessionInformationExpiredEvent.getResponse();
response.setContentType("json/application;chartset=utf-8");
response.getWriter().write(json);
}
}

Fininally, we need create WebSecurityConfig which extends WebSecurityConfigurerAdapter

WebSecurityConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package com.simplejourney.security.config;

import com.simplejourney.security.services.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;

@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;

@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;

@Autowired
private UserLogoutSuccessHandler logoutSuccessHandler;

@Autowired
private UserAccessDeniedHandler accessDeniedHandler;

@Autowired
private DemoAuthenticationEntryPoint authenticationEntryPoint;

@Autowired
private DemoSessionInformationExpiredStrategy sessionInformationExpiredStrategy;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.
// Disable Cross-Site Request Forgery protection
csrf().disable()
// Enable Cross-Origin Resource Sharing
.cors().disable()


/**
* Resources which need not authorization
*/
.authorizeRequests()
.antMatchers("/hello", "/auth/login").permitAll()


/**
* Other resources need permissions
*/
.anyRequest().authenticated()

/**
* Login
*/
.and().formLogin()
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler)
.permitAll()


/**
* Logout
*/
.and().logout()
.logoutSuccessHandler(logoutSuccessHandler)
.permitAll()
.deleteCookies("JSESSIONID")


/**
* Exception Handling
*/
.and().exceptionHandling()
// set process when autentication failed
.authenticationEntryPoint(authenticationEntryPoint)
// set handler for access denied
.accessDeniedHandler(accessDeniedHandler)


/**
* Session
*/
.and().sessionManagement()
.maximumSessions(1)
.expiredSessionStrategy(sessionInformationExpiredStrategy);
}
}

Now, we can add some Controllers and mark the permissions to test the results of above.

First, we need define the entities and services to fetch the data of book from database or other storages. Also, I just store them in memory.

Book.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.simplejourney.security.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private long id;
private String name;
private String description;
}

BookService.java

1
2
3
4
5
6
7
8
9
10
11
12
package com.simplejourney.security.services;

import com.simplejourney.security.dto.Book;

import java.util.List;

public interface BookService {
void add(Book book);
void delete(long id);
Book update(Book book);
List<Book> findAll();
}

BookServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.simplejourney.security.services.impl;

import com.google.common.collect.Lists;
import com.simplejourney.security.dto.Book;
import com.simplejourney.security.services.BookService;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class BookServiceImpl implements BookService {
private Map<Long, Book> books = new HashMap<Long, Book>() {{
put(new Long(0), new Book(0, "Charlotte's web", "Friendship of Charlotte and Web"));
put(new Long(1), new Book(1, "Animal Farm", "Revolution of animal in a farm"));
put(new Long(2), new Book(2, "The old man and the Sea", "A story of an old man and a fish"));
}};

@Override
public void add(Book book) {
long newId = books.size();
books.put(new Long(newId), new Book(newId, book.getName(), book.getDescription()));
}

@Override
public void delete(long id) {
books.remove(id);
}

@Override
public Book update(Book book) {
books.put(book.getId(), book);
return books.get(book.getId());
}

@Override
public List<Book> findAll() {
return Lists.newArrayList(books.values());
}
}

Then, we add HelloController, it only contains ‘hello’ api and it need not permissions.

HelloController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.simplejourney.security.controllers;

import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

/**
* Need not permission, everybody can access this api
*/

@Controller
public class HelloController {
@GetMapping("/hello")
public ResponseEntity<String> hello() {
return ResponseEntity.ok("Hello, world");
}
}

After that, we add the BookController for Books.

In this controller, the ‘listAll’ api need ‘View’ permission, it makes both of Tom and Jerry can access it. And ‘add’, ‘delete’ and ‘update’ apis will need ‘Create’, ‘Delete’ and ‘Modify’ permissions, it means only Jerry can access them.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.simplejourney.security.controllers;

import com.simplejourney.security.dto.Book;
import com.simplejourney.security.services.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
@RequestMapping("/book")
public class BookController {
@Autowired
private BookService bookService;

@PreAuthorize("hasAuthority('Create')") // If use custom access decision, remove it
@PostMapping()
public ResponseEntity add(@RequestBody Book book) {
bookService.add(book);
return ResponseEntity.ok().build();
}

@PreAuthorize("hasAuthority('Delete')") // If use custom access decision, remove it
@DeleteMapping("/{id}")
public ResponseEntity delete(@PathVariable long id) {
bookService.delete(id);
return ResponseEntity.ok().build();
}

@PreAuthorize("hasAuthority('Modify')") // If use custom access decision, remove it
@PutMapping()
public ResponseEntity<Book> update(@RequestBody Book book) {
return ResponseEntity.ok(bookService.update(book));
}

@PreAuthorize("hasAuthority('View')") // If use custom access decision, remove it
@GetMapping()
public ResponseEntity<List<Book>> listAll() {
return ResponseEntity.ok(bookService.findAll());
}
}

Ok, we can build and launch our application to test.

First, we add ‘Hello’ request to test the result before login

send it, it should always succeed.

Now, add ‘List Book’ request

send it without login, you should receive ‘Unauthorized’ response

Now, add ‘Login’ request in Postman as following

send it, if succeed, you will see

otherwise, you will see

send ‘List Book’ request again, you should get book list now.

Add other 3 requests, and also use current session to send requests, you should get ‘Access Denied’ response.

Then logout and re-login with Jerry. You should get succeed responses for all requests.

Customize authentication process

With above, the basic authentication and authorization process has been done. But, there is a problem. See the login request in Postman, you will find the username and password is shown in url. For security, it is not a good idea. We need move them into request body.

Read the source code of UsernamePasswordAuthnticationFilter class, we can find how the username and password is gotten from path variables.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package org.springframework.security.web.authentication;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;

public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}

@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}

@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}

protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}

public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}

public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}

public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}

public final String getUsernameParameter() {
return this.usernameParameter;
}

public final String getPasswordParameter() {
return this.passwordParameter;
}
}

So, if we want to get them from body, we just need customize this bean. To implement it, we can extends it and override ‘attemptAuthentication’ method.

JsonAuthenticationFilter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.simplejourney.security.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.Map;

/**
* For login with Json data in body
*/

@Component
public class JsonAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
/**
* Must has, otherwise following error will be occurred during launching application.
* "java.lang.IllegalArgumentException: authenticationManager must be specified"
*/
@Override
@Autowired
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = null;
try (InputStream is = request.getInputStream()) {
Map<String, String> authentication = mapper.readValue(is, Map.class);
usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(authentication.get("username"), authentication.get("password"));
} catch (Exception ex) {
ex.printStackTrace();
usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken("", "");
} finally {
setDetails(request, usernamePasswordAuthenticationToken);
return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
}
} else {
return super.attemptAuthentication(request, response);
}
}
}

Then we need modify the WebSecurityConfig.java to make it work

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
package com.simplejourney.security.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.simplejourney.security.services.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;

@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;

@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;

@Autowired
private UserAccessDeniedHandler accessDeniedHandler;

@Autowired
private DemoAuthenticationEntryPoint authenticationEntryPoint;

@Autowired
private DemoSessionInformationExpiredStrategy sessionInformationExpiredStrategy;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

UsernamePasswordAuthenticationFilter jsonAuthenticationFilter() throws Exception {
JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.
// Disable Cross-Site Request Forgery protection
csrf().disable()
// Enable Cross-Origin Resource Sharing
.cors().disable()


/**
* Resources which need not authorization
*/
.authorizeRequests()
.antMatchers("/hello", "/auth/login").permitAll()


/**
* Other resources need permissions
*/
.anyRequest().authenticated()


/**
* Logout
*/
.and().logout()
.permitAll()
.deleteCookies("JSESSIONID")


/**
* Exception Handling
*/
.and().exceptionHandling()
// set process when autentication failed
.authenticationEntryPoint(authenticationEntryPoint)
// set handler for access denied
.accessDeniedHandler(accessDeniedHandler)


/**
* Session
*/
.and().sessionManagement()
.maximumSessions(1)
.expiredSessionStrategy(sessionInformationExpiredStrategy);

// For login with Json data in body
http.addFilterAt(jsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}

Relaunch and add new login request in Postman as following

send it, you should get succeed response.

Enhance authorization for APIs

By default, the permission is hard-coded on the apis with following code during developing

1
@PreAuthorize("hasAuthority('View')")

If we has requirements to ask it should can be configured after deployed, how to make it? There also has easy way to make it to truth.

The AbstractSecurityInterceptor is used to get the required permissions and compare with current user’s. If has, the api will be called; otherwise, ‘Access Denied’ exception will be thrown.

To make it, we need implement some classes

  • DemoAbstractSecurityInterceptor extends AbstractSecurityInterceptor class and implements Filter interface as the entrance to run our logic
  • DemoFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource interface to fetch the permissions of each api from database or other storages
  • DemoAccessDecisionManager implements AccessDecisionManager to judge the permissions are match or not.
  • PathPermissionService interface and PathPermissionServiceImpl to fetch permissions by request path and method

Following are code of them

DemoAbstractSecurityInterceptor.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.simplejourney.security.config;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Iterator;

/**
* for Custom Access Decision
*/

@Component
public class DemoAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
Iterator<ConfigAttribute> iterator = collection.iterator();
while (iterator.hasNext()) {
ConfigAttribute ca = iterator.next();
// permission of current need
String needRole = ca.getAttribute();
// all permissions of user's
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
Permission.Permissions permission = Permission.Permissions.valueOf(authority.getAuthority());
if (permission.code().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("No Permission");
}

@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}

@Override
public boolean supports(Class<?> aClass) {
return true;
}
}

DemoFilterInvocationSecurityMetadataSource.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.simplejourney.security.config;

import com.simplejourney.security.entities.Permission;
import com.simplejourney.security.services.PathPermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.List;

/**
* for Custom Access Decision
*/

@Component
public class DemoFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
private PathPermissionService pathPermissionService;

@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) o).getRequestUrl();
String method = ((FilterInvocation) o).getRequest().getMethod();

System.out.println(String.format("[DemoFilterInvocationSecurityMetadataSource] Request URL: %s", requestUrl));

List<Permission.Permissions> permissions = pathPermissionService.findByPathAndMethod(requestUrl, method);
if (null == permissions || permissions.isEmpty()) {
return null;
}

String[] attributes = new String[permissions.size()];
for (int index = 0; index < permissions.size(); index++) {
attributes[index] = permissions.get(index).code();
}

return SecurityConfig.createList(attributes);
}

@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}

@Override
public boolean supports(Class<?> aClass) {
return true;
}
}

DemoAccessDecisionManager.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.simplejourney.security.config;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.Iterator;

/**
* for Custom Access Decision
*/

@Component
public class DemoAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
Iterator<ConfigAttribute> iterator = collection.iterator();
while (iterator.hasNext()) {
ConfigAttribute ca = iterator.next();
// permission of current need
String needRole = ca.getAttribute();
// all permissions of user's
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
Permission.Permissions permission = Permission.Permissions.valueOf(authority.getAuthority());
if (permission.code().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("No Permission");
}

@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}

@Override
public boolean supports(Class<?> aClass) {
return true;
}
}

PathPermissionService.java

1
2
3
4
5
6
7
8
9
package com.simplejourney.security.services;

import com.simplejourney.security.entities.Permission;

import java.util.List;

public interface PathPermissionService {
List<Permission.Permissions> findByPathAndMethod(String path, String method);
}

PathPermissionServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.simplejourney.security.services.impl;

import com.simplejourney.security.entities.Permission;
import com.simplejourney.security.services.PathPermissionService;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class PathPermissionServiceImpl implements PathPermissionService {
@Data
@AllArgsConstructor
class PathPermission {
private String path;
private String requestMethod;
private List<Permission.Permissions> permissions;
}

private List<PathPermission> pps = new ArrayList<PathPermission>() {{
add(new PathPermission("/book", "POST", new ArrayList<Permission.Permissions>() {{ add(Permission.Permissions.Create); }}));
add(new PathPermission("/book", "DELETE", new ArrayList<Permission.Permissions>() {{ add(Permission.Permissions.Delete); }}));
add(new PathPermission("/book", "PUT", new ArrayList<Permission.Permissions>() {{ add(Permission.Permissions.Modify); }}));
add(new PathPermission("/book", "GET", new ArrayList<Permission.Permissions>() {{ add(Permission.Permissions.View); }}));
}};

@Override
public List<Permission.Permissions> findByPathAndMethod(String path, String method) {
for (PathPermission pp : pps) {
if (pp.getPath().equals(path) && pp.getRequestMethod().equals(method)) {
return pp.getPermissions();
}
}
return null;
}
}

Then, make small changes on Permissions enum to make it easy to map the value which load from database or other storages.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.simplejourney.security.entities;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Permission {
public enum Permissions {
Create("PERMISSION_CREATE"),
Delete("PERMISSION_DELETE"),
Modify("PERMISSION_MODIFY"),
View("PERMISSION_VIEW");

private String code;
Permissions(String code) {
this.code = code;
}

public String code() {
return this.code;
}
}

private long id;
private Permissions permission;
}

After above, remove all PreAuthorize annotations from apis.

At last, modify WebSecurityConfig to make it enabled.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package com.simplejourney.security.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.simplejourney.security.services.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;

@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;

@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;

@Autowired
private UserAccessDeniedHandler accessDeniedHandler;

@Autowired
private DemoAuthenticationEntryPoint authenticationEntryPoint;

@Autowired
private DemoSessionInformationExpiredStrategy sessionInformationExpiredStrategy;

@Autowired
private DemoAbstractSecurityInterceptor securityInterceptor;

@Autowired
private DemoFilterInvocationSecurityMetadataSource securityMetadataSource;

@Autowired
private DemoAccessDecisionManager accessDecisionManager;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

UsernamePasswordAuthenticationFilter jsonAuthenticationFilter() throws Exception {
JsonAuthenticationFilter filter = new JsonAuthenticationFilter();
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.
// Disable Cross-Site Request Forgery protection
csrf().disable()
// Enable Cross-Origin Resource Sharing
.cors().disable()


/**
* Resources which need not authorization
*/
.authorizeRequests()
.antMatchers("/hello", "/auth/login").permitAll()


/**
* Other resources need permissions
*/
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(accessDecisionManager);//决策管理器
o.setSecurityMetadataSource(securityMetadataSource);//安全元数据源
return o;
}
})


/**
* Logout
*/
.and().logout()
.permitAll()
.deleteCookies("JSESSIONID")


/**
* Exception Handling
*/
.and().exceptionHandling()
// set process when autentication failed
.authenticationEntryPoint(authenticationEntryPoint)
// set handler for access denied
.accessDeniedHandler(accessDeniedHandler)


/**
* Session
*/
.and().sessionManagement()
.maximumSessions(1)
.expiredSessionStrategy(sessionInformationExpiredStrategy);

// for custom access decision
http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);

// For login with Json data in body
http.addFilterAt(jsonAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}

OK, all done. Launch and re-test with requests in Postman. All of above should work well.

Conclusion

I hope this post can give you help. If you have any questions or suggestions, please feel free to add your comments to Issue

Example project for this post

  • Example Project

    please read the comments in the code, follow the hints of it and comment or uncomment some code to reproduce what we discussing in this post.

Postman files:

References

Spring Security Reference