Spring Security - JWT授权实战
# Spring Security 授权
前言
在 Spring Security 中,权限控制的核心是通过 Authentication
对象和权限信息来判断当前用户是否有权限访问特定资源。这个过程通过 Spring Security 的多个过滤器实现,最终通过 FilterSecurityInterceptor
来做权限校验。
# 1. 权限控制的基本概念和作用
在实际的应用场景中,权限控制就是基于用户角色或其他标识来限定用户访问的功能。例如:
- 普通学生登录后,只能看到和使用借书、还书等功能。
- 图书馆管理员登录后,除了借书还书外,还可以进行添加、删除书籍的操作。
权限控制的重要性:
- 前端:用户界面展示时,仅仅通过前端来控制用户能否访问某些功能是不安全的。如果有人绕过前端,直接通过接口访问,可能会导致权限泄漏。
- 后端:所有的权限控制应该在后端进行。只有通过后台验证,才能确保安全性。
# 2. Spring Security 授权流程
Spring Security 使用 FilterSecurityInterceptor
来进行权限校验。该过滤器会从 SecurityContextHolder
获取当前用户的 Authentication
对象,从中提取出权限信息。然后,根据配置的访问控制规则,判断当前用户是否有权限访问资源。
在我们的实现中,我们需要:
- 存储用户权限:将用户的权限信息存储到
Authentication
对象中。 - 使用注解控制权限:通过
@PreAuthorize
或@Secured
注解限制访问特定资源的权限。 - 设置资源权限要求:通过配置来指定访问某个资源所需要的权限。
# 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();
}
}
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";
}
}
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 表示账户可用
}
}
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);
}
}
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);
}
}
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
解释:
- 解析 Token:从请求头获取
token
,解析JWT
获取用户的userId
。 - 从 Redis 获取用户信息:通过用户 ID 查询 Redis 中存储的用户信息。
- 封装用户认证信息:创建
UsernamePasswordAuthenticationToken
,并将用户的权限信息传入。 - 存入 SecurityContextHolder:将认证信息存入
SecurityContextHolder
,后续的过滤器可以直接获取并使用该信息。
# 3.4 测试授权访问
访问hello接口失败,该接口需要user权限
访问admin接口成功,该接口需要admin权限
# 4. 从数据库查询权限信息
# 4.1 RBAC权限模型
RBAC (Role-Based Access Control) 权限模型基于用户的角色来管理权限,具有较好的灵活性与可扩展性。其基本思想是将用户与角色关联,角色再与权限关联。用户通过角色获得权限。这样可以集中管理权限并避免直接为用户分配权限时产生的复杂性。
RBAC模型的典型结构如图所示:
# 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='菜单权限表'
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='角色信息表'
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='角色和菜单关联表'
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='用户和角色关联表'
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分配管理员角色
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
说明
sys_menu
:插入了一个菜单项用户管理
,它的权限标识是user:view
,图标是user-icon
。sys_role
:插入了一个角色管理员
,它的权限标识是admin
,表示管理员角色可以访问所有菜单。sys_role_menu
:将管理员
角色与用户管理
菜单进行关联。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; // 删除标志
}
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);
}
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>
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);
}
}
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!";
}
}
2
3
4
5
6
7
8
9
测试/sayHello
接口是否能成功访问:
# 4.4 权限查询的思路总结
- 角色与权限关联:我们通过
sys_user_role
表确定用户与角色的关联,然后通过sys_role_menu
表查询角色与菜单的关联,最终从sys_menu
表获取菜单的权限标识。 - 动态权限加载:在用户登录时,使用
MenuMapper
查询该用户所有角色的权限,并封装到LoginUser
对象中,在后续的业务处理过程中根据用户的权限标识进行权限验证。 - 数据库和代码映射:数据库中的
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秒
}
}
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();
}
}
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";
}
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";
}
2
3
4
示例:hasAnyRole
使用
@PreAuthorize("hasAnyRole('admin','system:dept:list')") // 判断用户是否有 "admin" 或 "system:dept:list" 角色中的任意一个
public String hello() {
return "hello";
}
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);
}
}
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";
}
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();
}
}
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 相关的问题。