Spring Boot JWT登录返回401 Unauthorized及用户角色分配实现咨询
Hey there, let's tackle your two main issues step by step: fixing the 401 on login, and implementing role assignment for new users.
The root cause of your login failure is mostly related to how Spring Security interacts with your User entity, plus a small mismatch in JWT handling. Here's how to fix it:
1.1 Your User class doesn't implement UserDetails
Spring Security depends on the UserDetails interface to fetch critical authentication data (like password, enabled status, and user authorities). Right now your User entity doesn't implement this interface, so the authentication manager can't validate user credentials properly.
Fix: Update your User.java to implement UserDetails and override all required methods:
@Entity @Table(name="user") public class User implements UserDetails { // Add this interface // ... existing fields ... // Implement UserDetails methods @Override public Collection<? extends GrantedAuthority> getAuthorities() { // Convert roles to Spring Security's GrantedAuthority format return roles.stream() .map(role -> new SimpleGrantedAuthority(role.getName())) .collect(Collectors.toList()); } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isAccountNonExpired() { return true; // Adjust based on your business rules } @Override public boolean isAccountNonLocked() { return true; // Adjust based on your business rules } @Override public boolean isCredentialsNonExpired() { return true; // Adjust based on your business rules } @Override public boolean isEnabled() { return this.enabled; } // ... existing constructors, getters, setters ... }
1.2 Mismatch in JWT Subject and Username Retrieval
In JwtTokenProvider.generateToken(), you're setting the JWT subject to the user ID (setSubject(userId)), but in getUsernameFromJwt() you're fetching the subject as if it's a username. This will break token validation later, and contributes to authentication confusion.
Fix: Set the JWT subject to the username (or retrieve the username from the claims map):
// Option 1: Set subject to username in generateToken() public String generateToken(Authentication authentication) { User user = (User) authentication.getPrincipal(); Date now = new Date(System.currentTimeMillis()); Date expiryDate = new Date(now.getTime() + 300_000); Map<String, Object> claims = new HashMap<>(); claims.put("id", Long.toString(user.getId())); claims.put("username", user.getUsername()); // Set subject to username instead of user ID return Jwts.builder() .setSubject(user.getUsername()) .setClaims(claims) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, "SECRETSECRETSECRET") .compact(); } // Keep getUsernameFromJwt as is - it now correctly fetches the username from the subject public String getUsernameFromJwt(String token) { Claims claims = Jwts.parser() .setSigningKey("SECRETSECRETSECRET") .parseClaimsJws(token) .getBody(); return claims.getSubject(); }
1.3 Quick Sanity Checks
- Ensure your
LoginRequestclass has properly named fields (usernameandpassword) that match the JSON keys you're sending in Postman. - Confirm the password stored in the database is properly BCrypt-encoded (your
UserService.saveUseralready does this, which is correct).
Here's a straightforward way to add role logic to your registration flow, and include roles in JWT tokens for authorization:
2.1 Assign Default Role on Registration
First, make sure you have a default role (like ROLE_USER) in your role database table (seed it manually or use a data loader if needed). Then update your UserService.saveUser() to assign this role to new users:
Modify UserService.java:
public User saveUser(User user) { try { user.setPassword(passwordEncoder.encode(user.getPassword())); user.setUsername(user.getUsername()); user.setConfirmPassword(""); user.setEnabled(true); // Assign default role (e.g., ROLE_USER) Role defaultRole = roleService.findByRoleName("ROLE_USER"); if (defaultRole != null) { user.getRoles().add(defaultRole); } else { throw new RuntimeException("Default role ROLE_USER not found in database"); } return userRepository.save(user); } catch(Exception e) { throw new UsernameAlreadyExistsException("User with username " + user.getUsername() + " already exists!"); } }
2.2 Add Roles to JWT Claims
Update JwtTokenProvider.generateToken() to include the user's roles in the JWT claims, so they can be used for authorization later:
public String generateToken(Authentication authentication) { User user = (User) authentication.getPrincipal(); Date now = new Date(System.currentTimeMillis()); Date expiryDate = new Date(now.getTime() + 300_000); // Extract role names as strings List<String> roles = user.getRoles().stream() .map(Role::getName) .collect(Collectors.toList()); Map<String, Object> claims = new HashMap<>(); claims.put("id", Long.toString(user.getId())); claims.put("username", user.getUsername()); claims.put("roles", roles); // Add roles to JWT claims return Jwts.builder() .setSubject(user.getUsername()) .setClaims(claims) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS512, "SECRETSECRETSECRET") .compact(); }
2.3 Populate Authorities in JwtAuthenticationFilter
Right now your filter sets an empty list of authorities, which means even after login, users won't have their roles recognized for authorization. Fix this by using the user's actual authorities:
@Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { try { String jwt = getJWTFromRequest(httpServletRequest); if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { String username = tokenProvider.getUsernameFromJwt(jwt); User userDetails = (User) userDetailsService.loadUserByUsername(username); // Use the user's actual authorities instead of an empty list UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception ex) { logger.error("Could not set user authentication in security context", ex); } filterChain.doFilter(httpServletRequest, httpServletResponse); }
2.4 Enable Role-Based Access Control
Now you can use Spring Security annotations to restrict access to your endpoints:
@GetMapping("/all") public String welcomeAll() { return "Anyone can view this!"; } @GetMapping("/admin") @PreAuthorize("hasRole('ADMIN')") // Restrict to admin users only public String adminPing(){ return "Only Admins Can view This"; } @GetMapping("/user") @PreAuthorize("hasRole('USER')") // Restrict to regular users public String userPing(){ return "Any User Can view This"; }
After making these changes:
- Test the registration endpoint again - new users should have the
ROLE_USERassigned automatically. - Test the login endpoint - it should return a valid JWT token instead of 401.
- Test the role-protected endpoints by including the JWT token in the
Authorization: Bearer <token>header.
内容的提问来源于stack exchange,提问作者ServletException

