Трансфер по ссылке:woohoo.call ICO people.com/spring-boot…
Оригинальный автор:Rajeev Kumar Singh
Еще статьи автора оригинала:www.callicoder.com/
1. Обзор архитектуры безопасности данной конструкции
- Внедрите регистрационный API, заполнив имя, имя пользователя, адрес электронной почты и пароль.
- Реализуйте API входа, указав имя пользователя (или адрес электронной почты) и пароль.
- После успешной проверки информации о пользователе сервер генерирует токен аутентификации JWT и возвращает его клиенту.
- Клиент получает доступ к определенным ресурсам, передавая токен JWT в заголовке запроса авторизации.
- Ограничьте доступ к определенным ресурсам, настроив безопасность Spring, например:
- Вход, регистрация и все статические ресурсы полностью открыты
- Другие специальные ресурсы доступны только для аутентифицированных пользователей.
- Настройте безопасность Spring, чтобы выдавать ошибку 401 Unauthorized при доступе к определенному ресурсу без действительной аутентификации токена JWT.
- Настройте механизмы авторизации на основе ролей для защиты ресурсов сервера.
2. Настройте Spring Security и JWT
код
SecurityConfig
package com.example.polls.config;
import com.example.polls.security.CustomUserDetailsService;
import com.example.polls.security.JwtAuthenticationEntryPoint;
import com.example.polls.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
securedEnabled = true,
jsr250Enabled = true,
prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomUserDetailsService customUserDetailsService;
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean(BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors()
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(unauthorizedHandler)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/",
"/favicon.ico",
"/**/*.png",
"/**/*.gif",
"/**/*.svg",
"/**/*.jpg",
"/**/*.html",
"/**/*.css",
"/**/*.js")
.permitAll()
.antMatchers("/api/auth/**")
.permitAll()
.antMatchers("/api/user/checkUsernameAvailability", "/api/user/checkEmailAvailability")
.permitAll()
.antMatchers(HttpMethod.GET, "/api/polls/**", "/api/users/**")
.permitAll()
.anyRequest()
.authenticated();
// Add our custom JWT security filter
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
Интерпретация кода
1. @EnableWebSecurity
- включить веб-безопасность
2. @EnableGlobalMethodSecurity
- Включить защиту на уровне метода на основе утверждений, 3 типа:
- secureEnabled, который включает утверждение @Secured, защищает метод контроллера/службы:
@Secured("ROLE_ADMIN") public User getAllUsers() {} @Secured({"ROLE_USER", "ROLE_ADMIN"}) public User getUser(Long id) {} @Secured("IS_AUTHENTICATED_ANONYMOUSLY") public boolean isUsernameAvailable() {}
- jsr250Enabled включает утверждения @RolesAllowed:
@RolesAllowed("ROLE_ADMIN") public Poll createPoll() {}
- prePostEnabled, включает более сложные выражения управления разрешениями, контролируемые @PreAuthorize и @PostAuthorize:
@PreAuthorize("isAnonymous()") public boolean isUsernameAvailable() {} @PreAuthorize("hasRole('USER')") public Poll createPoll() {}
3. WebSecurityConfigurerAdapter
- Укажите конфигурации безопасности по умолчанию, позволяющие другим классам наследовать и настроить конфигурации безопасности путем переопределения методов.
- SecurityConfig наследуется от этого класса и переопределяет некоторые методы для настройки конфигурации безопасности.
4. CustomUserDetailsService
- Реализовать интерфейс UserDetailsService.
- Аутентифицируйте пользовательские операции, переопределяя метод loadUserByUsername интерфейса.
5. JwtAuthenticationEntryPoint
- Реализуйте интерфейс AuthenticationEntryPoint.
- Ошибка 401 Unauthorized возвращается для пользователя, обращающегося к определенному ресурсу без аутентификации.
6. JwtAuthenticationFilter
- Получите токен JWT из заголовка авторизации всех запросов.
- проверить токен
- Загрузить информацию о пользователе, связанную с токеном
- Установите информацию о пользователе в контейнере безопасности Spring Security. Spring Security использует эту информацию о пользователе для проверки авторизации. Мы можем управлять нашей бизнес-логикой в контроллере, получая информацию о пользователе в контейнере безопасности.
7. AuthenticationManagerBuilder и AuthenticationManager
- AuthenticationManager — основной интерфейс для реализации аутентификации пользователей в Spring Security.
- AuthenticationManagerBuilder отвечает за создание AuthenticationManager
- Вы можете создать аутентификацию на основе памяти, аутентификацию LDAP, аутентификацию JDBC или добавить свою собственную аутентификацию с помощью AuthenticationManagerBuilder.
- В этом примере мы предоставляем customuserDetailssservice и Passwordencoder для создания аутентификации.
- Аутентифицируйте пользователей в API входа через настроенный AuthenticationManager.
8. HttpSecurity configurations
- Информация о конфигурации HttpSecurity включает функции настройки безопасности, такие как csrf, управление сеансами и политики защиты ресурсов, основанные на различных условиях.
- В этом примере мы разрабатываем статические ресурсы и некоторые общедоступные API, а другие ресурсы доступны только пользователям, прошедшим проверку подлинности.
- Мы добавили JWTAuthenticationEntryPoint и собственный JWTAuthenticationFilter в конфигурацию.
3. Создайте собственные классы безопасности Spring, фильтры и аннотации.
3.1 Настройка Spring Security AuthenticationEntryPoint
- Определяет JwtAuthenticationEntryPoint, реализующий интерфейс AuthenticationEntryPoint и его метод begin.
- Метод begin запускается, когда пользователь, не прошедший проверку подлинности, просматривает ресурс, требующий проверки подлинности.
- В этом примере мы просто возвращаем ошибку 401 с информацией об исключении.
JwtAuthenticationEntryPoint
package com.example.polls.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage());
}
}
3.2 Настройка пользовательских данных Spring Security
- UserPrincipal реализует интерфейс UserDetails.
- Пользовательский интерфейс UserDetailsService возвращает этот объект.
- Spring Security использует эту информацию для операций аутентификации и авторизации.
UserPrincipal
package com.example.polls.security;
import com.example.polls.model.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class UserPrincipal implements UserDetails {
private Long id;
private String name;
private String username;
@JsonIgnore
private String email;
@JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public UserPrincipal(Long id, String name, String username, String email, String password, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.name = name;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserPrincipal create(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
new SimpleGrantedAuthority(role.getName().name())
).collect(Collectors.toList());
return new UserPrincipal(
user.getId(),
user.getName(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities
);
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
@Override
public String getUsername() {
return username;
}
@Override
public String getPassword() {
return password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserPrincipal that = (UserPrincipal) o;
return Objects.equals(id, that.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
3.3 Настройка Spring Security UserDetailsService
- Загрузить пользовательские данные по имени пользователя
CustomUserDetailsService
package com.example.polls.security;
import com.example.polls.exception.ResourceNotFoundException;
import com.example.polls.model.User;
import com.example.polls.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.transaction.annotation.Transactional;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String usernameOrEmail)
throws UsernameNotFoundException {
User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
.orElseThrow(() ->
new UsernameNotFoundException("User not found with username or email : " + usernameOrEmail)
);
return UserPrincipal.create(user);
}
@Transactional
public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id).orElseThrow(
() -> new ResourceNotFoundException("User", "id", id)
);
return UserPrincipal.create(user);
}
}
3.4 Инструменты для создания и проверки JWT
- JwtTokenProvider создает JWT после успешного входа пользователя и отвечает за проверку JWT в заголовке запроса авторизации.
- Определите ключ JWT и срок действия в application.properties.
## jwt Properties
app.jwtSecret= JWTSuperSecretKey
app.jwtExpirationInMs = 604800000
JwtTokenProvider
package com.example.polls.security;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public Long getUserIdFromJWT(String token) {
Claims claims = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody();
return Long.parseLong(claims.getSubject());
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty.");
}
return false;
}
}
3.5 Настройка Spring Security AuthenticationFilter
- JWTAuthenticationFilter получает токен JWT из запроса, проверяет его, загружает информацию о пользователе, связанную с токеном, и передает ее в Spring Security.
JwtAuthenticationFilter
package com.example.polls.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
Long userId = tokenProvider.getUserIdFromJWT(jwt);
// 可以将用户名及角色信息都编码到 JWT claims中
// 然后通过解析JWT 的 claims 对象来 创建UserDetails信息
// 避免重复查询数据库
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
3.6 Пользовательское утверждение для получения текущего пользователя
- Spring Security предоставляет утверждение @AuthenticationPrincipal для получения текущего аутентифицированного пользователя в контроллере.
- Утверждение CurrentUser инкапсулирует AuthenticationPrincipal
CurrentUser
package com.example.polls.security;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import java.lang.annotation.*;
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {
}
- Создавая мета-утверждения, вы можете избежать чрезмерной привязки утверждений Spring Security в проекте и уменьшить зависимость от Spring Security.Когда вам нужно удалить Spring Security, вы можете просто изменить утверждение CurrentUser.
4. Войдите и зарегистрируйте API
4.1. Запрос полезной нагрузки
LoginRequest
import javax.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class LoginRequest {
@NotBlank
private String usernameOrEmail;
@NotBlank
private String password;
}
SignUpRequest
import javax.validation.constraints.*;
import lombok.Data;
@Data
public class SignUpRequest {
@NotBlank
@Size(min = 4, max = 40)
private String name;
@NotBlank
@Size(min = 3, max = 15)
private String username;
@NotBlank
@Size(max = 40)
@Email
private String email;
@NotBlank
@Size(min = 6, max = 20)
private String password;
// TODO: 手机号
}
4.2 Полезная нагрузка ответа
JwtAuthenticationResponse
import lombok.Data;
@Data
public class JwtAuthenticationResponse {
private String accessToken;
private String tokenType = "Bearer";
public JwtAuthenticationResponse(String accessToken) {
this.accessToken = accessToken;
}
}
ApiResponse
import lombok.Data;
@Data
public class ApiResponse {
private Boolean success;
private String message;
public ApiResponse(Boolean success, String message) {
this.success = success;
this.message = message;
}
}
4.3. Пользовательские бизнес-исключения
AppException
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class AppException extends RuntimeException {
public AppException(String message) {
super(message);
}
public AppException(String message, Throwable cause) {
super(message, cause);
}
}
BadRequestException
package com.example.polls.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
public BadRequestException(String message, Throwable cause) {
super(message, cause);
}
}
ResourceNotFoundException
package com.example.polls.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
private String resourceName;
private String fieldName;
private Object fieldValue;
public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) {
super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
this.resourceName = resourceName;
this.fieldName = fieldName;
this.fieldValue = fieldValue;
}
public String getResourceName() {
return resourceName;
}
public String getFieldName() {
return fieldName;
}
public Object getFieldValue() {
return fieldValue;
}
}
4.4 Контроллер аутентификации
AuthController
package com.example.polls.controller;
import com.example.polls.exception.AppException;
import com.example.polls.model.Role;
import com.example.polls.model.RoleName;
import com.example.polls.model.User;
import com.example.polls.payload.ApiResponse;
import com.example.polls.payload.JwtAuthenticationResponse;
import com.example.polls.payload.LoginRequest;
import com.example.polls.payload.SignUpRequest;
import com.example.polls.repository.RoleRepository;
import com.example.polls.repository.UserRepository;
import com.example.polls.security.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import javax.validation.Valid;
import java.net.URI;
import java.util.Collections;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
JwtTokenProvider tokenProvider;
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsernameOrEmail(),
loginRequest.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = tokenProvider.generateToken(authentication);
return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
}
@PostMapping("/signup")
public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
if(userRepository.existsByUsername(signUpRequest.getUsername())) {
return new ResponseEntity(new ApiResponse(false, "Username is already taken!"),
HttpStatus.BAD_REQUEST);
}
if(userRepository.existsByEmail(signUpRequest.getEmail())) {
return new ResponseEntity(new ApiResponse(false, "Email Address already in use!"),
HttpStatus.BAD_REQUEST);
}
// Creating user's account
User user = new User(signUpRequest.getName(), signUpRequest.getUsername(),
signUpRequest.getEmail(), signUpRequest.getPassword());
user.setPassword(passwordEncoder.encode(user.getPassword()));
Role userRole = roleRepository.findByName(RoleName.ROLE_USER)
.orElseThrow(() -> new AppException("User Role not set."));
user.setRoles(Collections.singleton(userRole));
User result = userRepository.save(user);
URI location = ServletUriComponentsBuilder
.fromCurrentContextPath().path("/users/{username}")
.buildAndExpand(result.getUsername()).toUri();
return ResponseEntity.created(location).body(new ApiResponse(true, "User registered successfully"));
}
}
5. Расширенное чтение
Официальная документация Spring Security
Аутентификация безопасности JWT и токен обновления