Spring Security+RBAC的权限验证示例

参考资料:【SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权-挑战黑马&尚硅谷】 https://www.bilibili.com/video/BV1mm4y1X7Hc/?p=33&share_source=copy_web&vd_source=bccce5410c11fef6dbb58270c065e8c8

重写实现UserDetails

示例

import com.alibaba.fastjson2.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
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;
import java.util.stream.Collectors;


//重新实现UserDetails接口,用于Spring Security的登录验证
@Data
@AllArgsConstructor
public class LoginStaff implements UserDetails {

    private final Staff staff;
    private final List<String> permissions; // 权限集合变量

    @JSONField(serialize = false) //不序列化到redis中,节省内存空间
    private List<SimpleGrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        //不为空就返回,提高效率
        if(authorities != null) {
            return authorities;
        }

        //将List<String> permissions转换为Collection<? extends GrantedAuthority>对象返回
        //通过查找GrantedAuthority的实现类SimpleGrantedAuthority发现可以将List<String>中的String封装为SimpleGrantedAuthority

//        List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
//        for (String permission : permissions) {
//            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
//            simpleGrantedAuthorities.add(simpleGrantedAuthority);
//        }

        //使用stream实现
        authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;// 返回权限集合
    }

    @Override
    public String getPassword() {
        //这儿返回密码会用到:DaoAuthenticationProvider 使用 PasswordEncoder 来比较用户提交的密码和数据库中存储的密码。
        return staff.getPasswordHash();
    }

    @Override
    public String getUsername() {
        return staff.getUsername();
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return !staff.getIsBanned();
    }
}

重写实现UserDetailsService

示例:

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cxy.cbms.manage.entity.LoginStaff;
import com.cxy.cbms.manage.entity.Staff;
import com.cxy.cbms.manage.mapper.StaffMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private StaffMapper staffMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {


        //查询用户信息
        LambdaQueryWrapper<Staff> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Staff::getUsername, username);
        Staff staff = staffMapper.selectOne(queryWrapper);
        if(Objects.isNull(staff))
        {
            throw new UsernameNotFoundException("用户名或密码错误");
        }
        // 查询用户的权限
        List<String> authorities = staffMapper.selectStaffAuthorities(staff.getId());
        //把数据封装成UserDetails返回
        return new LoginStaff(staff, authorities, null);
    }
}

Spring Sceurity配置

当 Spring Security 启动时,以下顺序会被执行:

  1. Spring 容器初始化 SecurityConfig 类。
  2. 创建 PasswordEncoder Bean。
  3. 创建 AuthenticationManager Bean。
  4. 调用 configureGlobal 方法,完成 UserDetailsServicePasswordEncoder 的绑定。
  5. 创建 SecurityFilterChain Bean,配置 HTTP 请求的安全规则。

SecurityConfig.java

import com.cxy.cbms.common.filter.JwtFilter;
import com.cxy.cbms.manage.service.impl.UserDetailsServiceImpl;
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.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
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig{

    @Autowired
    private JwtFilter jwtFilter; // 注入 JwtFilter

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 使用 BCrypt 加密
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);
        // 配置用户认证逻辑(例如从数据库中加载用户)
        authenticationManagerBuilder
                .userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
        return authenticationManagerBuilder.build();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable() // 禁用 CSRF
                .authorizeHttpRequests()
                .requestMatchers(PermitAllPaths.getPaths().toArray(new String[0])).permitAll() // 允许任何人访问接口
                .anyRequest().authenticated() // 其他请求需要认证
                .and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 关闭 session
                // 将 JwtFilter 添加到过滤器链中,放在 UsernamePasswordAuthenticationFilter 之前
        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public UserDetailsServiceImpl userDetailsService() {
        return new UserDetailsServiceImpl(); // 自定义用户详情服务
    }


}

JwtFilter.java

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.cxy.cbms.common.config.PermitAllPaths;
import com.cxy.cbms.common.entity.Result;
import com.cxy.cbms.common.utils.RedisService;
import com.cxy.cbms.common.utils.TokenUtil;
import com.cxy.cbms.manage.entity.LoginStaff;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Objects;

@Slf4j
@Component
public class JwtFilter extends OncePerRequestFilter {

    @Autowired
    private RedisService redisService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 获取当前请求路径
        String path = request.getServletPath();

        // 检查token是否存在且是否是需要放行的路径
        if (PermitAllPaths.isPermitAllPath(path)) {
            filterChain.doFilter(request, response); // 放行请求
            return;
        }

        //获取请求头中的token
        String token = extractTokenFromRequest(request);

        if (TokenUtil.validateToken(token)) {
            Long userid = TokenUtil.getIdFromToken(token);
            //从redis中获取员工信息
            LoginStaff loginStaff = (LoginStaff) redisService.getValue("Staff:" + userid);
            //如果redis中员工信息消失,新登录,如下原因会导致
            //1. token过期时间长于redis存储数据时间
            //2. 员工信息(如权限等)被修改后redis中员工信息被删除
            if(Objects.isNull(loginStaff))
            {
                log.warn("Redis 中未找到员工信息,用户 ID: {}", userid);
                // 返回 401 未授权错误
                returnResult(response, Result.unauthorized("请重新登录", null));
                return; // 不放行请求
            }
            log.info("员工信息:"+loginStaff);
            //可以将 loginStaff 存储在 SecurityContext 中,供后续流程使用
            SecurityContextHolder.getContext().setAuthentication(
                    new UsernamePasswordAuthenticationToken(loginStaff, null, loginStaff.getAuthorities())
            );
            filterChain.doFilter(request, response);
        }
        else {
            // Token 无效或不存在
            returnResult(response, Result.unauthorized("无效的 Token", null));
        }
    }

    /**
     * 从请求头中提取 Bearer Token
     *
     * @param request HTTP 请求对象
     * @return 返回提取的 token,如果未找到或格式错误则返回 null
     */
    public static String extractTokenFromRequest(HttpServletRequest request) {
        // 获取请求头中的 Authorization 头
        String tokenHeader = request.getHeader("Authorization");

        // 判断是否有 Authorization 头,并且是否以 Bearer 开头
        if (StringUtils.isNotBlank(tokenHeader) && tokenHeader.startsWith("Bearer ")) {
            return tokenHeader.substring(7); // 去掉 "Bearer " 前缀
        }

        // 如果没有有效的 Authorization 头,返回 null
        return null;
    }


    // 通用方法:返回 Result 格式数据
    private void returnResult(HttpServletResponse response, Result<?> result) throws IOException {
        // 设置响应状态码(可选)
        response.setStatus(result.getCode());
        // 设置响应内容类型
        response.setContentType("application/json;charset=UTF-8");
        // 将 Result 对象转换为 JSON 并写入响应体
        String jsonResult = new ObjectMapper().writeValueAsString(result);
        response.getWriter().write(jsonResult);
    }
}

LoginService服务实现

import com.cxy.cbms.common.entity.Result;
import com.cxy.cbms.common.utils.RedisService;
import com.cxy.cbms.manage.dto.StaffInfo;
import com.cxy.cbms.manage.dto.StaffLogin;
import com.cxy.cbms.manage.entity.LoginStaff;
import com.cxy.cbms.manage.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.List;
import static com.cxy.cbms.common.utils.TokenUtil.genAccessToken;

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisService redisService;
    @Override
    public Result login(StaffLogin staffLogin) {

        //AuthenticationManager authenticate进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(staffLogin.getUsername(), staffLogin.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        //如果认证没通过,给出对应的提示
        if (authenticate == null || !authenticate.isAuthenticated()) {
            return Result.unauthorized("用户名或密码错误",null);
        }
        //如果认证通过,使用userid生成一个jwt jwt存入StaffInfo返回
        LoginStaff loginStaff = (LoginStaff) authenticate.getPrincipal();
        //生成jwt
        Long id = loginStaff.getStaff().getId();
        String jwtToken = genAccessToken(id);
        //把完整的员工信息存入redis userid作为key
        redisService.setValueWithExpire("Staff:"+id,loginStaff,3600);
        //封装返回前端
        List<String> authorityList = loginStaff.getPermissions();
        StaffInfo staffInfo = loginStaff.getStaff().toStaffInfo(authorityList,jwtToken);
        return Result.success("登录成功",staffInfo);
    }
}

启用权限配置

在SceurityConfig上添加注解

@EnableGlobalMethodSecurity(prePostEnabled = true)

在Spring Security中,@EnableGlobalMethodSecurity 注解已被弃用。从Spring Security 5.6版本开始,推荐使用 @EnableMethodSecurity 注解来替代它。@EnableMethodSecurity 提供了更现代化的配置方式,并且与Spring Security的其他功能更加兼容。

@EnableMethodSecurity(prePostEnabled = true)

在对应Controller接口前使用@PreAuthorize配置访问接口需要的权限

在访问接口时,会先读取SecurityContext中存储的权限,有PreAuthorize中对应的权限才会放行接口访问权

@PreAuthorize("hasAuthority('your_authority')")
posted @ 2025-03-05 15:09  NONAME-X  阅读(39)  评论(0)    收藏  举报