Spring Security + JWT + Spring Boot 整合教程(增强版)

618 阅读4分钟

Spring Security + JWT + Spring Boot 整合教程(增强版)

权限控制增强

Spring Security 提供了强大的基于角色和权限的访问控制机制。我们可以通过以下方式增强权限控制:

细化权限模型

创建权限枚举类:

package com.example.securityjwt.model;

public enum Permission {
    USER_READ("user:read"),
    USER_WRITE("user:write"),
    ADMIN_READ("admin:read"),
    ADMIN_WRITE("admin:write");
    
    private final String permission;

    Permission(String permission) {
        this.permission = permission;
    }

    public String getPermission() {
        return permission;
    }
}

修改角色枚举类:

package com.example.securityjwt.model;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static com.example.securityjwt.model.Permission.*;

@RequiredArgsConstructor
public enum Role {
    USER(Collections.emptySet()),
    ADMIN(
            Set.of(
                    ADMIN_READ,
                    ADMIN_WRITE,
                    USER_READ,
                    USER_WRITE
            )
    );

    @Getter
    private final Set<Permission> permissions;

    public List<SimpleGrantedAuthority> getAuthorities() {
        var authorities = getPermissions()
                .stream()
                .map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
                .collect(Collectors.toList());
        authorities.add(new SimpleGrantedAuthority("ROLE_" + this.name()));
        return authorities;
    }
}

修改用户实体类:

package com.example.securityjwt.entity;

import com.example.securityjwt.model.Role;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
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;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "_user")
public class User implements UserDetails {

    @Id
    @GeneratedValue
    private Integer id;
    private String firstname;
    private String lastname;
    private String username;
    private String password;
    
    @Enumerated(EnumType.STRING)
    private Role role;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return role.getAuthorities();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
增强安全配置

修改 SecurityConfig 以配置更精细的权限控制:

package com.example.securityjwt.config;

import com.example.securityjwt.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import static com.example.securityjwt.model.Permission.*;
import static com.example.securityjwt.model.Role.*;
import static org.springframework.http.HttpMethod.*;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf()
                .disable()
            .authorizeRequests()
                .antMatchers("/api/auth/**").permitAll()
                
                // 用户管理API权限控制
                .antMatchers(GET, "/api/users/**").hasAnyAuthority(USER_READ.getPermission(), ADMIN_READ.getPermission())
                .antMatchers(POST, "/api/users/**").hasAnyAuthority(USER_WRITE.getPermission(), ADMIN_WRITE.getPermission())
                .antMatchers(PUT, "/api/users/**").hasAnyAuthority(USER_WRITE.getPermission(), ADMIN_WRITE.getPermission())
                .antMatchers(DELETE, "/api/users/**").hasAuthority(ADMIN_WRITE.getPermission())
                
                // 管理员专用API
                .antMatchers("/api/admin/**").hasRole(ADMIN.name())
                
                .anyRequest().authenticated()
                .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
}
在控制器中使用注解进行权限控制
package com.example.securityjwt.controller;

import com.example.securityjwt.entity.User;
import com.example.securityjwt.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping
    @PreAuthorize("hasAuthority('user:read') or hasAuthority('admin:read')")
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }

    @PostMapping
    @PreAuthorize("hasAuthority('user:write') or hasAuthority('admin:write')")
    public User createUser(@RequestBody User user) {
        return userService.createUser(user);
    }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasAuthority('admin:write')")
    public void deleteUser(@PathVariable Integer id) {
        userService.deleteUser(id);
    }
}

刷新令牌机制

为了提高安全性和用户体验,我们添加刷新令牌机制:

创建刷新令牌实体类

java

package com.example.securityjwt.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.Instant;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "refresh_token")
public class RefreshToken {

    @Id
    @GeneratedValue
    private Long id;
    
    @OneToOne
    @JoinColumn(name = "user_id", referencedColumnName = "id")
    private User user;
    
    @Column(nullable = false, unique = true)
    private String token;
    
    @Column(nullable = false)
    private Instant expiryDate;
}
创建刷新令牌服务
package com.example.securityjwt.service;

import com.example.securityjwt.entity.RefreshToken;
import com.example.securityjwt.entity.User;
import com.example.securityjwt.repository.RefreshTokenRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.Optional;
import java.util.UUID;

@Service
public class RefreshTokenService {

    @Value("${jwt.refresh.expiration}")
    private Long refreshTokenDurationMs;

    @Autowired
    private RefreshTokenRepository refreshTokenRepository;

    @Autowired
    private UserService userService;

    public Optional<RefreshToken> findByToken(String token) {
        return refreshTokenRepository.findByToken(token);
    }

    public RefreshToken createRefreshToken(String username) {
        User user = userService.findByUsername(username)
                .orElseThrow(() -> new RuntimeException("User not found"));
        
        RefreshToken refreshToken = RefreshToken.builder()
                .user(user)
                .token(UUID.randomUUID().toString())
                .expiryDate(Instant.now().plusMillis(refreshTokenDurationMs))
                .build();
        
        return refreshTokenRepository.save(refreshToken);
    }

    public RefreshToken verifyExpiration(RefreshToken token) {
        if (token.getExpiryDate().isBefore(Instant.now())) {
            refreshTokenRepository.delete(token);
            throw new RuntimeException("Refresh token was expired. Please make a new signin request");
        }
        
        return token;
    }

    @Transactional
    public int deleteByUserId(Integer userId) {
        return refreshTokenRepository.deleteByUser(userService.findById(userId)
                .orElseThrow(() -> new RuntimeException("User not found")));
    }
}
修改认证服务以支持刷新令牌
package com.example.securityjwt.service;

import com.example.securityjwt.entity.*;
import com.example.securityjwt.repository.UserRepository;
import com.example.securityjwt.utils.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthenticationService {

    private final UserRepository repository;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;
    private final AuthenticationManager authenticationManager;
    private final RefreshTokenService refreshTokenService;

    public AuthenticationResponse register(RegisterRequest request) {
        var user = User.builder()
                .firstname(request.getFirstname())
                .lastname(request.getLastname())
                .username(request.getUsername())
                .password(passwordEncoder.encode(request.getPassword()))
                .role(request.getRole())
                .build();
        repository.save(user);
        
        var jwtToken = jwtUtil.generateToken(user);
        var refreshToken = refreshTokenService.createRefreshToken(user.getUsername());
        
        return AuthenticationResponse.builder()
                .accessToken(jwtToken)
                .refreshToken(refreshToken.getToken())
                .build();
    }

    public AuthenticationResponse authenticate(AuthenticationRequest request) {
        authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        request.getUsername(),
                        request.getPassword()
                )
        );
        
        var user = repository.findByUsername(request.getUsername())
                .orElseThrow();
        
        var jwtToken = jwtUtil.generateToken(user);
        var refreshToken = refreshTokenService.createRefreshToken(user.getUsername());
        
        return AuthenticationResponse.builder()
                .accessToken(jwtToken)
                .refreshToken(refreshToken.getToken())
                .build();
    }

    public AuthenticationResponse refreshToken(RefreshTokenRequest request) {
        return refreshTokenService.findByToken(request.getRefreshToken())
                .map(refreshTokenService::verifyExpiration)
                .map(RefreshToken::getUser)
                .map(user -> {
                    String accessToken = jwtUtil.generateToken(user);
                    return AuthenticationResponse.builder()
                            .accessToken(accessToken)
                            .refreshToken(request.getRefreshToken())
                            .build();
                })
                .orElseThrow(() -> new RuntimeException("Refresh token not found"));
    }
}
添加刷新令牌控制器
package com.example.securityjwt.controller;

import com.example.securityjwt.entity.AuthenticationResponse;
import com.example.securityjwt.entity.RefreshTokenRequest;
import com.example.securityjwt.service.AuthenticationService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {

    private final AuthenticationService authenticationService;

    // 已有方法保持不变...

    @PostMapping("/refresh-token")
    public ResponseEntity<AuthenticationResponse> refreshToken(
            @RequestBody RefreshTokenRequest request
    ) {
        return ResponseEntity.ok(authenticationService.refreshToken(request));
    }
}

增强 JWT 工具类

添加从令牌中提取权限的方法:

package com.example.securityjwt.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@Component
public class JwtUtil {

    private static final String SECRET_KEY = "YOUR_SECRET_KEY_HERE_THIS_SHOULD_BE_LONG_AND_SECURE";

    // 已有方法保持不变...

    public List<GrantedAuthority> extractAuthorities(String token) {
        final Claims claims = extractAllClaims(token);
        List<Map<String, String>> authorityMaps = claims.get("authorities", List.class);
        
        return authorityMaps.stream()
                .map(authorityMap -> 
                        new org.springframework.security.core.authority.SimpleGrantedAuthority(
                                authorityMap.get("authority")
                        )
                )
                .collect(Collectors.toList());
    }

    public String generateToken(
            Map<String, Object> extraClaims,
            UserDetails userDetails
    ) {
        // 添加权限信息到claims
        extraClaims.put("authorities", userDetails.getAuthorities().stream()
                .map(authority -> Map.of("authority", authority.getAuthority()))
                .collect(Collectors.toList()));
        
        return Jwts
                .builder()
                .setClaims(extraClaims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24)) // 24小时
                .signWith(getSignInKey(), SignatureAlgorithm.HS256)
                .compact();
    }
}

使用方法更新

  1. 刷新令牌请求

    POST /api/auth/refresh-token
    {
        "refreshToken": "your_refresh_token_here"
    }
    
  2. 基于权限的访问控制

    • 普通用户可以访问 /api/users(需要 user:read 权限)
    • 管理员可以访问所有 /api/users 接口(需要 admin:write 等权限)
    • 只有管理员可以访问 /api/admin 下的接口

总结

通过这些增强,我们的安全架构现在具有:

  1. 更精细的权限控制

    • 基于角色和权限的双重访问控制
    • 使用注解和配置相结合的方式定义访问规则
    • 清晰的权限模型设计
  2. 刷新令牌机制

    • 分离访问令牌和刷新令牌的职责
    • 延长用户会话时间,提高用户体验
    • 增强安全性,缩短访问令牌的有效期
  3. 优化的 JWT 实现

    • 在令牌中包含详细的权限信息

    • 更安全的令牌生成和验证机制

这些改进使系统更加安全、灵活,同时保持良好的用户体验。根据实际需求,你还可以进一步扩展这些功能,如添加多因素认证、IP 绑定等安全措施。