【Spring Security】 表达式授权案例
【Spring Security】 表达式授权案例
Metadata
title: 【Spring Security】 表达式授权案例
date: 2023-02-02 14:17
tags:
- 行动阶段/完成
- 主题场景/组件
- 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
- 细化主题/Module/SpringSecurity
categories:
- SpringSecurity
keywords:
- SpringSecurity
description: 【Spring Security】 表达式授权案例
【Spring Security】 表达式授权案例
1. 添加查询角色和权限接口
既然是基于角色和权限进行授权,那么首先我们登陆的时候就需要查出用户的角色和权限。
先添加一个查询用户信息,包含角色列表 和权限列表的接口。
PO 类:
@Data
public class UserInfoDO implements Serializable {
private static final long serialVersionUID = 1L;
private Long userId;
private String userName;
private String password;
private String loginName;
private Integer gender;
private String phone;
private String address;
private Integer organizationId;
private Boolean state;
private String email;
// 权限集合
List<Permission> permissionList;
// 角色集合
List<Role> roleList;
}
mapper 接口:
public interface UserMapper extends BaseMapper<User> {
UserInfoDO getUserInfoByUserName(String username);
}
mapper.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="org.pearl.springbootsecurity.demo.dao.UserMapper">
<select id="getUserInfoByUserName" resultMap="userInfo">
SELECT
*
FROM
user u
LEFT JOIN user_role ur ON u.user_id = ur.user_id
LEFT JOIN role r ON ur.role_id = r.role_id
LEFT JOIN role_permission rp ON r.role_id = rp.role_id
LEFT JOIN permission p ON rp.permission_id = p.permission_id
WHERE
u.user_name = #{username}
</select>
<!--用户信息映射关系-->
<resultMap id="userInfo" type="org.pearl.springbootsecurity.demo.pojo.UserInfoDO">
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="password" column="password"/>
<result property="loginName" column="login_name"/>
<result property="gender" column="gender"/>
<result property="phone" column="phone"/>
<result property="email" column="email"/>
<!--角色-->
<collection property="roleList" ofType="org.pearl.springbootsecurity.demo.entity.Role">
<id column="role_id" property="roleId"/>
<result column="role_name" property="roleName"/>
</collection>
<!--权限-->
<collection property="permissionList" ofType="org.pearl.springbootsecurity.demo.entity.Permission">
<id column="permission_id" property="permissionId"/>
<result column="code" property="code"/>
<result column="desc" property="desc"/>
</collection>
</resultMap>
</mapper>
UserServiceImpl:
@Override
public UserInfoDO getUserInfoByName(String username) {
return userMapper.getUserInfoByUserName(username);
}
2. 改造 UserDetailsService
之前我们在实现 UserDetailsService 接口时,有这么一行代码:
// 2. 设置权限集合,后续需要数据库查询
List<GrantedAuthority> authorityList =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
在构建 UserDetails 时,封装了一个集合,这个集合就表示了当前用户的权限。
Collection<? extends GrantedAuthority> getAuthorities();
commaSeparatedStringToAuthorityList 方法,是将逗号分隔的权限字符串,转为 GrantedAuthority 集合,每个 GrantedAuthority 就代表一个权限。那我们查询出的角色和权限值集合,就需要转为 GrantedAuthority 集合。
改造 MyUserDetailsService :
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserService userService;
/**
* @param username 用户名
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 数据库查询用户
/*User user = userService.getUserByName(username);*/
UserInfoDO userInfoDO = userService.getUserInfoByName(username);
if (userInfoDO == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
// 2. 设置权限集合,后续需要数据库查询
/* List<GrantedAuthority> authorityList =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");*/
// 2. 角色权限集合转为 List<GrantedAuthority>
List<Role> roleList = userInfoDO.getRoleList();
List<Permission> permissionList = userInfoDO.getPermissionList();
List<GrantedAuthority> authorityList=new ArrayList<>();
roleList.forEach(e->{
String roleCode = e.getRoleCode();
if (StringUtils.isNotBlank(roleCode)){
SimpleGrantedAuthority simpleGrantedAuthority=new SimpleGrantedAuthority(roleCode);
authorityList.add(simpleGrantedAuthority);
}
});
permissionList.forEach(e->{
String code = e.getCode();
if (StringUtils.isNotBlank(code)){
SimpleGrantedAuthority simpleGrantedAuthority=new SimpleGrantedAuthority(code);
authorityList.add(simpleGrantedAuthority);
}
});
// 3. 返回自定义的用户信息
MyUser myUser = new MyUser(userInfoDO.getUserName(), userInfoDO.getPassword(), authorityList);
// 设置自定义扩展信息
myUser.setDeptId(userInfoDO.getOrganizationId());
myUser.setLoginTime(new Date());
return myUser;
}
}
改造完成后,我们重新登录并查看 Authentication 信息,可以看到角色和权限值的信息已经添加到当前 SecurityContext 中了。
3. @EnableGlobalMethodSecurity
要开启注解授权,还需要开启 EnableGlobalMethodSecurity 注解。
EnableGlobalMethodSecurity 源码:
// 开启全局方法级别权限控制,类似于XML :global-method-security
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ GlobalMethodSecuritySelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableGlobalMethodSecurity {
// 开启 @PreAuthorize 和 @PostAuthorize注解支持,默认false
boolean prePostEnabled() default false;
// 确定是否应启用 Spring Security 的 {@link @Secured}注解
boolean securedEnabled() default false;
// 确定是否应启用 JSR-250 注解。默认为false。
boolean jsr250Enabled() default false;
// 指示是否要创建基于子类 (CGLIB) 的代理,而不是基于标准 Java 接口的代理默认值为 {@code false}
boolean proxyTargetClass() default false;
// 建议模式
AdviceMode mode() default AdviceMode.PROXY;
// 指示在特定连接点应用多个建议时安全顾问的执行顺序
int order() default Ordered.LOWEST_PRECEDENCE;
}
我们在 MyWebSecurityConfiguration 加上此注解:
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
4 . @Secured
@Secured 用于判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 “ROLE_“。
我们在 controller 方法上添加 @Secured 注解,并添加多个角色值,表示当前登陆用户必须有其中一个角色,否则无法访问。
@GetMapping("/test")
@Secured({"ROLE_root","ROLE_manager"})
public Object test() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
MyUser principal = (MyUser)authentication.getPrincipal();
return principal;
}
登录后访问这个接口时,发现报错 403,而且显示我们没有定义这个 403 的错误页面,报错 403 实际就是因为没有权限访问,我们可以自定义这个页面。
当我们在数据库中给这个用户添加 ROLE_manager 角色后,就能访问这个接口了。
5. @PreAuthorize 和 @PostAuthorize
@PreAuthorize 和 @PostAuthorize 权限注解,可以作用于方法或类,可以结合 EL 表达式进行访问控制,区别是 @PreAuthorize 是方法执行前,@PostAuthorize 是执行后,当表达式结果为 true 时,才能进入。
常用表达式示例:
// 表示有ROLE_ROOT角色才能访问
@PreAuthorize("hasRole('ROLE_ROOT')")
// 表示有ROLE_root或者ROLE_manager角色即可访问
@PreAuthorize("hasAnyRole('ROLE_root','ROLE_manager')")
// 表示有add:user这个权限值即可访问,不区分角色或者权限
@PreAuthorize("hasAuthority('add:user')")
// 有其中一个权限值即可访问
@PreAuthorize("hasAnyAuthority('add:user','user:update')")
// 表示只要登录都可以访问
@PreAuthorize("permitAll()")
// 拒绝所有访问
@PreAuthorize("denyAll()")
6. @PreFilter 和 @PostFilter
使用 @PreFilter 和 @PostFilter 可以对集合类型的参数或返回值进行过滤。
使用 @PostFilter 注解时,Spring Security 会遍历返回的集合或映射,并删除提供的表达式为 false 的任何元素。使用 @PreFilter,尽管这是一个不太常见的要求。语法是一样的,但是如果有多个参数是集合类型,那么您必须使用 filterTarget 此注释的属性按名称选择一个。
也就是可以过滤参数或者返回值,但是一般也不会这么做,效率比较低。
比如以下案例,会过滤掉返回值中 userName 不为 test 的元素。
@PostFilter(value = "filterObject.userName == 'test'")
public Object test() {
List<User> userList = userService.list();
return userList;
}
所以最后查询,虽然数据库查询了所有数据,但是只输出了 userName 为 test 的单条数据。