
本文旨在提供一份关于在spring boot应用中实现基于jwt(json web token)的角色授权的教程。我们将详细探讨核心安全配置、jwt请求过滤器的工作原理以及用户认证与令牌生成过程。此外,文章还将深入分析导致“401 unauthorized”错误(特别是在应用`hasauthority()`进行权限控制时)的常见原因,并提供相应的排查策略,重点关注权限数据模型与加载机制。
在Spring Boot中实现基于JWT的权限控制,主要涉及以下几个核心组件:安全配置 (WebSecurityConfigurerAdapter)、JWT请求过滤器 (OncePerRequestFilter) 以及用户认证与令牌生成逻辑。这些组件协同工作,确保请求的认证和授权过程顺畅且安全。
安全配置是定义应用安全策略的关键。在这里,我们配置了Spring Security如何处理HTTP请求,包括禁用CSRF、CORS,设置会话管理策略为无状态,并定义URL路径的访问权限。
import org.springframework.context.annotation.Configuration;
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.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtRequestFilter jwtRequestFilter;
private final InvalidUserAuthEntryPoint invaildUserAuthEntryPoint; // 自定义认证入口点
public WebSecurityConfig(JwtRequestFilter jwtRequestFilter, InvalidUserAuthEntryPoint invaildUserAuthEntryPoint) {
this.jwtRequestFilter = jwtRequestFilter;
this.invaildUserAuthEntryPoint = invaildUserAuthEntryPoint;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 禁用CSRF
.cors().disable() // 禁用CORS(根据实际需求配置)
.authorizeRequests()
// 针对不同角色定义访问权限
.antMatchers("/**", "/user/**", "/document/**", "/appointment/**", "/activity/**").hasAuthority(UserRole.ADMIN.name())
.antMatchers("/user/**", "/activity/**", "/appointment/", "/document/", "/appointment/**", "/document/**").hasAuthority(UserRole.SUPPORTEXECUTIVE.name())
.antMatchers("/user/**", "/activity/**", "/appointment/", "/document/", "/appointment/**").hasAuthority(UserRole.FIELDEXECUTIVE.name())
// 其他路径可以根据需要添加 permitAll() 或 authenticated()
.anyRequest().authenticated() // 任何其他未匹配的请求都需要认证
.and()
.exceptionHandling().authenticationEntryPoint(invaildUserAuthEntryPoint) // 配置未认证入口点
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置会话管理为无状态
.and()
// 在UsernamePasswordAuthenticationFilter之前添加自定义的JWT过滤器
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}配置说明:
JwtRequestFilter 负责拦截所有受保护的HTTP请求,从请求头中提取JWT,验证其有效性,并根据令牌中的信息设置Spring Security的认证上下文。
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
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;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
private final UserDetailsService userDetailsService;
private final JwtUtil util; // JWT工具类,用于生成、解析和验证JWT
public JwtRequestFilter(UserDetailsService userDetailsService, JwtUtil util) {
this.userDetailsService = userDetailsService;
this.util = util;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// 检查Authorization头是否包含Bearer令牌
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwtToken = authorizationHeader.substring(7); // 提取JWT
try {
username = util.extractUsername(jwtToken); // 从JWT中提取用户名
} catch (Exception e) {
// 处理JWT解析异常,例如令牌过期、无效签名等
logger.error("Error extracting username from JWT: " + e.getMessage());
}
}
// 如果成功提取到用户名且当前SecurityContext中没有认证信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); // 加载用户详情
// 验证令牌是否有效
if (util.validateToken(jwtToken, userDetails.getUsername())) {
// 构建认证对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将认证信息设置到SecurityContext中
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response); // 继续过滤器链
}
}过滤器说明:
用户通过提供用户名和密码进行登录时,控制器会负责认证这些凭据,并在认证成功后生成一个JWT返回给客户端。
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtUtil jwtUtil; // JWT工具类
public AuthController(AuthenticationManager authenticationManager, JwtUtil jwtUtil) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;
}
@PostMapping("/authenticate") // 假设登录接口为 /authenticate
public ResponseEntity<UserResponse> loginUser(@RequestBody UserRequest request) throws Exception {
try {
// 使用AuthenticationManager进行用户认证
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUserEmail(), request.getPassword()));
// 认证成功后,生成JWT
String token = jwtUtil.generateToken(request.getUserEmail());
System.out.println("Generated Token: " + token);
return ResponseEntity.ok(new UserResponse(token));
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
}
}
// 假设的请求和响应类
class UserRequest {
private String userEmail;
private String password;
// Getters and Setters
public String getUserEmail() { return userEmail; }
public void setUserEmail(String userEmail) { this.userEmail = userEmail; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
class UserResponse {
private String jwtToken;
public UserResponse(String jwtToken) { this.jwtToken = jwtToken; }
public String getJwtToken() { return jwtToken; }
public void setJwtToken(String jwtToken) { this.jwtToken = jwtToken; }
}认证说明:
当hasAuthority()检查失败并返回401 Unauthorized时,一个常见但容易被忽视的原因是权限数据本身的问题。即使JWT令牌有效,如果Spring Security无法从UserDetails中获取到正确的权限信息,授权也会失败。
在数据库中,你需要为用户存储其对应的角色或权限。这通常通过以下方式实现:
无论哪种方式,关键是当UserDetailsService加载用户时,能够获取到这些权限信息。
UserDetailsService 的 loadUserByUsername 方法是加载用户详情的核心。它不仅要加载用户名和密码,更重要的是要加载用户所拥有的权限,并将其封装到 UserDetails 对象的 getAuthorities() 方法中返回。
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository; // 假设有一个UserRepository来访问数据库
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库加载用户信息
com.example.demo.model.User appUser = userRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + username));
// 关键:将从数据库获取的角色/权限转换为GrantedAuthority对象
// 假设User实体中有一个getRole()方法返回角色字符串
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority(appUser.getRole()));
// 如果用户有多个角色,可能需要从关联表中获取并转换为List<GrantedAuthority>
/*
List<GrantedAuthority> authorities = appUser.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
*/
return new User(appUser.getEmail(), appUser.getPassword(), authorities);
}
}注意事项:
当您遇到 401 Unauthorized 错误,特别是当 permitAll() 工作正常但 hasAuthority() 失败时,请按照以下步骤进行排查:
这是最基本也是最常见的错误原因。
这是 hasAuthority() 失败的核心原因。
虽然通常 permitAll() 成功意味着令牌生成和基本解析没问题,但仍需考虑:
实现Spring Boot JWT权限控制需要对Spring Security的工作原理有清晰的理解。当遇到 401 Unauthorized 错误时,尤其是涉及 hasAuthority() 的情况,问题的根源往往在于:
最佳实践:
通过遵循上述指南和排查步骤,您将能够有效地在Spring Boot应用中实现健壮的JWT角色授权,并快速解决常见的权限相关问题。
以上就是Spring Boot JWT 角色授权实现与401错误排查指南的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号