所以我是 Spring Security 的新手,我尝试使用 REST 服务构建 Spring Boot 应用程序,并使用 JWT 实现 Spring Security。我大部分时间都遵循了安全教程,并且我运行了应用程序,但是当我尝试调用身份验证(/auth/**)端点时,我只收到 401 未经授权的错误。由于 spring 已更新到 v 3.0.0,现在
WebSecurityConfigurerAdapter
已弃用,我已相应地更改了配置。
这是我的配置文件
import com.example.myshroombackend.security.JwtAuthenticationEntryPoint;
import com.example.myshroombackend.security.JwtAuthenticationFilter;
import com.example.myshroombackend.service.AuthServiceImpl;
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.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.WebSecurityCustomizer;
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.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig{
@Autowired
AuthServiceImpl authService;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Bean JwtAuthenticationFilter jwtAuthenticationFilter(){
return new JwtAuthenticationFilter();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(authService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfiguration) throws Exception {
return authConfiguration.getAuthenticationManager();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/js/**", "/images/**");
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf()
.and()
.cors()
.disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests()
.requestMatchers("/auth/**").permitAll() //every /auth/login or /auth/register are permitted
.anyRequest().authenticated(); //all other requests must be authenticated ==>
// you first have to authenticate yourself to perform the other requests
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(jwtAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
我的自定义过滤器
import org.jetbrains.annotations.NotNull;
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.web.filter.OncePerRequestFilter;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
AuthServiceImpl authService;
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,@NotNull FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtTokenProvider.validateToken(jwt)) {
String username = jwtTokenProvider.getUserNameFromToken(jwt);
UserDetails userDetails = authService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails,
null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String jwt = jwtTokenProvider.getJwtFromCookies(request);
return jwt;
}
}
我的代币服务
import com.example.myshroombackend.exception.JwtAuthenticationException;
import com.example.myshroombackend.service.AuthServiceImpl;
import com.example.myshroombackend.service.UserDetailsImpl;
import io.jsonwebtoken.*;
import jakarta.servlet.http.Cookie;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.util.WebUtils;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Optional;
@Component
@Slf4j
public class JwtTokenService {
@Value("${application.secret")
private String jwtSecret;
@Value("${application.jwtCookieName}")
private String jwtCookie;
private AuthServiceImpl authService;
private static final Instant EXPIRATIONTIME = Instant.now().plus(20, ChronoUnit.HOURS);
public String generateToken(String userName) {
Instant now = Instant.now();
return Jwts.builder()
.setSubject(userName)
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(EXPIRATIONTIME))
.signWith(SignatureAlgorithm.HS512, jwtSecret.getBytes(StandardCharsets.UTF_8))
.compact();
}
public boolean validateToken(String token) {
if ((token != null) && (!"".equals(token))) {
Jws<Claims> claimsJws;
try{
claimsJws = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
if (claimsJws != null) {
final String userId = Optional
.ofNullable(claimsJws.getBody().get(new String("id")))
.map(Object::toString)//
.orElseThrow(
() -> new AuthenticationCredentialsNotFoundException("No username given in jwt"));
}
return true;
} catch (ExpiredJwtException e) {
throw new RuntimeException(e);
} catch (UnsupportedJwtException e) {
throw new RuntimeException(e);
} catch (MalformedJwtException e) {
throw new RuntimeException(e);
} catch (SignatureException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (AuthenticationCredentialsNotFoundException e) {
throw new RuntimeException(e);
}
}
return false;
}
public String generateToken(Authentication authentication) {
User user = (User) authentication.getPrincipal();
return generateToken(user.getUsername());
}
public String getJwtFromCookies(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, jwtCookie);
if (cookie != null) {
return cookie.getValue();
} else {
return null;
}
}
public String generateTokenFromUsername(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + 10000000))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public ResponseCookie generateJwtCookie(UserDetailsImpl userPrincipal) {
String jwt = generateTokenFromUsername(userPrincipal.getUsername());
ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/auth").maxAge(24 * 60 * 60).httpOnly(true).build();
return cookie;
}
public String getUserNameFromToken(final String token) throws JwtAuthenticationException {
String userName = null;
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(jwtSecret.getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token);
if (claimsJws != null) {
userName = claimsJws.getBody().getSubject();
}
} catch (final SignatureException | MalformedJwtException | UnsupportedJwtException ex) {
log.error("Unsupported jwt token {} with exception {}",
token,
ex.getMessage());
throw new JwtAuthenticationException(ex);
} catch (final ExpiredJwtException ex) {
log.error("Expired jwt token {}",
ex.getMessage());
throw new JwtAuthenticationException(ex);
} catch (final AuthenticationCredentialsNotFoundException ex) {
log.error("An error occured while trying to create authentication based on jwt token, missing credentials {}",
ex.getMessage());
throw new JwtAuthenticationException(ex);
} catch (final Exception ex) {
log.error("Unexpected exception occured while parsing jwt {} exception: {}",
token,
ex.getMessage());
throw new JwtAuthenticationException(ex);
}
return userName;
}
}
还有我的 RestController
import com.example.myshroombackend.dto.LoginRequestDto;
import com.example.myshroombackend.entity.Rank;
import com.example.myshroombackend.entity.UserEntity;
import com.example.myshroombackend.repository.UserRepository;
import com.example.myshroombackend.security.JwtTokenService;
import com.example.myshroombackend.service.UserDetailsImpl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
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.*;
import java.util.Optional;
@RestController
@RequestMapping("/auth")
@CrossOrigin
public class AuthController {
private final UserRepository userRepository;
private AuthenticationManager authenticationManager;
private JwtTokenService provider;
private PasswordEncoder encoder;
public AuthController(UserRepository userRepository, AuthenticationManager authenticationManager, JwtTokenService provider) {
this.userRepository = userRepository;
this.authenticationManager = authenticationManager;
this.provider = provider;
}
@PostMapping("/login")
public ResponseEntity<LoginRequestDto> loginUser(@RequestBody LoginRequestDto dto) {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(dto.getUserName(), dto.getPassword()));
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
SecurityContextHolder.getContext().setAuthentication(authentication);
ResponseCookie jwtCookie = provider.generateJwtCookie(userDetails);
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
.body(new LoginRequestDto(
userDetails.getUsername()));
}
@PostMapping("/register")
public ResponseEntity<?> registerUser( @RequestBody LoginRequestDto dto) {
Optional<UserEntity> optionalUserEntity = userRepository.findByUserName(dto.getUserName());
if (optionalUserEntity.isPresent()) {
return ResponseEntity.badRequest().body("User already in database");
}
// Create new user's account
UserEntity userEntity = new UserEntity();
userEntity.setUserName(dto.getUserName());
userEntity.setPassword(encoder.encode(dto.getUserName()));
userEntity.setRank(Rank.BEGINNER);
userRepository.save(userEntity);
return ResponseEntity.ok("User registered successfully!");
}
}
我使用
requestMatchers()
代替此安全版本中不再存在的 antMatchers()
,并使用 authorizeHttpRequests()
代替 authorizeRequests()
。不知道问题出在哪里还是别的地方
当使用
authorizeHttpRequests
代替 authorizeRequests
时,则使用 AuthorizationFilter
代替 FilterSecurityInterceptor
。
据我了解,在 Spring Security 6 之前,
AuthorizationFilter
和 FilterSecurityInterceptor
仅对第一个请求执行授权检查,但自 Spring Security 6 以来,它似乎适用于每个请求(即所有调度程序类型)至少对于 AuthorizationFilter
(FilterSecurityInterceptor
现已弃用)。
来自文档:
Spring Security 5.8 及更早版本仅执行一次授权 要求。这意味着像 FORWARD 和 INCLUDE 这样的调度程序类型 默认情况下,在 REQUEST 之后运行的程序不受保护。推荐的是 Spring Security 可以保护所有调度类型。因此,在 6.0 中, Spring Security 更改了此默认值。
所以我的猜测是,当您的请求未发送时,它实际上可以工作,但一旦您的请求转发到另一个资源,它就会失败。
也许尝试通过使用
shouldFilterAllDispatcherTypes
或 dispatcherTypeMatchers
来明确表明您不想将授权规则应用于所有调度程序类型。
例如,授予对调度程序类型
ASYNC
或 FORWARD
的请求的所有访问权限:
http
...
.authorizeHttpRequests(auth -> auth
.dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.FORWARD).permitAll()
...
);
当然,如果需要,您也可以自定义它以需要特定角色(
hasRole
)而不是permitAll
。
使用 Spring Boot,您还应该能够通过
spring.security.filter.dispatcher-types
属性来配置它。
请参阅文档了解更多信息。