OAuth2.0 资源服务器

1. 资源权限数据

资源权限数据是存储在mysql数据库中的。
可以把mysql中的资源权限数据在系统启动时加载到redis中,根据系统情形来判断。

1. 频繁变动且需要高性能

如果权限数据会频繁变动或查询权限数据开销较大,可以考虑在应用启动时或动态变更时将资源权限数据加载到 Redis 中,以提高查询效率。Redis 可以作为权限数据的缓存层,用于加速频繁的权限检查。常见的做法是:
应用启动时加载:将权限数据从数据库加载到 Redis 中。
权限变更时更新缓存:在权限变动时同步更新 Redis 中的缓存。

这样在实际权限检查时,只需要从 Redis 获取数据,而不是每次从数据库中查询。

2. 权限数据较少且变动不频繁
如果权限数据较少,并且不经常变化,可以直接从数据库或代码中进行权限管理,不需要 Redis 缓存。Spring Security 的权限校验是轻量级的,直接从内存中获取权限信息通常也足够快。

Redis 中的数据格式
如果将权限数据加载到 Redis,通常使用的格式如下:

  • Key:资源的唯一标识符(可以是资源路径或资源 ID)
  • Value:资源对应的权限信息(例如角色或访问控制列表 ACL)
Key: "/admin/**"
Value: [ "ROLE_ADMIN", "ROLE_SUPERADMIN" ]

在实际的权限校验时,Spring Security 可以从 Redis 中快速检索权限数据,提升访问控制的效率。

Redis 加载的方案示例
假设权限数据是从数据库中加载并存入 Redis,你可以在应用启动时将这些权限数据加载到 Redis 中:

@Component
public class PermissionLoader {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private PermissionRepository permissionRepository;

    // 在应用启动时加载权限数据
    @PostConstruct
    public void loadPermissionsIntoRedis() {
        List<Permission> permissions = permissionRepository.findAll();

        for (Permission permission : permissions) {
            String resourceKey = permission.getResourceUrl() + ":" + permission.getMethod();
            redisTemplate.opsForHash().put(resourceKey, "roles", permission.getRoles());
        }
    }
}

这样,权限数据会被加载到 Redis 中,并可以在权限校验时从 Redis 中直接获取,减少对数据库的访问。

2. RBAC规范表结构

在基于 RBAC(Role-Based Access Control,基于角色的访问控制)权限控制模型下,结合 Spring Security 进行权限管理的数据库表设计,一般需要定义几张关键的表,用于表示用户、角色、权限和它们之间的关系。常见的设计有以下几张表:

1. 用户表(User Table)

存储用户的基本信息。

CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,   -- 用户名
    password VARCHAR(100) NOT NULL,         -- 密码(加密存储)
    enabled BOOLEAN NOT NULL DEFAULT TRUE,  -- 是否启用
    account_non_expired BOOLEAN NOT NULL DEFAULT TRUE, -- 账号是否未过期
    credentials_non_expired BOOLEAN NOT NULL DEFAULT TRUE, -- 凭据是否未过期
    account_non_locked BOOLEAN NOT NULL DEFAULT TRUE, -- 账号是否未锁定
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 更新时间
);

2. 角色表(Role Table)

存储系统中的角色。

CREATE TABLE roles (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    role_name VARCHAR(50) NOT NULL UNIQUE,  -- 角色名称(如:ROLE_ADMIN, ROLE_USER)
    description VARCHAR(100)                -- 角色描述
);

3. 权限表(Permission Table)

存储系统中可以分配的具体权限。

CREATE TABLE permissions (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    permission_name VARCHAR(50) NOT NULL UNIQUE,  -- 权限名称(如:READ_PRIVILEGE, WRITE_PRIVILEGE)
    description VARCHAR(100)                      -- 权限描述
);

4. 用户-角色关联表(User-Role Table)

定义用户和角色的多对多关系。

CREATE TABLE user_roles (
    user_id BIGINT NOT NULL,
    role_id BIGINT NOT NULL,
    PRIMARY KEY (user_id, role_id),
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);

5. 角色-权限关联表(Role-Permission Table)

定义角色和权限的多对多关系。

CREATE TABLE role_permissions (
    role_id BIGINT NOT NULL,
    permission_id BIGINT NOT NULL,
    PRIMARY KEY (role_id, permission_id),
    FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
    FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);

6. 请求路径-权限关联表(Request-Permission Table)

用于将请求路径与权限绑定,Spring Security 可以根据用户的权限决定是否允许访问。

CREATE TABLE request_permissions (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    url VARCHAR(255) NOT NULL,               -- 请求的URL路径
    permission_id BIGINT NOT NULL,           -- 对应的权限ID
    method VARCHAR(10),                      -- 请求方法(GET, POST等),可选
    FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);

设计说明
用户与角色 是多对多关系,一个用户可以拥有多个角色,一个角色也可以分配给多个用户。
角色与权限 是多对多关系,一个角色可以拥有多个权限,一个权限可以分配给多个角色。
权限与请求路径 可以用于将具体的权限绑定到特定的 URL 路径上,从而通过权限控制用户对不同路径的访问。

Spring Security 中的集成:
用户登录 时,通过 UserDetailsService 从数据库中加载用户的角色信息。
权限校验 时,通过 GrantedAuthority 检查用户的权限是否与当前请求所需的权限匹配。
这种表结构能够很好地支持 RBAC 模型的灵活性和扩展性,在用户权限管理中有很广泛的应用。

3. 登录认证过滤器

1. 自定义登录认证过滤器

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authenticationManager;

    public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        // 自定义登录路径
        this.setFilterProcessesUrl("/custom-login");  
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        // 从请求中获取用户名和密码
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        // 构造认证对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);

        // 使用 AuthenticationManager 进行认证
        return authenticationManager.authenticate(authenticationToken);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                            FilterChain chain, Authentication authResult) throws IOException, ServletException {
        // 自定义认证成功逻辑,比如生成 JWT 或设置响应头
        // 可以在这里添加 JWT token 返回
        response.getWriter().write("Login successful!");
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException failed) throws IOException, ServletException {
        // 自定义认证失败逻辑
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write("Login failed: " + failed.getMessage());
    }
}

JwtAuthenticationLoginFilter 过滤器

@Configuration
public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    /**
     * userDetailService
     */
    @Qualifier("jwtTokenUserDetailsService")
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 登录成功处理器
     */
    @Autowired
    private LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler;

    /**
     * 登录失败处理器
     */
    @Autowired
    private LoginAuthenticationFailureHandler loginAuthenticationFailureHandler;

    /**
     * 加密
     */
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 将登录接口的过滤器配置到过滤器链中
     * 1. 配置登录成功、失败处理器
     * 2. 配置自定义的userDetailService(从数据库中获取用户数据)
     * 3. 将自定义的过滤器配置到spring security的过滤器链中,配置在UsernamePasswordAuthenticationFilter之前
     * @param http
     */
    @Override
    public void configure(HttpSecurity http) {
        JwtAuthenticationLoginFilter filter = new JwtAuthenticationLoginFilter();
        filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        //认证成功处理器
        filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
        //认证失败处理器
        filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
        //直接使用DaoAuthenticationProvider
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        //设置userDetailService
        provider.setUserDetailsService(userDetailsService);
        //设置加密算法
        provider.setPasswordEncoder(passwordEncoder);
        http.authenticationProvider(provider);
        //将这个过滤器添加到UsernamePasswordAuthenticationFilter之前执行
        http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
    }
}

所有的逻辑都在public void configure(HttpSecurity http)这个方法中,如下:

  • 设置认证成功处理器loginAuthenticationSuccessHandler
  • 设置认证失败处理器loginAuthenticationFailureHandler
  • 设置userDetailService的实现类JwtTokenUserDetailsService
  • 设置加密算法passwordEncoder
  • 将JwtAuthenticationLoginFilter这个过滤器加入到过滤器链中,直接加入到UsernamePasswordAuthenticationFilter这个过滤器之前。

将自定义过滤器加入过滤器链

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtTokenUtil jwtTokenUtil;
    private final UserDetailsService userDetailsService;

    public SecurityConfig(JwtTokenUtil jwtTokenUtil, UserDetailsService userDetailsService) {
        this.jwtTokenUtil = jwtTokenUtil;
        this.userDetailsService = userDetailsService;
    }

    // 配置 SecurityFilterChain
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // 创建 JwtAuthenticationLoginFilter 并指定 AuthenticationManager
        JwtAuthenticationLoginFilter jwtAuthenticationLoginFilter = new JwtAuthenticationLoginFilter(authenticationManager(http.getSharedObject(AuthenticationConfiguration.class)), jwtTokenUtil);

        http
            .csrf().disable()
            .authorizeHttpRequests(authz -> authz
                .antMatchers("/login", "/register").permitAll()  // 公开的接口
                .anyRequest().authenticated()  // 其他接口需要认证
            )
            .sessionManagement(sess -> sess
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // 无状态的会话管理
            );

        // 将 JwtAuthenticationLoginFilter 添加到 UsernamePasswordAuthenticationFilter 之前
        http.addFilterBefore(jwtAuthenticationLoginFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

4. 认证成功处理器

1. 实现步骤

  • 创建 JWT Token 生成和验证的工具类:负责生成 accessToken 和 refreshToken。
  • 自定义 AuthenticationSuccessHandler:在认证成功后返回这两个 Token。
  • Spring Security 配置:将自定义的 AuthenticationSuccessHandler 集成到认证流程中。

2. 创建 JWT 工具类

public class JwtTokenProvider {

    private final String jwtSecret = "secretKey";  // 请替换为更安全的密钥
    private final long accessTokenExpiration = 15 * 60 * 1000; // 15分钟有效期
    private final long refreshTokenExpiration = 7 * 24 * 60 * 60 * 1000; // 7天有效期

    // 生成 accessToken
    public String generateAccessToken(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return generateToken(userDetails.getUsername(), accessTokenExpiration);
    }

    // 生成 refreshToken
    public String generateRefreshToken(Authentication authentication) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return generateToken(userDetails.getUsername(), refreshTokenExpiration);
    }

    // 生成 JWT token 的通用方法
    private String generateToken(String username, long expirationTime) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expirationTime);

        return Jwts.builder()
            .setSubject(username)
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(SignatureAlgorithm.HS512, jwtSecret)
            .compact();
    }

    // 解析 token
    public String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
            .setSigningKey(jwtSecret)
            .parseClaimsJws(token)
            .getBody();

        return claims.getSubject();
    }

    // 验证 token 是否有效
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

3. 自定义 AuthenticationSuccessHandler

public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthenticationSuccessHandler(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        // 生成 accessToken 和 refreshToken
        String accessToken = jwtTokenProvider.generateAccessToken(authentication);
        String refreshToken = jwtTokenProvider.generateRefreshToken(authentication);

        // 构建返回的 JSON 数据
        String jsonResponse = String.format("{\"accessToken\": \"%s\", \"refreshToken\": \"%s\"}", accessToken, refreshToken);

        // 设置响应头和响应体
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(jsonResponse);
    }
}

4. Spring Security 配置
将自定义的 JwtAuthenticationSuccessHandler 集成到 Spring Security 的认证流程中。

@Configuration
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;

    public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Bean
    public JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler() {
        return new JwtAuthenticationSuccessHandler(jwtTokenProvider);
    }
}

5. 认证失败处理器

在 Spring Security 中,AuthenticationFailureHandler 是用于处理认证失败的处理器接口。当用户登录失败时,比如用户名或密码错误,系统会调用 AuthenticationFailureHandler 处理相关逻辑,通常包括返回错误信息、重定向、记录日志等。

1. 自定义 AuthenticationFailureHandler
在前后端分离的应用中,通常需要在登录失败时返回 JSON 格式的错误信息,而不是页面跳转。通过自定义 AuthenticationFailureHandler,可以轻松实现这一需求。

public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        // 设置 HTTP 状态码为 401
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        // 返回详细的 JSON 错误信息
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"status\": 401, \"error\": \"Unauthorized\", \"message\": \"" + exception.getMessage() + "\"}");
    }
}

2. 在 Spring Security 配置

@Configuration
public class SecurityConfig {
    @Bean
	public RestAuthenticationFailureHandler restAuthenticationFailureHandler() {
	    return new RestAuthenticationFailureHandler();
	}
}

6. AuthenticationEntryPoint配置

在 Spring Security 中,AuthenticationEntryPoint 是用于处理未认证用户访问受保护资源时的响应处理器。当未经过身份验证的用户请求需要认证的资源时,Spring Security 会调用 AuthenticationEntryPoint 来引导用户进行身份验证,通常是返回 401 Unauthorized 错误或重定向到登录页面。

1. 自定义 AuthenticationEntryPoint

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        String ajaxHeader = request.getHeader("X-Requested-With");

        // 判断是否为 AJAX 请求
        if ("XMLHttpRequest".equals(ajaxHeader)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"AJAX request not authenticated\"}");
        } else {
            // 如果不是 AJAX 请求,可以重定向到登录页面
            response.sendRedirect("/login");
        }
    }
}

2. 配置 AuthenticationEntryPoint

在 Spring Security 配置类中,使用自定义的 AuthenticationEntryPoint 来处理未认证用户的访问请求。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public CustomAuthenticationEntryPoint customAuthenticationEntryPoint() {
        return new CustomAuthenticationEntryPoint();
    }
}

7. AccessDeniedHandler配置

在 Spring Security 中,AccessDeniedHandler 是用来处理已认证用户尝试访问他们没有权限访问的资源时的情况。当一个用户已经通过认证,但没有足够的权限访问某个资源时,Spring Security 会调用 AccessDeniedHandler 来处理该请求。通常情况下,会返回一个 HTTP 403 Forbidden 错误。

1. 自定义 AccessDeniedHandler

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        String ajaxHeader = request.getHeader("X-Requested-With");

        // 判断是否为 AJAX 请求
        if ("XMLHttpRequest".equals(ajaxHeader)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"error\": \"Forbidden\", \"message\": \"AJAX request access denied\"}");
        } else {
            // 如果不是 AJAX 请求,可以重定向到 403 错误页面
            response.sendRedirect("/403");
        }
    }
}

2. 配置 AccessDeniedHandler

@Configuration
public class SecurityConfig {
    @Bean
    public CustomAccessDeniedHandler customAccessDeniedHandler() {
        return new CustomAccessDeniedHandler();
    }
}

8. UserDetailsService配置

在 Spring Security 中,UserDetailsService 接口是用来加载用户的详细信息的,这些详细信息将用于认证和授权过程。实现自定义的 UserDetailsService 可以从数据库或其他数据源中获取用户信息,然后由 Spring Security 进行认证和权限管理。

1. 什么是 UserDetailsService

UserDetailsService 接口有一个核心方法 loadUserByUsername(String username),该方法根据用户名加载用户的详细信息,包括用户名、密码、权限等信息。实现这个接口的目的是为了自定义用户信息的加载逻辑,通常从数据库中查找用户信息。

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

2. 自定义 UserDetailsService

实现 UserDetailsService 接口时,通常会根据用户名从数据库中加载用户信息。UserDetails 是 Spring Security 的一个核心接口,它封装了用户的认证信息。需要自定义 UserDetailsService 并返回一个实现了 UserDetails 接口的用户对象。

示例:从数据库加载用户信息
假设有一个用户实体类 User 和一个用户仓库 UserRepository,可以这样实现自定义的 UserDetailsService:

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名从数据库查找用户
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
        
        // 返回实现了 UserDetails 的自定义用户对象
        return new org.springframework.security.core.userdetails.User(user.getUsername(), 
                                                                      user.getPassword(), 
                                                                      user.getAuthorities());
    }
}

3. UserDetails 接口

UserDetails 是 Spring Security 的核心接口,定义了认证过程中所需的用户信息,包括:

  • 用户名 (getUsername())
  • 密码 (getPassword())
  • 用户的权限 (getAuthorities())
  • 账户是否过期 (isAccountNonExpired())
  • 账户是否锁定 (isAccountNonLocked())
  • 凭证是否过期 (isCredentialsNonExpired())
  • 账户是否启用 (isEnabled())

可以直接使用 Spring 提供的 User 类,它已经实现了 UserDetails 接口,也可以自定义一个类来实现 UserDetails。

public class CustomUserDetails implements UserDetails {

    private String username;
    private String password;
    private boolean enabled;
    private Collection<? extends GrantedAuthority> authorities;

    public CustomUserDetails(String username, String password, boolean enabled, 
                             Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.authorities = authorities;
    }

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

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

4. 配置 UserDetailsService 到 Spring Security

在 Spring Security 配置中,将自定义的 UserDetailsService 注入到 AuthenticationManager 中,以便在认证过程中使用自定义的用户加载逻辑。

@Configuration
public class SecurityConfig {

    private final CustomUserDetailsService customUserDetailsService;

    public SecurityConfig(CustomUserDetailsService customUserDetailsService) {
        this.customUserDetailsService = customUserDetailsService;
    }

    // 注入自定义的 AuthenticationManager,并使用自定义的 UserDetailsService
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

}

当我们在 Spring Security 中配置 AuthenticationManager 时,它会自动使用我们自定义的 UserDetailsService,这是因为 Spring Security 自动从上下文中找到 UserDetailsService 并将其应用于默认的认证机制。

通过 AuthenticationConfiguration 提供的 authenticationManager() 方法,我们可以自动获取到一个已经配置好的 AuthenticationManager,它会包含我们自定义的 UserDetailsService。
5. 使用加密密码

为了确保密码安全,通常会对用户密码进行加密。Spring Security 提供了 PasswordEncoder 接口来处理密码加密和匹配。BCryptPasswordEncoder 是一个常见的实现,用于安全地加密和验证密码。

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService)
        .passwordEncoder(passwordEncoder());  // 设置密码加密器
}

在保存用户密码到数据库时,应使用 BCryptPasswordEncoder 对密码进行加密:

String rawPassword = "userPassword";
String encodedPassword = passwordEncoder().encode(rawPassword);
// 存储 encodedPassword 到数据库

在登录时,Spring Security 会自动使用 PasswordEncoder 进行密码匹配。

9. Token校验过滤器

1. 什么是 Token 校验过滤器

Token 校验过滤器是用于在每个请求到达受保护的资源之前,检查请求头中是否包含有效的 Token(例如 JWT)。如果 Token 有效,则将用户的身份信息加载到 SecurityContext 中,使得 Spring Security 能够正常处理授权逻辑。

2. 创建 Token 校验过滤器
一个典型的 Token 校验过滤器会从请求头中提取 Token,解析 Token,验证其有效性,并根据 Token 提取用户信息。如果 Token 无效,则拒绝该请求。

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final UserDetailsService userDetailsService;
    private final JwtTokenUtil jwtTokenUtil;

    public JwtAuthenticationFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
        this.userDetailsService = userDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        String requestTokenHeader = request.getHeader("Authorization");

        String username = null;
        String jwtToken = null;

        // JWT Token 在 "Bearer token" 格式中传递
        if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
            jwtToken = requestTokenHeader.substring(7);
            try {
                username = jwtTokenUtil.getUsernameFromToken(jwtToken);
            } catch (IllegalArgumentException e) {
                System.out.println("Unable to get JWT Token");
            } catch (ExpiredJwtException e) {
                System.out.println("JWT Token has expired");
            } catch (JwtException e) {
                System.out.println("JWT Token is invalid");
            }
        } else {
            logger.warn("JWT Token does not begin with Bearer String");
        }

        // 校验 Token 并设置身份验证信息
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

            // 验证 Token
            if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
                JwtAuthenticationToken authentication = new JwtAuthenticationToken(
                    userDetails, null, userDetails.getAuthorities()
                );
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                
                // 将认证信息设置到 SecurityContext 中
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        // 继续处理其他过滤器链
        filterChain.doFilter(request, response);
    }
}

3. JWT Token 工具类

为了处理 JWT Token 的生成、解析和校验逻辑,需要创建一个工具类 JwtTokenUtil,负责处理 Token 相关的操作。

@Component
public class JwtTokenUtil {

    private final String SECRET_KEY = "secret";

    // 从 Token 中获取用户名
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    // 验证 Token 是否有效
    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    // 判断 Token 是否过期
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    // 从 Token 中获取到期时间
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    // 从 Token 中获取指定的 Claim
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    // 解析 Token 中的 Claims
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
    }

    // 生成 Token
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    // 创建 Token
    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))  // 10小时
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }
}

4. 配置 Token 校验过滤器

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final UserDetailsService userDetailsService;

    public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, UserDetailsService userDetailsService) {
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
        this.userDetailsService = userDetailsService;
    }

    // 配置 SecurityFilterChain
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests(authz -> authz
                .antMatchers("/authenticate", "/register").permitAll()  // 公开的接口
                .anyRequest().authenticated()  // 其他接口需要认证
            )
            .sessionManagement(sess -> sess
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // 无状态的会话管理
            );

        // 添加 JWT 校验过滤器到过滤器链
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

5. 总结

  • JwtAuthenticationFilter 通过拦截请求,提取和验证 JWT Token,并将用户信息加载到 SecurityContext 中。
  • JwtTokenUtil 负责 Token 的生成、解析和校验,确保 Token 的有效性。
  • SecurityConfig 中将 Token 校验过滤器添加到 Spring Security 过滤器链中,并使用无状态的会话管理来确保每个请求都携带有效的Token。

这样配置后,每次请求都会经过 Token 校验,确保用户的身份和权限。

10. 刷新令牌接口

在 Spring Security 中,刷新令牌(Refresh Token)接口通常用于获取新的 Access Token,而不需要用户重新登录。这个机制通常与 JWT(JSON Web Token)配合使用,避免频繁要求用户输入凭证。

注意:实际生产中refreshToken令牌的生成方式、加密算法可以和accessToken不同。

1. 实现刷新令牌接口

@RestController
@RequestMapping("/api/token")
public class RefreshTokenController {

    private final JwtTokenUtil jwtTokenUtil;
    private final UserDetailsService userDetailsService;

    public RefreshTokenController(JwtTokenUtil jwtTokenUtil, UserDetailsService userDetailsService) {
        this.jwtTokenUtil = jwtTokenUtil;
        this.userDetailsService = userDetailsService;
    }

    @PostMapping("/refresh")
    public ResponseEntity<?> refreshAccessToken(@RequestBody TokenRefreshRequest request) {
        String refreshToken = request.getRefreshToken();
        if (jwtTokenUtil.validateRefreshToken(refreshToken)) {
            // 从 Refresh Token 中获取用户名
            String username = jwtTokenUtil.getUsernameFromToken(refreshToken);

            // 加载用户详细信息
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            // 生成新的 Access Token
            String newAccessToken = jwtTokenUtil.generateToken(userDetails);

            // 返回新的 Access Token
            return ResponseEntity.ok(new TokenRefreshResponse(newAccessToken));
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid Refresh Token");
        }
    }
}

在这个控制器中:

  • refreshAccessToken 方法用于处理刷新令牌的请求。
  • 客户端将 Refresh Token 发送到这个接口,服务端验证其有效性并生成新的 Access Token。

2. 定义请求和响应类

需要定义客户端发送的请求体和服务端的响应体。

public class TokenRefreshRequest {
    private String refreshToken;

    public String getRefreshToken() {
        return refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }
}

public class TokenRefreshResponse {
    private String accessToken;

    public TokenRefreshResponse(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }
}

3. JWT Token 工具类中的 Refresh Token 验证

在 JwtTokenUtil 中,需要有方法来验证和处理 Refresh Token。

@Component
public class JwtTokenUtil {

    private final String SECRET_KEY = "secret";

    // 从 Token 中获取用户名
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    // 验证 Token 是否有效
    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    // 验证 Refresh Token 是否有效
    public boolean validateRefreshToken(String token) {
        return !isTokenExpired(token);  // 可以自定义更多验证逻辑
    }

    // 判断 Token 是否过期
    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    // 从 Token 中获取到期时间
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    // 从 Token 中获取指定的 Claim
    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getAllClaimsFromToken(token);
        return claimsResolver.apply(claims);
    }

    // 解析 Token 中的 Claims
    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
    }

    // 生成 Access Token
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        return createToken(claims, userDetails.getUsername());
    }

    // 生成 Token
    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))  // 10小时
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }
}

4. 完整的刷新令牌接口工作流程

  • 客户端请求 POST /api/token/refresh,并在请求体中包含 Refresh Token。
  • 服务器验证 Refresh Token 的有效性:
    • 如果有效,生成新的 Access Token 并返回给客户端。
    • 如果无效,返回 401 未授权状态码,提示 Refresh Token 无效或已过期。
  • 客户端接收新的 Access Token,并继续使用它进行 API 调用。

5. 总结

  • Refresh Token 是一种用来延长用户登录状态的机制,不需要用户重新登录。
  • 通过创建一个刷新令牌接口,客户端可以使用 Refresh Token 来获取新的 Access Token。
  • 在服务器端,我们通过 JWT 工具类来验证和生成令牌,并确保每次请求的安全性。

11. Spring Security全局配置

在全局配置类做一些配置,如下:

  • 应用登录过滤器的配置
  • 将登录接口、令牌刷新接口放行,不需要拦截
  • 配置AuthenticationEntryPoint、AccessDeniedHandler
  • 禁用session,前后端分离+JWT方式不需要session
  • 将token校验过滤器TokenAuthenticationFilter添加到过滤器链中,放在UsernamePasswordAuthenticationFilter之前。
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
    private final TokenAuthenticationFilter tokenAuthenticationFilter;

    public SecurityConfig(JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, 
                          JwtAccessDeniedHandler jwtAccessDeniedHandler,
                          TokenAuthenticationFilter tokenAuthenticationFilter) {
        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
        this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
        this.tokenAuthenticationFilter = tokenAuthenticationFilter;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 关闭 CSRF,因为是前后端分离,不使用表单登录
            .csrf().disable()

            // 异常处理,未认证或权限不足时的处理
            .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)  // 未登录处理
                .accessDeniedHandler(jwtAccessDeniedHandler)            // 权限不足处理

            // 禁用 session,JWT 是无状态的,所以不需要会话管理
            .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

            // 允许不需要认证的接口
            .and()
            .authorizeHttpRequests()
                .antMatchers("/login", "/token/refresh").permitAll()   // 登录和令牌刷新接口放行
                .anyRequest().authenticated()                          // 其他接口需要认证

            // 将自定义的 TokenAuthenticationFilter 添加到过滤器链中,并放在 UsernamePasswordAuthenticationFilter 之前
            .and()
            .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

12. 测试

1. 测试登录接口

postman访问 http://localhost:2001/security-jwt/login
在这里插入图片描述
可以看到,成功返回了两个token。

2. 请求头不携带token,直接请求http://localhost:2001/security-jwt/hello,如下:
在这里插入图片描述
可以看到,直接进入了EntryPointUnauthorizedHandler这个处理器。

3. 携带token访问http://localhost:2001/security-jwt/hello,如下:
在这里插入图片描述
成功访问,token是有效的。

4. 刷新令牌接口测试,携带一个过期的令牌访问如下:
在这里插入图片描述
5. 刷新令牌接口测试,携带未过期的令牌测试,如下:
在这里插入图片描述
可以看到,成功返回了两个新的令牌。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值