【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 的单条数据。