自学内容网 自学内容网

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


原文地址:https://blog.csdn.net/2201_75490194/article/details/145242676

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!