程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • SpringSecurity

    • Spring Security是什么
    • 认证与授权的基本概念
    • Spring Security 的默认配置
    • SpringSecurity的默认登录页
    • Spring Security 的 Filter 机制
    • HttpSecurity 与自定义登录页面
    • Spring Security 的核心组件与扩展
    • Spring Security 中的用户认证与角色管理
    • Spring Security 的授权机制与安全表达式
    • Security 的 Session 与 Token 管理
    • Spring Security 集成第三方登录
    • Spring Security 集成 QQ 登录与 JWT 认证
    • Spring Security 登录认证源码
    • Spring Security - JWT认证实战
    • Spring Security - JWT授权实战
      • 1. 权限控制的基本概念和作用
      • 2. Spring Security 授权流程
      • 3. 授权实现
        • 3.1 限制访问资源所需权限
        • 3.2 封装权限信息
        • 1. 修改UserDetails实现类
        • 2. 修改UserDetailsService实现类
        • 3.3 自定义JWT过滤器
        • 3.4 测试授权访问
      • 4. 从数据库查询权限信息
        • 4.1 RBAC权限模型
        • 4.2 数据库表结构
        • sys_menu:菜单权限表
        • sys_role:角色信息表
        • sys_role_menu:角色和菜单关联表
        • sys_user_role:用户和角色关联表
        • 分配 管理员 角色
        • 4.3 Java 代码实现
        • 1. 定义`Menu`实体类
        • 2. 定义`MenuMapper`接口
        • 3. 定义`MenuMapper.xml`
        • 4. 实现`UserDetailsService`
        • 5. 更新`HelloController`中的权限控制
        • 4.4 权限查询的思路总结
      • 5. 跨域处理
        • 5.1 配置 Spring Boot 允许跨域
        • 5.2 配置 Spring Security 允许跨域
      • 6. 其它权限校验方法
        • 6.1 hasAuthority 与 hasAnyAuthority
        • 6.2 hasRole 与 hasAnyRole
        • 6.3 自定义权限校验方法
        • 6.4 基于配置的权限控制
        • 6.5 CSRF(跨站请求伪造)
    • Spring Security 异常处理与自定义逻辑
  • SpringSecurity
  • SpringSecurity
scholar
2024-12-29
目录

Spring Security - JWT授权实战

# Spring Security 授权

前言

在 Spring Security 中,权限控制的核心是通过 Authentication 对象和权限信息来判断当前用户是否有权限访问特定资源。这个过程通过 Spring Security 的多个过滤器实现,最终通过 FilterSecurityInterceptor 来做权限校验。


# 1. 权限控制的基本概念和作用

在实际的应用场景中,权限控制就是基于用户角色或其他标识来限定用户访问的功能。例如:

  • 普通学生登录后,只能看到和使用借书、还书等功能。
  • 图书馆管理员登录后,除了借书还书外,还可以进行添加、删除书籍的操作。

权限控制的重要性:

  • 前端:用户界面展示时,仅仅通过前端来控制用户能否访问某些功能是不安全的。如果有人绕过前端,直接通过接口访问,可能会导致权限泄漏。
  • 后端:所有的权限控制应该在后端进行。只有通过后台验证,才能确保安全性。

# 2. Spring Security 授权流程

Spring Security 使用 FilterSecurityInterceptor 来进行权限校验。该过滤器会从 SecurityContextHolder 获取当前用户的 Authentication 对象,从中提取出权限信息。然后,根据配置的访问控制规则,判断当前用户是否有权限访问资源。

在我们的实现中,我们需要:

  1. 存储用户权限:将用户的权限信息存储到 Authentication 对象中。
  2. 使用注解控制权限:通过 @PreAuthorize 或 @Secured 注解限制访问特定资源的权限。
  3. 设置资源权限要求:通过配置来指定访问某个资源所需要的权限。

# 3. 授权实现

实现流程:

  • 权限控制:Spring Security 的权限控制通过 GrantedAuthority 来表示权限信息,并使用注解(如 @PreAuthorize)来限制对资源的访问。
  • 封装权限信息:通过 UserDetails 实现类 LoginUser 来封装用户信息和权限信息,并在自定义过滤器中将其存入 SecurityContextHolder。
  • JWT 认证与权限校验:通过自定义过滤器解析 JWT,将用户信息和权限信息存储到 SecurityContextHolder 中,从而支持后续的权限校验。

# 3.1 限制访问资源所需权限

首先,我们需要启用 Spring Security 的方法级权限控制。通过 @EnableGlobalMethodSecurity(prePostEnabled = true) 启用基于注解的权限控制。












































 
 













@Configuration
@EnableWebSecurity  // 开启Spring Security功能
@EnableGlobalMethodSecurity(prePostEnabled = true)  // 启用基于注解的权限控制
public class SecurityConfig {

    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;  // 注入AuthenticationConfiguration以获取AuthenticationManager

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AccessDeniedHandlerImpl accessDeniedHandler;  // 注入自定义的AccessDeniedHandler

    @Autowired
    AuthenticationEntryPointImpl authenticationEntryPoint;  // 注入自定义的AuthenticationEntryPoint

    // 配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();  // 使用BCrypt加密算法
    }

    // 配置AuthenticationManager
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return authenticationConfiguration.getAuthenticationManager();  // 获取AuthenticationManager
    }

    /**
     * 配置Spring Security的过滤链。
     *
     * @param http 用于构建安全配置的HttpSecurity对象。
     * @return 返回配置好的SecurityFilterChain对象。
     * @throws Exception 如果配置过程中发生错误,则抛出异常。
     */
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())  // 禁用CSRF保护
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  // 设置无状态会话
                .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/user/login").anonymous()  // 放行登录接口,允许匿名访问
                        .requestMatchers("/admin/**").hasAuthority("admin")  // 只有admin角色才能访问/admin/**路径
                        .requestMatchers("/user/**").hasAuthority("user")  // 只有user角色才能访问/user/**路径
                    .anyRequest().authenticated())  // 其他接口需要身份认证
                .cors(cors -> cors.configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues())) // 配置CORS
                // 添加JWT认证过滤器,确保在UsernamePasswordAuthenticationFilter之前执行
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                // 配置异常处理
                .exceptionHandling(exception -> exception
                        .accessDeniedHandler(accessDeniedHandler)// 配置授权失败处理器
                        .authenticationEntryPoint(authenticationEntryPoint));  // 配置认证失败处理器;
//        // 返回配置好的过滤链
        return http.build();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

在上述配置中,我们通过 hasAuthority("role") 来限制访问路径的权限。

方法级权限控制:在方法上面加上 @PreAuthorize 注解






 






 





@RestController
public class HelloController {

    // 只有具有 "user" 权限的用户才能访问
    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('user')")
    public String hello() {
        return "Hello, World!";
    }

    // 只有具有 "admin" 权限的用户才能访问
    @RequestMapping("/admin")
    @PreAuthorize("hasAuthority('admin')")
    public String admin() {
        return "Admin Page";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

通过上述方式,我们可以根据权限信息来控制不同的用户访问不同的资源。


# 3.2 封装权限信息

# 1. 修改UserDetails实现类

  • 为了使 Spring Security 识别用户权限,我们需要在 UserDetails 中封装权限信息。我们通过 LoginUser 类来封装用户信息和权限。
  • 首先修改 LoginUser 类,增加权限信息字段,并实现 getAuthorities 方法,为了方便测试,我们先直接把权限信息写死封装到UserDetails的实现类中。


















 





 
 























 
 
 
 
 
 
 
 
 
 
 






























































/**
 * 登录用户的封装类,实现 Spring Security 的 UserDetails 接口。
 * 用于封装用户信息和权限信息,并提供给 Spring Security 进行身份认证和授权。
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {

    /**
     * 封装用户的基本信息(如用户名、密码等),该对象来自数据库。
     */
    private User user;

    /**
     * 用户的权限信息,通常是一个权限的列表。
     * 这些权限信息通常来自数据库,并将用于控制用户可以访问的资源。
     */
    private List<String> permissions;

    /**
     * 用户权限的 Spring Security 表示形式,存储在 SimpleGrantedAuthority 对象中。
     * 这个字段不参与序列化,以避免冗余数据被存入 Redis。
     */
    @JSONField(serialize = false)   
    private List<SimpleGrantedAuthority> authorities;

    /**
     * 带有用户和权限信息的构造函数。
     * 
     * @param user 用户信息
     * @param permissions 用户的权限信息列表
     */
    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    /**
     * 重载的构造函数(此构造函数为空的,仅用于初始化)。
     */
    public LoginUser(User user) {
    }

    /**
     * 获取用户的权限信息,转换为 Spring Security 所需要的 GrantedAuthority 对象。
     * 
     * @return 用户的权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 如果 authorities 尚未被设置,进行权限信息的转换
        if (authorities == null) {
            // 将字符串类型的权限信息(如 "ROLE_USER")转换为 SimpleGrantedAuthority 对象
            authorities = permissions.stream()
                    .map(SimpleGrantedAuthority::new)  // 将权限字符串映射为 GrantedAuthority 对象
                    .collect(Collectors.toList());  // 收集成一个 List
        }
        return authorities;
    }

    /**
     * 获取用户的密码。
     * 
     * @return 用户的密码
     */
    @Override
    public String getPassword() {
        return user.getPassword();  // 返回用户密码
    }

    /**
     * 获取用户的用户名。
     * 
     * @return 用户名
     */
    @Override
    public String getUsername() {
        return user.getUserName();  // 返回用户名
    }

    /**
     * 判断账户是否已过期。返回 true 表示账户未过期。
     * 
     * @return 是否账户未过期
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;  // 返回 true 表示账户未过期
    }

    /**
     * 判断账户是否已被锁定。返回 true 表示账户未被锁定。
     * 
     * @return 是否账户未锁定
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;  // 返回 true 表示账户未被锁定
    }

    /**
     * 判断凭证(即密码)是否已过期。返回 true 表示凭证未过期。
     * 
     * @return 是否凭证未过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;  // 返回 true 表示凭证未过期
    }

    /**
     * 判断账户是否可用。返回 true 表示账户可用。
     * 
     * @return 是否账户可用
     */
    @Override
    public boolean isEnabled() {
        return true;  // 返回 true 表示账户可用
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

# 2. 修改UserDetailsService实现类

为了方便测试,我们先将权限信息暂时直接写死在代码中(List<String> list = new ArrayList<>(Arrays.asList("test", "admin"));),实际上,这些权限通常会从数据库中根据用户角色动态加载。

@Service // 声明为服务层组件
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper; // 注入 UserMapper,用于与数据库交互

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名查询数据库中的用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName, username);
        User user = userMapper.selectOne(queryWrapper);

        // 如果用户不存在,抛出异常
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名或密码错误");
        }

        // TODO: 如果需要权限信息,可在此处查询并封装
        List<String> list = new ArrayList<>(Arrays.asList("test","admin"));

        // 将用户信息封装为自定义的 UserDetails 实现类
        return new LoginUser(user,list);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 3.3 自定义JWT过滤器

在自定义的 JwtAuthenticationTokenFilter 中,我们需要解析请求中的 token,从 Redis 中获取用户信息,并将用户权限封装到 Authentication 中,然后将其存入 SecurityContextHolder。







































 
 
 
 






@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;  // Redis 缓存

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        // 1. 获取请求头中的 token
        String token = request.getHeader("token");

        if (!StringUtils.hasText(token)) {
            // 如果 token 为空,放行请求
            filterChain.doFilter(request, response);
            return;
        }

        // 2. 解析 token 获取用户 ID
        String subject;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            subject = claims.getSubject();  // 获取用户 ID
        } catch (Exception e) {
            // 如果解析失败,抛出异常
            throw new RuntimeException("token非法");
        }

        // 3. 从 Redis 获取用户信息
        String redisKey = "login:" + subject;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);  // 从 Redis 中获取用户信息

        if (Objects.isNull(loginUser)) {
            // 如果 Redis 中没有该用户信息,抛出异常
            throw new RuntimeException("用户未登录");
        }

        // 4. 将用户信息存入 SecurityContextHolder,设置权限信息
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 5. 放行请求
        filterChain.doFilter(request, response);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

解释:

  1. 解析 Token:从请求头获取 token,解析 JWT 获取用户的 userId。
  2. 从 Redis 获取用户信息:通过用户 ID 查询 Redis 中存储的用户信息。
  3. 封装用户认证信息:创建 UsernamePasswordAuthenticationToken,并将用户的权限信息传入。
  4. 存入 SecurityContextHolder:将认证信息存入 SecurityContextHolder,后续的过滤器可以直接获取并使用该信息。

# 3.4 测试授权访问

访问hello接口失败,该接口需要user权限

image-20241229213831060

访问admin接口成功,该接口需要admin权限

image-20241229214009280

# 4. 从数据库查询权限信息

# 4.1 RBAC权限模型

RBAC (Role-Based Access Control) 权限模型基于用户的角色来管理权限,具有较好的灵活性与可扩展性。其基本思想是将用户与角色关联,角色再与权限关联。用户通过角色获得权限。这样可以集中管理权限并避免直接为用户分配权限时产生的复杂性。

RBAC模型的典型结构如图所示:

image-20240510202313997

# 4.2 数据库表结构

我们通过以下SQL语句建立了4个数据表,用于存储菜单、角色、用户和权限信息。

# sys_menu:菜单权限表

CREATE TABLE `sys_menu` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` varchar(50) NOT NULL COMMENT '菜单名称',
  `parent_id` bigint DEFAULT '0' COMMENT '父菜单ID',
  `order_num` int DEFAULT '0' COMMENT '显示顺序',
  `path` varchar(200) DEFAULT '' COMMENT '路由地址',
  `component` varchar(255) DEFAULT NULL COMMENT '组件路径',
  `is_frame` int DEFAULT '1' COMMENT '是否为外链(0是 1否)',
  `menu_type` char(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
  `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
  `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
  `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT '' COMMENT '备注',
  `del_flag` char(1) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2034 DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# sys_role:角色信息表

CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(30) NOT NULL COMMENT '角色名称',
  `role_key` varchar(100) NOT NULL COMMENT '角色权限字符串',
  `role_sort` int NOT NULL COMMENT '显示顺序',
  `status` char(1) NOT NULL COMMENT '角色状态(0正常 1停用)',
  `del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
  `create_by` bigint DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint DEFAULT NULL COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COMMENT='角色信息表'
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# sys_role_menu:角色和菜单关联表

CREATE TABLE `sys_role_menu` (
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `menu_id` bigint NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色和菜单关联表'
1
2
3
4
5

# sys_user_role:用户和角色关联表

CREATE TABLE `sys_user_role` (
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户和角色关联表'
1
2
3
4
5

# 分配 管理员 角色

我们插入一个用户 user_id = 1,为其分配 管理员 角色,并且这个角色有访问 用户管理 菜单的权限。

-- 插入菜单信息 (例如: 用户管理,确保 perms 字段有值)
INSERT INTO `sys_menu` 
(`id`,`menu_name`, `parent_id`, `order_num`, `path`, `component`, `is_frame`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `del_flag`) 
VALUES 
('1','用户管理', 0, 1, '/user', '', 1, 'M', '0', '0', 'user:view', 'user-icon', 1, NOW(), 1, NOW(), '用户管理菜单', '0');

-- 插入管理员角色
INSERT INTO `sys_role` 
(`id`,`role_name`, `role_key`, `role_sort`, `status`, `del_flag`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) 
VALUES 
('1','管理员', 'admin', 1, '0', '0', 1, NOW(), 1, NOW(), '具有所有权限的管理员角色');`menu_name
menu_name`,

-- 将管理员角色与'用户管理'菜单关联
INSERT INTO `sys_role_menu` 
(`role_id`, `menu_id`) 
VALUES 
(1, 1); -- 管理员角色与'用户管理'菜单关联

-- 用户ID为1,给该用户分配管理员角色
INSERT INTO `sys_user_role` 
(`user_id`, `role_id`) 
VALUES 
(1, 1); -- 用户1分配管理员角色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

说明

  1. sys_menu:插入了一个菜单项 用户管理,它的权限标识是 user:view,图标是 user-icon。
  2. sys_role:插入了一个角色 管理员,它的权限标识是 admin,表示管理员角色可以访问所有菜单。
  3. sys_role_menu:将 管理员 角色与 用户管理 菜单进行关联。
  4. sys_user_role:为 user_id = 1 的用户分配了 管理员 角色。

# 4.3 Java 代码实现

# 1. 定义Menu实体类

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sys_menu")
public class Menu implements Serializable {
    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Long id; // 菜单ID
    private String menuName; // 菜单名称
    private Long parentId; // 父菜单ID
    private Integer orderNum; // 显示顺序
    private String path; // 路由地址
    private String component; // 组件路径
    private Integer isFrame; // 是否为外链(0是 1否)
    private String menuType; // 菜单类型(M目录 C菜单 F按钮)
    private String visible; // 菜单状态(0显示 1隐藏)
    private String status; // 菜单状态(0正常 1停用)
    private String perms; // 权限标识
    private String icon; // 菜单图标
    private Long createBy; // 创建者
    private LocalDateTime createTime; // 创建时间
    private Long updateBy; // 更新者
    private LocalDateTime updateTime; // 更新时间
    private String remark; // 备注
    private String delFlag; // 删除标志
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 2. 定义MenuMapper接口

public interface MenuMapper extends BaseMapper<Menu> {
    // 根据用户ID查询该用户的所有权限
    List<String> selectPermsByUserId(Long userId);
}
1
2
3
4

# 3. 定义MenuMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.scholar.securitytest.mapper.MenuMapper">

    <!-- 查询用户拥有的权限 -->
    <select id="selectPermsByUserId" resultType="java.lang.String">
        select
            distinct perms
        from sys_user_role sur
                 left join sys_role sr on sur.role_id = sr.id
                 left join sys_role_menu srm on sur.role_id = srm.role_id
                 left join sys_menu sm on srm.menu_id = sm.id
        where user_id = #{userId} and sr.status = 0 and sm.status = 0
    </select>

</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 4. 实现UserDetailsService

在UserDetailsServiceImpl中,我们通过menuMapper查询当前用户的权限,并封装到LoginUser对象中返回。




















 
 






@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    
    @Autowired
    private MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().eq(User::getUserName, username);
        User user = userMapper.selectOne(wrapper);

        // 如果没有该用户就抛出异常
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名或密码错误");
        }

        // 查询用户的权限信息
        List<String> permissions = menuMapper.selectPermsByUserId(user.getId());

        // 将用户信息和权限信息封装到LoginUser中
        return new LoginUser(user, permissions);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 5. 更新HelloController中的权限控制

此处需要注意修改@PreAuthorize注解中的权限信息, 之前写的是@PreAuthorize("hasAuthority('user')") ,要改为数据库中对应有的权限;





 





@RestController
public class HelloController {

    @PreAuthorize("hasAuthority('user:view')") // 这里需要修改为数据中的权限标识
    @GetMapping("/sayHello")
    public String sayHello() {
        return "Hello, World!";
    }
}
1
2
3
4
5
6
7
8
9

测试/sayHello接口是否能成功访问:

image-20241229235636404

# 4.4 权限查询的思路总结

  1. 角色与权限关联:我们通过sys_user_role表确定用户与角色的关联,然后通过sys_role_menu表查询角色与菜单的关联,最终从sys_menu表获取菜单的权限标识。
  2. 动态权限加载:在用户登录时,使用MenuMapper查询该用户所有角色的权限,并封装到LoginUser对象中,在后续的业务处理过程中根据用户的权限标识进行权限验证。
  3. 数据库和代码映射:数据库中的perms字段即为权限标识,代码中通过@PreAuthorize注解进行权限控制时,直接使用该权限标识。

# 5. 跨域处理

在前后端分离的项目中,前端和后端通常是运行在不同的域名或端口上,这就会引发跨域问题。浏览器基于安全考虑,默认情况下会限制跨域请求。因此,我们需要配置允许跨域访问。

# 5.1 配置 Spring Boot 允许跨域

在 Spring Boot 中,我们可以通过配置 WebMvcConfigurer 来允许跨域请求。

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 跨域配置类,用于设置跨域请求的相关配置。
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    /**
     * 配置允许跨域访问的路径和设置。
     * 
     * @param registry 跨域注册器,用于添加跨域配置。
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 配置允许跨域的路径
        registry.addMapping("/**")  // 允许所有路径都可以跨域
                .allowedOriginPatterns("*")  // 允许所有来源的请求
                .allowCredentials(true)  // 允许携带凭证(如cookie)
                .allowedMethods("GET", "POST", "DELETE", "PUT")  // 允许的HTTP方法
                .allowedHeaders("*")  // 允许的请求头
                .maxAge(3600);  // 跨域请求的缓存时间,单位为秒,默认3600秒
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 5.2 配置 Spring Security 允许跨域

Spring Security 默认会保护所有的资源,任何通过 Spring Security 保护的请求都需要进行身份认证和授权。如果你希望在启用 Spring Security 的情况下也能处理跨域请求,需要在 Spring Security 的配置中启用跨域。































































 






import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;

/**
 * Spring Security 配置类,用于设置跨域和安全相关配置。
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // JWT认证过滤器,假设你已经定义好 JWT 认证过滤器
    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    // 异常处理器
    private final AccessDeniedHandler accessDeniedHandler;
    private final AuthenticationEntryPoint authenticationEntryPoint;

    public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, 
                          AccessDeniedHandler accessDeniedHandler,
                          AuthenticationEntryPoint authenticationEntryPoint) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
        this.accessDeniedHandler = accessDeniedHandler;
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    /**
     * 配置 Spring Security 的过滤链。
     *
     * @param http HttpSecurity 对象,用于配置安全相关设置。
     * @return 配置好的 SecurityFilterChain 对象。
     * @throws Exception 如果配置过程中发生错误,则抛出异常。
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 添加 JWT 认证过滤器(确保在 UsernamePasswordAuthenticationFilter 之前执行)
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                
                // 禁用 CSRF 防护(对于无状态认证的 API 是必要的)
                .csrf(csrf -> csrf.disable())
                
                // 设置会话管理策略为无状态(无会话的方式)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                
                // 配置授权规则:登录接口不需要身份验证,其他接口需要认证
                .authorizeRequests(auth -> auth
                        .antMatchers("/user/login").anonymous()  // 允许匿名访问登录接口
                        .anyRequest().authenticated())  // 其他请求需要认证
                
                // 配置异常处理器:无权访问的请求将调用 accessDeniedHandler,未认证的请求将调用 authenticationEntryPoint
                .exceptionHandling(exception -> exception
                        .accessDeniedHandler(accessDeniedHandler)
                        .authenticationEntryPoint(authenticationEntryPoint))
                
                // 启用跨域支持
                .cors();  // 启用跨域访问

        // 构建并返回配置好的 SecurityFilterChain
        return http.build();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

# 6. 其它权限校验方法

Spring Security 提供了多种权限校验方法,例如 hasAuthority、hasAnyAuthority、hasRole 和 hasAnyRole 等。通过这些方法,可以对访问的资源进行灵活的权限控制。理解这些方法的原理非常重要,理解了 hasAuthority 后,其他方法的理解也会更容易。

# 6.1 hasAuthority 与 hasAnyAuthority

  • hasAuthority: 用来判断当前用户是否具有指定的权限。权限是通过用户的 Authentication 对象中的 GrantedAuthority 来验证的。
  • hasAnyAuthority: 用来判断当前用户是否有指定权限中的任意一个权限。

示例:hasAuthority 使用

@PreAuthorize("hasAuthority('admin')")  // 判断用户是否有 "admin" 权限
public String hello() {
    return "hello";
}
1
2
3
4

# 6.2 hasRole 与 hasAnyRole

  • hasRole: 用来判断当前用户是否具有指定的角色。角色会自动加上 ROLE_ 前缀,因此你传入的角色名需要加上 ROLE_ 前缀。
  • hasAnyRole: 用来判断当前用户是否有指定角色中的任意一个角色。

示例:hasRole 使用

@PreAuthorize("hasRole('ROLE_system:dept:list')")  // 判断用户是否有角色 "ROLE_system:dept:list"
public String hello() {
    return "hello";
}
1
2
3
4

示例:hasAnyRole 使用

@PreAuthorize("hasAnyRole('admin','system:dept:list')")  // 判断用户是否有 "admin" 或 "system:dept:list" 角色中的任意一个
public String hello() {
    return "hello";
}
1
2
3
4

# 6.3 自定义权限校验方法

除了使用 Spring Security 提供的默认方法外,我们还可以自定义权限校验方法。自定义的权限校验方法可以根据实际业务需求进行灵活配置。

1. 自定义权限校验方法

我们可以创建一个类,在该类中定义自定义的权限校验方法,并在 @PreAuthorize 注解中使用。

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.List;

/**
 * 自定义权限校验类,提供自定义权限校验方法。
 */
@Component("ex")
public class SGExpressionRoot {

    /**
     * 自定义权限校验方法
     *
     * @param authority 权限字符串,例如 "system:dept:list"
     * @return 如果当前用户拥有该权限,返回 true,否则返回 false
     */
    public boolean hasAuthority(String authority) {
        // 获取当前用户的认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 获取登录用户信息(假设为 LoginUser 对象)
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 获取用户的权限列表
        List<String> permissions = loginUser.getPermissions();
        // 判断权限列表中是否包含传入的权限
        return permissions.contains(authority);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

2. 在 @PreAuthorize 中使用自定义校验方法

在 @PreAuthorize 注解中使用自定义的权限校验方法,可以通过 @ex 引用 SGExpressionRoot 类中的方法。

@RequestMapping("/hello")
@PreAuthorize("@ex.hasAuthority('system:dept:list')")  // 调用自定义权限校验方法
public String hello() {
    return "hello";
}
1
2
3
4
5

# 6.4 基于配置的权限控制

除了在方法级别使用 @PreAuthorize 注解进行权限控制外,还可以在 Spring Security 配置类中进行全局的权限控制。这样,我们可以在一个地方集中管理所有资源的权限访问规则。

在 Spring Security 配置类中,可以使用 .authorizeRequests() 来指定不同路径的访问权限。







































 




















import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.http.SessionCreationPolicy;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    private final AuthenticationEntryPoint authenticationEntryPoint;
    private final AccessDeniedHandler accessDeniedHandler;

    public SecurityConfig(JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter, 
                          AuthenticationEntryPoint authenticationEntryPoint,
                          AccessDeniedHandler accessDeniedHandler) {
        this.jwtAuthenticationTokenFilter = jwtAuthenticationTokenFilter;
        this.authenticationEntryPoint = authenticationEntryPoint;
        this.accessDeniedHandler = accessDeniedHandler;
    }

    /**
     * 配置 Spring Security 的权限控制
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 禁用 CSRF 防护(适用于 RESTful API)
            .csrf().disable()
            
            // 设置无状态的会话管理
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            
            // 配置 URL 访问权限
            .and()
            .authorizeRequests()
            // 允许匿名访问 "/user/login" 路径
            .antMatchers("/user/login").anonymous()
            // 只有具有 "system:dept:list" 权限的用户才能访问 "/testCors" 路径
            .antMatchers("/testCors").hasAuthority("system:dept:list222")
            // 其他路径需要认证
            .anyRequest().authenticated()
            
            // 添加 JWT 认证过滤器
            .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
            
            // 配置异常处理器
            .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)  // 认证失败处理器
                .accessDeniedHandler(accessDeniedHandler)  // 授权失败处理器
            
            // 启用跨域访问支持
            .cors();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 6.5 CSRF(跨站请求伪造)

CSRF(Cross-Site Request Forgery)是一种通过伪造用户请求来攻击 web 应用的攻击方式。攻击者伪装成用户发起请求,利用用户的登录状态进行操作。

  • 防范 CSRF:Spring Security 默认启用了 CSRF 防护,通过 csrf_token 机制来防止跨站请求伪造。前端每次发送请求时需要携带 csrf_token,否则请求会被拒绝。
  • 在前后端分离项目中,前端通常不使用 Cookie 存储认证信息,而是使用 Token,因此 CSRF 攻击的风险大大降低。

CSRF 在前后端分离中的影响

由于前后端分离的架构中,Token 通常由前端传递并通过请求头(如 Authorization)携带,因此 CSRF 攻击在这种情况下并不是特别重要。在这种情况下,我们可以禁用 CSRF 防护(如上面代码中的 .csrf().disable()),从而避免 CSRF 相关的问题。

编辑此页 (opens new window)
上次更新: 2025/01/01, 12:41:26
Spring Security - JWT认证实战
Spring Security 异常处理与自定义逻辑

← Spring Security - JWT认证实战 Spring Security 异常处理与自定义逻辑→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式