Spring считается надежным фреймворком в экосистеме Java и широко используется. Уже неправомерно называть Spring фреймворком, поскольку это скорее зонтичный термин, охватывающий различные фреймворки. Одним из таких фреймворков является Spring Security, который представляет собой мощный и настраиваемый фреймворк аутентификации и авторизации. Он считается стандартом де-факто для обеспечения безопасности приложений на базе Spring.
Несмотря на его популярность, я должен признать, что когда речь идет об одностраничных приложениях, его нельзя назвать простым и понятным в настройке. Я подозреваю, что причина этого в том, что изначально это был фреймворк, ориентированный на приложения MVC, где рендеринг веб-страниц происходит на стороне сервера, а взаимодействие основано на сеансах.
Если бэкэнд основан на Java и Spring, имеет смысл использовать Spring Security для аутентификации/авторизации и настроить его для связи без статического состояния. Я решил написать эту статью, в которой постараюсь обобщить и охватить все тонкие детали и трудности, с которыми вы можете столкнуться в процессе настройки.
— Определение терминологии
Прежде чем погрузиться в технические детали, я хочу четко определить терминологию, используемую в контексте Spring Security, чтобы убедиться, что мы все говорим на одном языке.
Вот термины, которые мы должны рассмотреть:
Аутентификация — это процесс проверки личности пользователя на основе предоставленных учетных данных. Обычный пример — ввод имени пользователя и пароля при входе на сайт. Это можно рассматривать как ответ на вопрос «Кто ты?
Авторизация относится к процессу определения того, имеет ли пользователь разрешение на выполнение определенного действия или чтение определенных данных, предполагая, что пользователь успешно прошел аутентификацию. По сути, это ответ на вопрос: «Может ли пользователь сделать или прочитать это?
Принцип относится к текущему аутентифицированному пользователю.
Предоставленные полномочия относятся к разрешению аутентифицированного пользователя.
Роль относится к группе разрешений аутентифицированного пользователя.
— Создание базового приложения Spring
Прежде чем мы перейдем к настройке фреймворка Spring Security, давайте создадим базовое веб-приложение Spring. Для этого мы можем использовать Spring Initializr и сгенерировать пример проекта. Для простого веб-приложения достаточно зависимости от веб-фреймворка Spring:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
После создания проекта мы можем добавить простой REST-контроллер следующим образом:
@RestController @RequestMapping("hello")
public class HelloRestController {
@GetMapping("user")
public String helloUser() {
return "Hello User";
}
@GetMapping("admin")
public String helloAdmin() {
return "Hello Admin";
}
}
После этого, если мы соберем и запустим проект, мы сможем получить доступ к следующим URL-адресам в веб-браузере:
http://localhost:8080/hello/user => Hello User
http://localhost:8080/hello/admin => Здравствуйте, админ.
Теперь мы можем добавить фреймворк Spring Security в наш проект, и мы можем сделать это, добавив следующую зависимость в наш файл pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
Добавление других зависимостей Spring framework обычно не оказывает немедленного влияния на приложение, пока мы не предоставим соответствующую конфигурацию, но Spring Security отличается тем, что оказывает немедленное влияние, что обычно сбивает с толку новых пользователей. После его добавления, если мы пересоздадим и запустим проект, а затем попытаемся обратиться к одному из вышеуказанных URL-адресов, вместо просмотра результата мы будем перенаправлены на http://localhost:8080/login . Это поведение по умолчанию, поскольку Spring Security требует аутентификации для всех URL-адресов в первую очередь.
Чтобы пропустить аутентификацию, мы можем использовать стандартное имя пользователя user и найти автоматически сгенерированный пароль в нашей консоли:
Using generated security password: 1fc10045-dfaa-4baq-a119-e32ez32c99ez
Помните, что пароль меняется каждый раз при повторном запуске приложения. Если мы хотим изменить это поведение и сделать пароль статичным, мы можем добавить следующую конфигурацию в файл application.properties:
spring.security.user.password=Passer123@
Теперь, если мы введем учетные данные в форму входа, мы будем перенаправлены на наш URL и увидим правильный результат. Обратите внимание, что процесс аутентификации «из коробки» основан на сеансе, и если мы хотим выйти из системы, мы можем обратиться к следующему URL-адресу
http://localhost:8080/logout
Такое поведение «из коробки» может быть полезно для классических веб-приложений MVC, где у нас есть аутентификация на основе сеансов, но в случае одностраничных приложений оно, как правило, не пригодится, поскольку в большинстве случаев мы используем рендеринг на стороне клиента и аутентификацию без статических данных на основе JWT. В этом случае нам потребуется серьезная настройка фреймворка Spring Security, что мы и сделаем в оставшейся части статьи.
В качестве примера мы реализуем классическое приложение веб-магазина и создадим back-end, который будет предоставлять CRUD API для создания поставщиков и товаров, а также API для управления пользователями и аутентификации.
Обзор архитектуры Spring Security
Прежде чем приступить к настройке конфигурации, давайте сначала рассмотрим, как аутентификация Spring Security работает за кулисами.
Следующая диаграмма показывает поток и то, как обрабатываются запросы на аутентификацию:
— Архитектура безопасности Spring
Теперь давайте разобьем эту диаграмму на компоненты и обсудим каждый из них отдельно.
Пружина Защитная фильтрующая цепь
Когда вы добавляете фреймворк Spring Security в свое приложение, он автоматически регистрирует цепочку фильтров, которая перехватывает все входящие запросы. Эта цепочка состоит из нескольких фильтров, и каждый фильтр обрабатывает определенный случай использования.
Например:
Проверьте, является ли запрашиваемый URL общедоступным, в зависимости от конфигурации.
В случае аутентификации на основе сеанса, проверьте, не является ли пользователь уже аутентифицированным в текущем сеансе.
Проверьте, авторизован ли пользователь для выполнения запрашиваемого действия, и так далее.
Хочу отметить одну важную деталь: фильтры Spring Security регистрируются с наименьшим порядком и являются первыми вызываемыми фильтрами. Для некоторых случаев использования, если вы хотите разместить свой пользовательский фильтр перед ними, вам нужно будет добавить прокладку к их порядку. Это можно сделать с помощью следующей конфигурации:
spring.security.filter.order=10
Когда мы добавим эту конфигурацию в файл application.properties, у нас появится место для 10 пользовательских фильтров перед фильтрами Spring Security.
AuthenticationManager
AuthenticationManager можно рассматривать как координатор, в котором можно зарегистрировать несколько провайдеров, и в зависимости от типа запроса он будет направлять запрос на аутентификацию нужному провайдеру.
AuthenticationProvider
AuthenticationProvider работает с определенными типами аутентификации. Его интерфейс раскрывает только две функции:
-
authenticate выполняет аутентификацию запроса.
-
supports проверяет, поддерживает ли данный провайдер указанный тип аутентификации.
Важной реализацией интерфейса, который мы используем в нашем примере проекта, является DaoAuthenticationProvider, который получает данные пользователя из UserDetailsService .
UserDetailsService
UserDetailsService описан в документации Spring как базовый интерфейс, который загружает данные, специфичные для пользователя.
В большинстве случаев провайдеры аутентификации извлекают идентификационную информацию пользователя из учетных данных в базе данных, а затем выполняют проверку. Поскольку этот случай использования настолько распространен, разработчики Spring решили выделить его в отдельный интерфейс, который раскрывает единственную функцию :
loadUserByUsername принимает имя пользователя в качестве параметра и возвращает объект идентификации пользователя.
Аутентификация с помощью JWT в Spring Security
Обсудив внутреннее устройство фреймворка Spring Security, давайте настроим его для аутентификации без статических данных с помощью токена JWT.
Чтобы настроить Spring Security, нам нужен класс конфигурации, аннотированный аннотацией @EnableWebSecurity в нашем пути класса. Кроме того, чтобы упростить процесс настройки, фреймворк раскрывает класс WebSecurityConfigurerAdapter. Мы расширим этот адаптер и переопределим две его функции для :
1 — Настройте обработчик аутентификации с правильным провайдером.
2 — Настройка веб-безопасности (публичные URL, частные URL, авторизация и т.д.)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO configure authentication manager
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO configure web security
}
}
В нашем примере приложения мы храним идентификаторы пользователей в базе данных H2, в коллекции users. Эти личности отображаются сущностью User, а их CRUD-операции определяются репозиторием UserRepo Spring Data.
Теперь, когда мы принимаем запрос на аутентификацию, нам нужно получить правильную идентификацию из базы данных, используя предоставленные учетные данные, а затем проверить ее. Для этого нам нужна реализация интерфейса UserDetailsService, который определяется следующим образом:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException;
}
Здесь мы видим, что нам нужно вернуть объект, реализующий интерфейс UserDetails, а наша сущность User реализует его (подробности реализации см. в репозитории проекта примера). Поскольку он раскрывает только прототип единственной функции, мы можем рассматривать его как функциональный интерфейс и предоставить реализацию в виде лямбда-выражения.
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserRepo userRepo;
public SecurityConfig(UserRepo userRepo) {
this.userRepo = userRepo;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(username -> userRepo
.findByUsername(username)
.orElseThrow(
() -> new UsernameNotFoundException(
format("User: %s, not found", username)
)
));
}
// Details omitted for brevity
}
Здесь вызов функции auth.userDetailsService инициирует экземпляр DaoAuthenticationProvider, используя нашу реализацию интерфейса UserDetailsService, и регистрирует его в менеджере аутентификации.
С помощью провайдера аутентификации нам нужно настроить обработчик аутентификации с правильной схемой кодирования пароля, которая будет использоваться для проверки учетных данных. Для этого нам нужно представить предпочтительную реализацию интерфейса PasswordEncoder в виде боба.
В нашем примере проекта мы будем использовать алгоритм хэширования паролей bcrypt.
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserRepo userRepo;
public SecurityConfig(UserRepo userRepo) {
this.userRepo = userRepo;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(username -> userRepo
.findByUsername(username)
.orElseThrow(
() -> new UsernameNotFoundException(
format("User: %s, not found", username)
)
));
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// Details omitted for brevity
}
Настроив обработчик аутентификации, нам теперь нужно настроить веб-безопасность. Мы реализуем REST API и нуждаемся в аутентификации без статических данных с помощью маркера JWT; поэтому нам необходимо установить следующие параметры:
- Включите CORS и отключите CSRF.
- Установите управление сеансами в режим stateless.
- Установите обработчик исключений для неавторизованных запросов.
- Установите разрешения на конечных точках.
- Добавьте фильтр для токенов JWT.
Эта конфигурация реализуется следующим образом:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserRepo userRepo;
private final JwtTokenFilter jwtTokenFilter;
public SecurityConfig(UserRepo userRepo,
JwtTokenFilter jwtTokenFilter) {
this.userRepo = userRepo;
this.jwtTokenFilter = jwtTokenFilter;
}
// Details omitted for brevity
@Override
protected void configure(HttpSecurity http) throws Exception {
// Enable CORS and disable CSRF
http = http.cors().and().csrf().disable();
// Set session management to stateless
http = http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
// Set unauthorized requests exception handler
http = http
.exceptionHandling()
.authenticationEntryPoint(
(request, response, ex) -> {
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
ex.getMessage()
);
}
)
.and();
// Set permissions on endpoints
http.authorizeRequests()
// Our public endpoints
.antMatchers("/api/public/**").permitAll()
.antMatchers(HttpMethod.GET, "/api/author/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/author/search").permitAll()
.antMatchers(HttpMethod.GET, "/api/book/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/book/search").permitAll()
// Our private endpoints
.anyRequest().authenticated();
// Add JWT token filter
http.addFilterBefore(
jwtTokenFilter,
UsernamePasswordAuthenticationFilter.class
);
}
// Used by spring security if CORS is enabled.
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
Обратите внимание, что мы добавили JwtTokenFilter перед внутренним Spring Security UsernamePasswordAuthenticationFilter. Мы делаем это потому, что на данном этапе нам нужен доступ к идентификатору пользователя для выполнения аутентификации/авторизации, а его извлечение осуществляется в фильтре JWT-токенов на основе предоставленного JWT-токена. Это реализуется следующим образом:
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final UserRepo userRepo;
public JwtTokenFilter(JwtTokenUtil jwtTokenUtil,
UserRepo userRepo) {
this.jwtTokenUtil = jwtTokenUtil;
this.userRepo = userRepo;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
// Get authorization header and validate
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (isEmpty(header) || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
// Get jwt token and validate
final String token = header.split(" ")[1].trim();
if (!jwtTokenUtil.validate(token)) {
chain.doFilter(request, response);
return;
}
// Get user identity and set it on the spring security context
UserDetails userDetails = userRepo
.findByUsername(jwtTokenUtil.getUsername(token))
.orElse(null);
UsernamePasswordAuthenticationToken
authentication = new UsernamePasswordAuthenticationToken(
userDetails, null,
userDetails == null ?
List.of() : userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}
Прежде чем реализовать нашу API-функцию входа в систему, нам нужно позаботиться еще об одном шаге: нам нужен доступ к менеджеру аутентификации. По умолчанию он не является общедоступным, и нам нужно явно раскрыть его как боб в нашем классе конфигурации.
Это можно сделать следующим образом:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Details omitted for brevity
@Override @Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
И теперь мы готовы реализовать нашу функцию API входа в систему:
@Api(tags = "Authentication")
@RestController @RequestMapping(path = "api/public")
public class AuthApi {
private final AuthenticationManager authenticationManager;
private final JwtTokenUtil jwtTokenUtil;
private final UserViewMapper userViewMapper;
public AuthApi(AuthenticationManager authenticationManager,
JwtTokenUtil jwtTokenUtil,
UserViewMapper userViewMapper) {
this.authenticationManager = authenticationManager;
this.jwtTokenUtil = jwtTokenUtil;
this.userViewMapper = userViewMapper;
}
@PostMapping("login")
public ResponseEntity<UserView> login(@RequestBody @Valid AuthRequest request) {
try {
Authentication authenticate = authenticationManager
.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(), request.getPassword()
)
);
User user = (User) authenticate.getPrincipal();
return ResponseEntity.ok()
.header(
HttpHeaders.AUTHORIZATION,
jwtTokenUtil.generateAccessToken(user)
)
.body(userViewMapper.toUserView(user));
} catch (BadCredentialsException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
}
Здесь мы проверяем предоставленные учетные данные с помощью обработчика аутентификации, и в случае успеха генерируем токен JWT и отправляем его обратно в качестве заголовка ответа вместе с идентификационной информацией пользователя в теле ответа.
Авторизация с помощью Spring Security
В предыдущем разделе мы установили процесс аутентификации и настроили публичные/частные URL-адреса. Этого может быть достаточно для простых приложений, но для большинства реальных случаев использования нам все равно нужны политики доступа на основе ролей для наших пользователей. В этой главе мы рассмотрим этот вопрос и реализуем схему авторизации на основе ролей, используя фреймворк Spring Security.
В нашем примере приложения мы определили следующие три роли:
USER_ADMIN позволяет нам управлять пользователями приложения.
PARTNER_ADMIN позволяет нам управлять поставщиками.
PRODUCT_ADMIN позволяет нам управлять продуктами.
Теперь нам нужно применить их к соответствующим URL-адресам:
api/public является общедоступным.
api/admin/user могут получить доступ пользователи с ролью USER_ADMIN.
api/partner могут получить доступ пользователи с ролью PARTNER_ADMIN.
api/product могут получить доступ пользователи с ролью PRODUCT_ADMIN.
Фреймворк Spring Security предоставляет нам два варианта настройки схемы авторизации:
Конфигурация на основе URL
Конфигурация на основе аннотаций
Во-первых, давайте посмотрим, как работает конфигурация на основе URL. Его можно применить к конфигурации веб-безопасности следующим образом:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Details omitted for brevity
@Override
protected void configure(HttpSecurity http) throws Exception {
// Enable CORS and disable CSRF
http = http.cors().and().csrf().disable();
// Set session management to stateless
http = http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
// Set unauthorized requests exception handler
http = http
.exceptionHandling()
.authenticationEntryPoint(
(request, response, ex) -> {
response.sendError(
HttpServletResponse.SC_UNAUTHORIZED,
ex.getMessage()
);
}
)
.and();
// Set permissions on endpoints
http.authorizeRequests()
// Our public endpoints
.antMatchers("/api/public/**").permitAll()
.antMatchers(HttpMethod.GET, "/api/partner/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/partner/search").permitAll()
.antMatchers(HttpMethod.GET, "/api/product/**").permitAll()
.antMatchers(HttpMethod.POST, "/api/product/search").permitAll()
// Our private endpoints
.antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN)
.antMatchers("/api/partner/**").hasRole(Role.PARTNER_ADMIN)
.antMatchers("/api/product/**").hasRole(Role.PRODUCT_ADMIN)
.anyRequest().authenticated();
// Add JWT token filter
http.addFilterBefore(
jwtTokenFilter,
UsernamePasswordAuthenticationFilter.class
);
}
// Details omitted for brevity
}
Как видите, этот подход прост и понятен, но у него есть недостаток. Схема авторизации нашего приложения может быть сложной, и если мы определим все правила в одном месте, она станет очень большой, сложной и трудночитаемой. По этой причине я обычно предпочитаю использовать конфигурацию на основе аннотаций.
Фреймворк Spring Security определяет следующие аннотации для веб-безопасности:
@PreAuthorize поддерживает язык выражений Spring и используется для обеспечения контроля доступа на основе выражений перед выполнением метода.
@PostAuthorize поддерживает язык выражений Spring и используется для обеспечения контроля доступа на основе выражений после выполнения метода (предоставляет возможность доступа к результату метода).
@PreFilter поддерживает язык выражений Spring и используется для фильтрации коллекции или массивов перед выполнением метода на основе определенных нами пользовательских правил безопасности.
@PostFilter поддерживает язык выражений Spring и используется для фильтрации коллекции или массивов, возвращаемых после выполнения метода, в соответствии с определенными нами правилами безопасности (обеспечивает возможность доступа к результату метода).
@Secured не поддерживает Spring Expression Language и используется для указания списка ролей для метода.
@RolesAllowed не поддерживает Spring Expression Language и является аннотацией JSR 250, эквивалентной аннотации @Secured.
Эти аннотации отключены по умолчанию и могут быть включены в нашем приложении следующим образом:
@EnableWebSecurity
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Details omitted for brevity
}
securedEnabled = true включает аннотацию @Secured.
jsr250Enabled = true включает аннотацию @RolesAllowed.
prePostEnabled = true включает аннотации @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter.
После их включения мы можем применить политики доступа на основе ролей к конечным точкам API следующим образом:
@Api(tags = "UserAdmin")
@RestController @RequestMapping(path = "api/admin/user")
@RolesAllowed(Role.USER_ADMIN)
public class UserAdminApi {
// Details omitted for brevity
}
@Api(tags = "Partner")
@RestController @RequestMapping(path = "api/partner")
public class AuthorApi {
// Details omitted for brevity
@RolesAllowed(Role.PARTNER_ADMIN)
@PostMapping
public void create() { }
@RolesAllowed(Role.PARTNER_ADMIN)
@PutMapping("{id}")
public void edit() { }
@RolesAllowed(Role.PARTNER_ADMIN)
@DeleteMapping("{id}")
public void delete() { }
@GetMapping("{id}")
public void get() { }
@GetMapping("{id}/product")
public void getProducts() { }
@PostMapping("search")
public void search() { }
}
@Api(tags = "Product")
@RestController @RequestMapping(path = "api/product")
public class BookApi {
// Details omitted for brevity
@RolesAllowed(Role.PRODUCT_ADMIN)
@PostMapping
public BookView create() { }
@RolesAllowed(Role.PRODUCT_ADMIN)
@PutMapping("{id}")
public void edit() { }
@RolesAllowed(Role.PRODUCT_ADMIN)
@DeleteMapping("{id}")
public void delete() { }
@GetMapping("{id}")
public void get() { }
@GetMapping("{id}/partner")
public void getPartners() { }
@PostMapping("search")
public void search() { }
}
Обратите внимание, что аннотации безопасности могут быть предоставлены как на уровне класса, так и на уровне метода.
Показанные примеры просты и не представляют реальных сценариев, но Spring Security предоставляет богатый набор аннотаций, и вы можете управлять сложной схемой авторизации, если решите их использовать.
Имя роли Префикс по умолчанию
В этом отдельном подразделе я хочу осветить еще одну тонкую деталь, которая сбивает с толку многих новых пользователей.
Структура Spring Security различает два термина:
Авторитет представляет собой индивидуальное разрешение.
Роль представляет собой группу разрешений.
Оба интерфейса могут быть представлены одним интерфейсом под названием GrantedAuthority и позже проверены с помощью языка выражений Spring в аннотациях Spring Security следующим образом:
Власть @PreAuthorize(«hasAuthority(‘EDIT_PRODUCT’)»)
Роль @PreAuthorize(«hasRole(‘PRODUCT_ADMIN’)»)
Чтобы сделать разницу между этими двумя терминами более явной, фреймворк Spring Security по умолчанию добавляет к имени роли префикс ROLE_. Поэтому вместо проверки наличия роли с именем PRODUCT_ADMIN будет проверяться наличие ROLE_PRODUCT_ADMIN.
Я лично нахожу такое поведение непонятным и предпочитаю отключать его в своих приложениях. Его можно отключить в конфигурации Spring Security следующим образом:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// Details omitted for brevity
@Bean
GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
}
}