Spring Security 6.X + JWT + RBAC 权限管理实战教程(上)

前言

本教程基于 Spring Boot 3.x + Spring Security 6.x 实现,采用 JWT + Redis 的认证方案,结合 RBAC 权限模型,实现了一个完整的权限管理系统。

一、项目依赖配置

关键依赖说明:

	<!-- SpringWeb -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	
	<!-- lombok -->
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<optional>true</optional>
	</dependency>
	
	<!-- mysql驱动 -->
	<dependency>
		<groupId>com.mysql</groupId>
		<artifactId>mysql-connector-j</artifactId>
		<scope>runtime</scope>
	</dependency>
	
	<!-- Druid 连接池 -->
	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>druid-spring-boot-3-starter</artifactId>
		<version>1.2.21</version>
	</dependency>
	
	<!-- MybatisPlus起步依赖 -->
	<dependency>
		<groupId>com.baomidou</groupId>
		<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
		<version>3.5.5</version>
	</dependency>
	
	<!-- Knife4j API文档生产工具 -->
	<dependency>
		<groupId>com.github.xiaoymin</groupId>
		<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
		<version>4.4.0</version>
	</dependency>
	<!-- swagger注解支持:Knife4j依赖本依赖 -->
	<dependency>
		<groupId>io.swagger</groupId>
		<artifactId>swagger-annotations</artifactId>
		<version>1.5.22</version>
	</dependency>
	
	<!-- redis -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-redis</artifactId>
	</dependency>
	
	<!-- SpringSecurity -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-security</artifactId>
	</dependency>
	
	<!-- validation参数校验依赖 -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-validation</artifactId>
	</dependency>
	
	<!-- fastjson -->
	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>fastjson</artifactId>
		<version>2.0.40</version>
	</dependency>
	
	<!-- Hutool工具类库:图形验证码生成、加解密、简单http请求、类拷贝等 -->
	<dependency>
		<groupId>cn.hutool</groupId>
		<artifactId>hutool-all</artifactId>
		<version>5.8.25</version>
	</dependency>
	
	<!-- JWT依赖:用于生成和解析JWT -->
	<dependency>
		<groupId>io.jsonwebtoken</groupId>
		<artifactId>jjwt</artifactId>
		<version>0.9.1</version>
	</dependency>
	<!-- 如果jdk大于1.8,则还需导入以下依赖 -->
	<dependency>
		<groupId>javax.xml.bind</groupId>
		<artifactId>jaxb-api</artifactId>
		<version>2.3.1</version>
	</dependency>
	
	<!-- SpringTest -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>

二、创建RBAC权限数据表

RBAC 模型基于角色的访问控制,这是软件设计中最常用的权限管理模型,通过权限关联角色、角色关联用户的方法来间接地赋予用户权限,从而实现用户与权限的解耦。
在这里插入图片描述

💡 RBAC模型说明:

  • 用户表(user):存储用户基本信息
  • 角色表(role):定义系统角色
  • 菜单表(menu):存储系统菜单和权限标识
  • 关联表:通过中间表实现多对多关系

1. 员工表(用户表)

-- auto-generated definition
create table emp
(
    id          bigint unsigned auto_increment comment 'ID'
        primary key,
    dept_id     bigint unsigned              null comment '部门ID',
    username    varchar(50)                  not null comment '用户名',
    password    varchar(255)                 null comment '密码',
    name        varchar(50)                  not null comment '姓名',
    status      tinyint unsigned default '0' null comment '状态 0-正常 1-禁用',
    constraint username
        unique (username)
)
    comment '员工表' row_format = DYNAMIC;

2.菜单表

-- auto-generated definition
create table menu
(
    id          bigint unsigned auto_increment comment '菜单ID'
        primary key,
    parent_id   bigint unsigned            null comment '父菜单ID(支持多级菜单)',
    menu_name   varchar(50) default 'NULL' not null comment '菜单名称',
    path        varchar(255)               null comment '路由地址',
    component   varchar(255)               null comment '组件路径',
    visible     tinyint     default 0      null comment '菜单状态(0显示 1隐藏)',
    status      tinyint     default 0      null comment '菜单状态(0正常 1禁用)',
    perms       varchar(100)               null comment '权限标识( 如user:read )',
    icon        varchar(100)               null comment '菜单图标',
    order_num   tinyint                    null comment '显示顺序',
    type        char                       null comment '菜单类型(''M''-菜单 ''B''-按钮)',
    create_time datetime                   null comment '创建时间',
    create_user bigint                     null comment '创建者ID',
    update_time datetime                   null comment '更新时间',
    update_user bigint                     null comment '更新者ID'
)
    comment '菜单表' row_format = DYNAMIC;

3.角色表

-- auto-generated definition
create table role
(
    id          bigint unsigned auto_increment comment '主键ID'
        primary key,
    name        varchar(50)       null comment '角色名称',
    role_key    varchar(50)       null comment '角色权限标识(如ADMIN)',
    description varchar(255)      null comment '角色描述',
    status      tinyint default 0 null comment '角色状态(0正常 1停用)',
    create_time datetime          null comment '创建时间',
    create_user bigint            null comment '创建者ID',
    update_time datetime          null comment '更新时间',
    update_user bigint            null comment '更新者ID'
)
    comment '角色表' row_format = DYNAMIC;

4.员工角色关联表

-- auto-generated definition
create table emp_role
(
    emp_id  bigint unsigned auto_increment comment '员工id',
    role_id bigint unsigned default '0' not null comment '角色id',
    primary key (emp_id, role_id)
)
    row_format = DYNAMIC;

5.角色菜单关联表

-- auto-generated definition
create table role_menu
(
    role_id bigint unsigned auto_increment comment '角色ID',
    menu_id bigint unsigned default '0' not null comment '菜单id',
    primary key (role_id, menu_id)
)
    row_format = DYNAMIC;

三、核心配置类

1. 安全配置类

创建 SecurityConfig.java

/**
 * @description SpringSecurity配置类
 */
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 开启SpringSecurity的自定义配置(在SpringBoot项目中可以省略)
@EnableMethodSecurity // 开启全局函数权限
public class SecurityConfig {

    // 自定义的用于认证的过滤器,进行jwt的校验操作
    private final JwtTokenOncePerRequestFilter jwtTokenFilter;

    // 认证用户无权限访问资源的处理器
    private final CustomerAccessDeniedHandler customerAccessDeniedHandler;

    // 客户端进行认证数据的提交时出现异常,或者是匿名用户访问受限资源的处理器
    private final AnonymousAuthenticationHandler anonymousAuthentication;

    // 用户认证校验失败处理器
    private final LoginFailureHandler loginFailureHandler;


    /**
     * 创建BCryptPasswordEncoder注入容器,用于密码加密
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 登录时调用AuthenticationManager.authenticate执行一次校验
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{

        // 添加自定义异常处理类
        http.exceptionHandling(configurer -> {
            configurer.accessDeniedHandler(customerAccessDeniedHandler) // 配置认证用户无权限访问资源的处理器
                    .authenticationEntryPoint(anonymousAuthentication); // 配置匿名用户未认证的处理器
        });

        // 配置关闭csrf机制
        http.csrf(AbstractHttpConfigurer::disable);
        // 用户认证校验失败处理器
        http.formLogin(conf -> conf.failureHandler(loginFailureHandler));
        // STATELESS(无状态):表示应用程序是无状态的,不创建会话
        http.sessionManagement(conf -> conf.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        // 配置放行路径
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers(
                        "/swagger-ui/**", // 放行Swagger相关路径
                        "/swagger-ui.html",
                        "/swagger-resources/**",
                        "/v3/api-docs/**",
                        "/webjars/**",
                        "/doc.html",
                        "/admin/emp/login"  // 放行登录接口路径
                ).permitAll()
                .anyRequest().authenticated()
        );
        // 配置过滤器的执行顺序
        http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }


}

什么是 BCrypt 加密?
BCrypt 是一个基于 Blowfish 密码算法的密码哈希函数,专门为密码加密而设计。它具有以下特点:

  1. 自适应性:可以通过增加迭代次数来增加计算强度
  2. 加盐处理:自动生成随机盐值并混入密码
  3. 防彩虹表:每次加密同一个密码得到的结果都不同
  4. 单向加密:无法通过加密后的密文反推原始密码

2. JWT工具类

创建 JwtUtil.java

/**
 * JWT令牌工具类
 */
public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}

application.yml中加入JWT相关配置:

jwt:
  secret-key: your_key # jwt签名加密秘钥
  ttl: 7200000 # jwt过期时间
  token-name: Authorization # 前端传递过来的令牌名称

创建 JwtProperties.java

/**
 * 生成jwt令牌相关配置
 */
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {


    private String secretKey; // jwt签名加密秘钥
    private long ttl; // jwt过期时间
    private String tokenName; // jwt签名加密秘钥

}

四、用户认证实现

1.创建员工实体类

创建 Emp.java

/**
 * 管理员实体类
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Tag(name = "Emp", description = "员工实体")
public class Emp implements Serializable {

    @Serial
    private static final long serialVersionUID = 4317337818874663187L;

    @Schema(description = "员工ID")
    private Long id;

    @Schema(description = "部门ID")
    private Long deptId;

    @Schema(description = "用户名")
    private String username;

    @JSONField(serialize = false)
    @Schema(description = "密码")
    private String password;

    @Schema(description = "姓名")
    private String name;

    @Schema(description = "状态: 0-正常, 1-禁用")
    private Integer status;
}

2. 自定义用户详情类

创建 EmpLogin.java实现UserDetails接口:用于封装用户的详细信息权限列表

/**
 * @description UserDetails的实现类
 */
@Data
@NoArgsConstructor
public class EmpLogin implements UserDetails {

    @Serial
    private static final long serialVersionUID = 7330836274775504268L;

    public EmpLogin(Emp emp, List<String> list) {
        this.emp = emp;
        this.list = list;
    }

    // 权限列表
    private List<String> list;

    private Emp emp;

    //自定义一个权限列表的集合,中转操作
    @JSONField(serialize = false) //在序列化对象时忽略该字段
    private List<SimpleGrantedAuthority> authorities;

    // 用于返回权限信息
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) {
            return authorities;
        }
        authorities = new ArrayList<>();
        for (String item : list) {
            if (item != null && !item.trim().isEmpty()) {
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(item);
                authorities.add(authority);
            }
        }
        return authorities;
    }

    // 获取密码
    @Override
    public String getPassword() {
        return emp.getPassword();
    }

    // 获取用户名
    @Override
    public String getUsername() {
        return emp.getUsername();
    }

    // 账号是否未过期
    @Override
    public boolean isAccountNonExpired() {
        return UserDetails.super.isAccountNonExpired();
    }

    // 判断账号是否没有锁定
    @Override
    public boolean isAccountNonLocked() {
        return UserDetails.super.isAccountNonLocked();
    }

    // 判断账户是否没有超时
    @Override
    public boolean isCredentialsNonExpired() {
        return UserDetails.super.isCredentialsNonExpired();
    }

    // 判断账号是否可用
    @Override
    public boolean isEnabled() {
        return UserDetails.super.isEnabled();
    }
}

说明:

  • getAuthorities 方法构建用户的权限集合。
  • 重写 getPasswordgetUsername 方法,用于提供用户的凭证

3. 实现UserDetailsService

创建 UserDetailsServiceImpl.java实现UserDetailsService接口:完成自定义用户查询逻辑

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final EmpMapper empMapper;
    private final MenuMapper menuMapper;

    /**
     * 根据用户名查询用户信息
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        if (username.isEmpty()){
            throw new InternalAuthenticationServiceException("");
        }
        //  根据用户名查询用户信息
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        Emp emp = empMapper.selectOne(wrapper);
        // 判断是否查到用户 如果没查到抛出异常
        if (ObjectUtil.isNull(emp)){
            throw new UsernameNotFoundException("");
        }
        // 2.赋权操作 查询数据库
        List<String> list = menuMapper.getMenuByUserId(emp.getId());

        for (String s : list) {
            System.out.println(s);
        }

        return new EmpLogin(emp, list);
    }
}

说明:

  • 根据用户名查询用户信息,如果用户不存在,抛出异常。
  • 从数据库中查询用户的权限列表并封装到EmpLogin对象中。

五、JWT认证过滤器

创建 JwtAuthenticationTokenFilter.java:用于拦截请求并校验 JWT 的有效性

/**
 * @description 每次请求的 Security 过滤类。执行jwt有效性检查,如果失败,不会设置 SecurityContextHolder 信息,会进入 AuthenticationEntryPoint
 */
// 每一个servlet请求,只执行一次
@Component
@Slf4j
public class JwtTokenOncePerRequestFilter extends OncePerRequestFilter {

    @Autowired
    private JwtProperties jwtProperties; // JWT相关属性配置类

    @Autowired
    private RedisUtil redisUtil; // Redis工具类

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    // 添加白名单路径列表
    private final String[] whitelist = {
            "/admin/emp/login",
            "/swagger-ui/**",
            "/swagger-ui.html",
            "/swagger-resources/**",
            "/v3/api-docs/**",
            "/webjars/**",
            "/doc.html"
    };

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 1. 判断当前请求是否在白名单中
        String uri = request.getRequestURI();
        if (isWhitelisted(uri)) {
            filterChain.doFilter(request, response);
            return;
        }
        try {
            // 2. 校验token
            this.validateToken(request);
        } catch (AuthenticationException e) {
            loginFailureHandler.onAuthenticationFailure(request, response, e); // 处理登录失败的异常
            return;
        }
        filterChain.doFilter(request, response);
    }

    // 判断请求路径是否在白名单中
    private boolean isWhitelisted(String uri) {
        for (String pattern : whitelist) {
            if (pattern.endsWith("/**")) {
                // 处理通配符路径
                String basePattern = pattern.substring(0, pattern.length() - 3);
                if (uri.startsWith(basePattern)) {
                    return true;
                }
            } else if (pattern.equals(uri)) {
                // 精确匹配
                return true;
            }
        }
        return false;
    }

    // 校验token
    private void validateToken(HttpServletRequest request) {
        // 说明:登录了,再次请求其他需要认证的资源
        String token = request.getHeader("Authorization");
        if (ObjectUtils.isEmpty(token)) { // header没有token
            token = request.getParameter("Authorization");
        }
        if (ObjectUtils.isEmpty(token)) {
            throw new CustomerAuthenticationException("token为空");
        }
        // redis进行校验
        if (!redisUtil.hasKey("token_" + token)) {
            throw new CustomerAuthenticationException("token已过期");
        }
        // 校验token
        EmpLogin empLogin;
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getSecretKey(), token);
            String loginUserString = claims.get(JwtClaimsConstant.EMP_LOGIN).toString();
            // 把json字符串转为对象
            empLogin = JSON.parseObject(loginUserString, EmpLogin.class);
            log.info("当前员工id:{}", empLogin.getEmp().getId());
            BaseContext.setCurrentId(empLogin.getEmp().getId());
        } catch (Exception ex) {
            throw new CustomerAuthenticationException("token校验失败");
        }
        BaseContext.setCurrentId(empLogin.getEmp().getId());
        // 把校验后的用户信息再次放入到SpringSecurity的上下文中
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(empLogin, null,empLogin.getAuthorities()); // 已认证的 Authentication 对象,包含用户的权限信息
        SecurityContextHolder.getContext().setAuthentication(authentication);
        System.out.println(empLogin.getAuthorities());
    }
}

说明:

  • 过滤器在每次请求时执行 JWT 校验。
  • 通过 Redis 检查 Token 的有效性,解析后将用户信息存入 SecurityContextHolder

六、自定义处理器

帮助我们在认证失败或者授权失败的情况下也能和我们接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。

1.自定义验证异常类

创建 CustomerAuthenticationException.java:用于在认证失败的情况下,抛出自定义的认证异常

/**
 * @description 自定义认证验证异常
 */
public class CustomerAuthenticationException extends AuthenticationException {

    public CustomerAuthenticationException(String msg) {
        super(msg);
    }
}

说明:
通过继承 AuthenticationException,可以将自定义异常与 Spring Security 的认证机制结合,支持在认证失败时抛出特定的错误消息。

2.编写认证用户无权限访问处理器

创建CustomerAccessDeniedHandler.java:处理已认证用户尝试访问无权限资源的情况,返回统一格式的 JSON 响应

/**
 * @description 认证用户无权限访问的处理器
 */
@Component
@Slf4j
public class CustomerAccessDeniedHandler implements AccessDeniedHandler{
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {

        log.error("权限不足,URI:{},异常:{}", request.getRequestURI(), accessDeniedException.getMessage());
        // 发生这个行为,做响应处理,给一个响应的结果
        response.setContentType("application/json;charset=utf-8");
        // 构建输出流对象
        ServletOutputStream outputStream = response.getOutputStream();
        // 调用fastjson工具,进行Result对象序列化
        String error = JSON.toJSONString(Result.error("权限不足,请联系管理员"));
        outputStream.write(error.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

说明:

  • 使用 AccessDeniedHandler 接口实现自定义逻辑。
  • 捕获访问拒绝异常并记录日志,同时通过输出流返回统一的 JSON 响应结构,方便前端统一处理。

3.编写匿名用户访问受限资源的处理器

创建AnonymousAuthenticationHandler.java:处理未认证用户(匿名用户)访问受限资源的情况,返回特定的错误信息。

/**
 * @description 客户端进行认证数据的提交时出现异常,或者是匿名用户访问受限资源的处理器
 */
@Component
public class AnonymousAuthenticationHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {


        // 发生这个行为,做响应处理,给一个响应的结果
        response.setContentType("application/json;charset=utf-8");
        // 构建输出流对象
        ServletOutputStream outputStream = response.getOutputStream();
        // 调用fastjson工具,进行Result对象序列化
        String error = "";
        if (authException instanceof BadCredentialsException){
            // 用户名或密码错误  401
            error = JSON.toJSONString(Result.error(authException.getMessage()));
        } else if (authException instanceof InternalAuthenticationServiceException) {
            error = JSON.toJSONString(Result.error("用户名为空"));
        } else{
            error = JSON.toJSONString(Result.error("匿名用户无权限访问"));
        }
        outputStream.write(error.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
        
    }
}

说明:

  • 通过实现 AuthenticationEntryPoint 接口处理未认证用户的访问异常。
  • 根据不同类型的 AuthenticationException 返回不同的错误信息,增强了响应的针对性。

4.编写自定义认证失败的处理器

创建LoginFailureHandler.java:处理用户登录失败时的异常,返回详细的失败原因。

/**
 * @description 用户校验认证失败的处理器
 */
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        // 发生这个行为,做响应处理,给一个响应的结果
        response.setContentType("application/json;charset=utf-8");
        // 构建输出流对象
        ServletOutputStream outputStream = response.getOutputStream();
        String message;
        if (exception instanceof AccountExpiredException) {
            message = "用户过期,登录失败";
        } else if (exception instanceof BadCredentialsException) {
            message = "用户名或密码错误,请重新输入!";
        } else if (exception instanceof CredentialsExpiredException) {
            message = "密码过期,请重新输入!";
        } else if (exception instanceof DisabledException) {
            message = "账户被禁用,登录失败!";
        } else if (exception instanceof LockedException) {
            message = "账户被锁,登录失败!";
        } else if (exception instanceof InternalAuthenticationServiceException) {
            message = "账户不存在,登录失败!";
        } else if (exception instanceof CustomerAuthenticationException) {
            message = exception.getMessage();
        } else {
            message = "登录失败!";
        }
        // 调用fastjson工具,进行Result对象序列化
        String error = JSON.toJSONString(Result.error(message));
        outputStream.write(error.getBytes(StandardCharsets.UTF_8));
        outputStream.flush();
        outputStream.close();
    }
}

说明:

  • 覆写 onAuthenticationFailure 方法,根据不同的认证异常类型返回详细的失败原因。
  • 提供灵活的错误提示信息,便于用户快速定位登录失败的具体原因。

5.编写全局异常处理器

创建GlobalExceptionHandler.java:捕获并统一处理项目中的异常,包括业务异常和 SQL 异常。

/**
 * 全局异常处理器,处理项目中抛出的业务异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获所有异常
     */
    @ExceptionHandler(Exception.class)
    public Result ex(Exception ex){

        // 如果是访问拒绝异常,不处理,让SecurityConfig中配置的处理器处理
        if(ex instanceof AccessDeniedException) {

            throw (AccessDeniedException)ex;

        }

        log.error("全局异常信息:{}", ex.getMessage());
        return Result.error(StringUtils.hasLength(ex.getMessage()) ? ex.getMessage() : "操作失败");
    }

    /**
     * 捕获业务异常
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex){
        log.error("业务异常信息:{}", ex.getResultEnum().message());
        return Result.error(ex.getResultEnum());
    }

    /**
     * 处理SQL异常
     */
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        // 错误信息:Duplicate entry 'zhaosi' for key 'employee.idx_username' -- > 用户ID重复
        String message = ex.getMessage();
        if(message.contains("Duplicate entry")){
            String[] split = message.split(" ");
            String username = split[2];
            String msg = username + ResultEnum.USER_NAME_HAS_EXISTED.message();
            return Result.error(msg);
        }else {
            return Result.error(ResultEnum.UNKNOWN_ERROR);
        }

    }
}

在全局异常处理器中为何要这样写?
在上一篇文章中,进行详细的讲解 Spring Security 6.3 权限异常处理实战解析

七、登录接口实现

1.实体类准备

创建EmpLoginVO.java

/**
 * 用户登录响应对象
 */
@Data
@Builder
@Tag(name = "EmpLoginVO", description = "员工登录响应")
public class EmpLoginVO implements Serializable {

    @Serial
    private static final long serialVersionUID = 4393557997355879737L;

    @Schema(description = "用户ID")
    private Long id;

    @Schema(description = "用户名")
    private String username;

    @Schema(description = "姓名")
    private String name;

    @Schema(description = "令牌")
    private String token;
}

创建EmpLoginDTO.java

@Data
public class EmpLoginDTO implements Serializable {

    @Serial
    private static final long serialVersionUID = 8347822700891152077L;

    @NotBlank(message = "账号不能为空")
    @Pattern(regexp = "^\\w{5,20}$", message = "用户名的长度必须为5~16位")
    private String username; // 账号

    @NotBlank(message = "密码不能为空")
    @Pattern(regexp = "^\\w{5,16}$", message = "密码的长度必须为5~16位")
    private String password; // 密码
}

2.ThreadLocal工具类准备

创建BaseContext.java

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

3.Controller层

创建EmpController.java:

@CrossOrigin // 允许跨域
@RestController
@RequestMapping("admin/emp")
@Tag(name = "员工管理接口")
@Slf4j
@RequiredArgsConstructor
public class EmpController {

    private final EmpService empService;
    private final RedisUtil redisUtil;

    /**
     * 员工登录
     * @param empLoginDTO 员工登录信息
     * @return  统一返回结果
     */
    @PostMapping("/login")
    @Operation(summary = "员工登录")
    public Result<EmpLoginVO> login(@Validated @RequestBody EmpLoginDTO empLoginDTO) {
        log.info("员工:{},登录成功", empLoginDTO.getUsername());
        EmpLoginVO empLoginVO = empService.empLogin(empLoginDTO);

        return Result.success(empLoginVO);
    }

    /**
     * 员工退出登录
     * @return  统一返回结果
     */
    @PostMapping("/logout")
    @Operation(summary = "员工退出登录")
    public Result logout(HttpServletRequest request, HttpServletResponse response) {
        log.info("员工ID:{},退出登录", BaseContext.getCurrentId());

        String token = request.getHeader("Authorization");
        if (ObjectUtils.isEmpty(token)) { // header没有token
            token = request.getParameter("Authorization");
        }
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            // 清除上下文
            new SecurityContextLogoutHandler().logout(request, response, authentication);
            // 清理redis
            redisUtil.del("token_" + token);
            // 清理ThreadLocal
            BaseContext.removeCurrentId();

        }
        return Result.success();
    }
}

4.Service层

创建EmpService.java接口:

public interface EmpService extends IService<Emp> {
    /**
     * 管理员登录
     * @param empLoginDTO 管理员登录表单
     * @return 员工登录VO
     */
    EmpLoginVO empLogin(EmpLoginDTO empLoginDTO);
    
}

创建EmpServiceImpl.java实现类:

@Service
@Slf4j
@RequiredArgsConstructor
public class EmpServiceImpl extends ServiceImpl<EmpMapper, Emp> implements EmpService {

    private final EmpMapper empMapper;
    private final AuthenticationManager authenticationManager;
    private final JwtProperties jwtProperties;
    private final RedisUtil redisUtil;
    /**
     * 管理员登录
     */
    public EmpLoginVO empLogin(EmpLoginDTO empLoginDTO) {
        String username = empLoginDTO.getUsername();
        String password = empLoginDTO.getPassword();

        // 1. 封装用户登录表单,创建未认证Authentication对象
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);
        // 2. 进行校验
        Authentication authenticate = authenticationManager.authenticate(authentication);
        // 3. 获取用户信息
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误");
        }
        EmpLogin empLogin = (EmpLogin) authenticate.getPrincipal();
        Emp emp = empLogin.getEmp();
        if (emp.getStatus() == 1){
            throw new RuntimeException("账号被禁用");
        }
        log.info("员工 {} 登录成功", empLogin.getEmp().getName());

        // 登录成功,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        // 使用fastjson的方法,把对象转换成json字符串
        String loginEmpString = JSON.toJSONString(empLogin);
        claims.put(JwtClaimsConstant.EMP_LOGIN, loginEmpString);
        String token = JwtUtil.createJWT(
                jwtProperties.getSecretKey(),
                jwtProperties.getTtl(),
                claims);

        // 存储redis白名单
        String tokenKey = "token_" + token;
        redisUtil.set(tokenKey, token, jwtProperties.getTtl()/1000);

        BaseContext.setCurrentId(emp.getId());


        //3、返回实体对象
        return EmpLoginVO.builder()
                .id(emp.getId())
                .token(token)
                .username(emp.getUsername())
                .name(emp.getName())
                .build();

    }
}

5.Mapper层

创建EmpMapper.java接口:

@Mapper
public interface EmpMapper extends BaseMapper<Emp> {

}

八、权限控制使用

1. 注解方式

在需要权限控制的接口上添加注解


// 在启动类或者SecurityConfig配置类上添加
@EnableMethodSecurity

// 需要 "ems:employee:list" 权限才能访问
@PreAuthorize("hasAuthority('ems:employee:list')")
@GetMapping("/page")
public Result<PageResult> getList(EmpPageDTO empPageDTO) {
    return Result.success(empService.pageQuery(empPageDTO));
}

// 需要多个权限中的任意一个
@PreAuthorize("hasAnyAuthority('ems:employee:add','ems:employee:edit')")
@PostMapping
public Result add(@RequestBody EmpAddDTO empAddDTO) {
    empService.add(empAddDTO);
    return Result.success();
}

2. 配置方式

SecurityConfig 中配置:

.authorizeRequests()
    .antMatchers("/admin/emp/login").anonymous()  // 允许匿名访问
    .antMatchers("/admin/emp/info").authenticated()  // 需要认证
    .antMatchers("/admin/emp/**").hasRole("ADMIN")  // 需要ADMIN角色

总结

本教程介绍了 Spring Security 框架的基础搭建过程,包括认证、授权、异常处理等核心功能的实现。通过这些基础配置,我们已经构建了一个安全、可靠的权限管理框架。在下篇教程中,我们将继续完善角色管理和动态权限控制,敬请期待!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/956505.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

flutter 常用UI组件

文章目录 1. Toast 文本提示框oktoastbot_toast2. loading 加载窗flutter_easyloading3. 对话框gex dialog4.下拉刷新pull_to_refresh5. pop 窗custom_pop_up_menu6. pin code 密码框pinput7. 二维码qr_flutter8. swiper 滚动组件carousel_sliderflutter_swiper_view9. Badge 角…

《汽车维修技师》是什么级别的期刊?是正规期刊吗?能评职称吗?

​问题解答&#xff1a; 问&#xff1a;《汽车维修技师》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的正规学术期刊。 问&#xff1a;《汽车维修技师》级别&#xff1f; 答&#xff1a;省级。主管单位&#xff1a;北方联合出版传媒&#xff08;…

大语言模型的语境中“越狱”和思维链

大语言模型的语境中“越狱”和思维链 越狱(Jailbreaking) 含义:在大语言模型的语境中,“越狱”是指用户试图绕过语言模型的安全限制和使用规则,让模型生成违反伦理道德、包含有害内容(如暴力、歧视、恶意软件代码等)的输出。这些安全限制是由模型开发者设置的,目的是确…

【Rust自学】13.2. 闭包 Pt.2:闭包的类型推断和标注

13.2.0. 写在正文之前 Rust语言在设计过程中收到了很多语言的启发&#xff0c;而函数式编程对Rust产生了非常显著的影响。函数式编程通常包括通过将函数作为值传递给参数、从其他函数返回它们、将它们分配给变量以供以后执行等等。 在本章中&#xff0c;我们会讨论 Rust 的一…

Spring篇 解决因为Bean的循环依赖——理论篇

Spring Bean 循环依赖 循环依赖是指两个或多个 Bean 互相依赖&#xff0c;形成一个闭环。例如&#xff0c;A 依赖 B&#xff0c;B 又依赖 A。Spring则 提供了几种方式来解决这种循环依赖问题。 常见的几类 Bean 循环依赖场景 场景1&#xff1a; 解释&#xff1a;由于Bean A依…

三天急速通关Java基础知识:Day1 基本语法

三天急速通关JAVA基础知识&#xff1a;Day1 基本语法 0 文章说明1 关键字 Keywords2 注释 Comments2.1 单行注释2.2 多行注释2.3 文档注释 3 数据类型 Data Types3.1 基本数据类型3.2 引用数据类型 4 变量与常量 Variables and Constant5 运算符 Operators6 字符串 String7 输入…

Excel 技巧10 - 如何检查输入重复数据(★★)

本文讲了如何在Excel中通过COUNTIF来检查输入重复数据。 当输入重复数据时&#xff0c;显示错误提示。 1&#xff0c;通过COUNTIF来检查输入重复数据 比如下面是想检查不要输入重复的学号。 选中C列&#xff0c;点 Menu > 数据 > 数据验证 在数据验证页面&#xff0c…

请求响应-

一.DispatcherServlet 前端控制器 二.HttpServletRequest 请求:获取请求数据 三.HttpServletResponse 响应:设置响应数据 四.简单参数接收 简单参数:参数名与形参变量名相同,定义形参即可接收参数。 如果参数对应不上需要通过RequestParam完成映射,注意事项:加上了参数就必须…

MySQL中的GROUP_CONCAT函数将分组后的多个行值合并成一个字符串,并用指定分隔符连接

文章目录 前言什么是GROUP_CONCAT&#xff1f;基本语法 使用示例示例1: 基本用法示例2: 去重并排序 高级应用应用场景示例注意事项 结论表结构1. Orders 表 (订单表)2. Order_Details 表 (订单详情表) 示例数据Orders 表的数据Order_Details 表的数据 使用 GROUP_CONCAT 的查询…

游戏开发中常用的设计模式

目录 前言一、工厂模式二、单例模式三、观察者模式观察者模式的优势 四、状态模式状态模式的优势 五、策略模式策略模式的优势策略模式与状态模式有什么区别呢? 六、组合模式七、命令模式八、装饰器模式 前言 本文介绍了游戏开发中常用的设计模式&#xff0c;如工厂模式用于创…

【前端】用OSS增强Hexo的搜索功能

文章目录 前言配置 _config.fluid.yml云端实时更新 local-search.xml解决 OSS.Bucket 的跨域问题 前言 原文地址&#xff1a;https://blog.dwj601.cn/FrontEnd/Hexo/hexo-enhance-local-search-with-oss/ 考虑到某著名云服务商提供的云服务器在两年的 99 计划后续费价格高达四…

Redis和MongoDB的区别

前言 在项目选型阶段&#xff0c;MongoDB被选中主要是基于其处理大规模数据集的能力&#xff0c;而当时并未深入探讨其他替代方案。此前&#xff0c;Redis被用于管理少量但访问频繁的热数据。目前&#xff0c;项目采用MongoDB存储百万级数据&#xff0c;预计未来数据量将增长至…

[JavaScript] 深入理解流程控制结构

文章目录 1. **if-else 语句**基本语法&#xff1a;示例&#xff1a;扩展&#xff1a;else if 2. **switch-case 语句**基本语法&#xff1a;示例&#xff1a;注意事项&#xff1a; 3. **for 循环**基本语法&#xff1a;示例&#xff1a;扩展&#xff1a;for-in 和 for-of 4. *…

渗透笔记1

第一天 工具&#xff1a;cs cobalt strike 4.9 / msf kali &#xff08;自带 Ubuntu&#xff09; cs cobalt strike 4.9&#xff1a;server-client server部署在云服务器上&#xff0c;client分别在各地&#xff0c;与server相连接&#xff1b;连接上后就可以共享上线主机。…

C#--在多线程中使用任务并行库(TPL)--15

目录 一.任务并行库的概念以及定义 二.主要特性 三.代码使用示例 1.最基础的Parallel.For使用方式 2.使用 ParallelOptions 来控制并行执行 3.Parallel.ForEach的使用(用于处理集合) 4.带有本地变量的并行循环(用于需要累加或统计的场景) 5.结合Task和Parallel的高级示…

与“神”对话:Swift 语言在 2025 中的云霓之望

0. 引子 夜深人静&#xff0c;是一片极度沉醉的黑&#xff0c;这便于我与深沉的 macbook 悄悄隐秘于其中。一股异香袭来&#xff0c;恍惚着&#xff0c;撸码中身心极度疲惫、头脑昏沉的我仿佛感觉到了一束淡淡的微光轻洒在窗边。 我的对面若隐若现逐渐浮现出一个熟悉的身影。他…

WOA-Transformer鲸鱼算法优化编码器时间序列预测(Matlab实现)

WOA-Transformer鲸鱼算法优化编码器时间序列预测&#xff08;Matlab实现&#xff09; 目录 WOA-Transformer鲸鱼算法优化编码器时间序列预测&#xff08;Matlab实现&#xff09;预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.Matlab实现WOA-Transformer鲸鱼算法优化编…

基于SpringBoot和PostGIS的各国及所属机场信息检索及可视化实现

目录 前言 一、空间数据简介 1、全球国家信息表 2、机场信息表 3、国家机场检索实现 二、SpringBoot后台实现 1、模型层实现 2、控制层实现 三、WebGIS可视化实现 1、Leaflet界面实现 2、国家及其机场可视化成果 3、全球机场数量排行榜 四、总结 前言 新春佳节即将…

MLMs之Agent:Phidata的简介、安装和使用方法、案例应用之详细攻略

MLMs之Agent&#xff1a;Phidata的简介、安装和使用方法、案例应用之详细攻略 目录 Phidata简介 Phidata安装和使用方法 1、安装 2、使用方法 (1)、认证 (2)、创建 Agent (3)、运行 Agent (4)、Agent Playground Phidata 案例应用 1、多工具 Agent 2、多模态 Agent …

【机器学习实战入门项目】使用深度学习创建您自己的表情符号

深度学习项目入门——让你更接近数据科学的梦想 表情符号或头像是表示非语言暗示的方式。这些暗示已成为在线聊天、产品评论、品牌情感等的重要组成部分。这也促使数据科学领域越来越多的研究致力于表情驱动的故事讲述。 随着计算机视觉和深度学习的进步&#xff0c;现在可以…