0


Spring Security 权限控制

权限控制介绍

在前面的章节中,已经给大家介绍了Spring Security的很多功能,在这些众多功能中,我们知道其核心功能其实就是 认证+授权。在前面我们分别基于内存模型、基于默认的数据库模型、基于自定义数据库模型实现了认证和授权功能,但是不管哪种方式,我们对某个接口的拦截限制,都是通过编写一个SecurityConfig配置类,在该类的configure(HttpSecurity http)方法中,通过http.authorizeRequests().antMatchers("/admin/**")...这样的代码进行的权限控制。

这种权限控制方法虽然也可以实现对某些接口的拦截或放行,但是不够灵活,其实Spring Security对接口的拦截或放行的写法,还有另外的方式,接下来请跟我学习一下吧!

权限控制方式

在Spring Security 中,我们既可以使用 Spring Security 提供的默认方式进行授权,也可以进行自定义授权,总之在Spring Security中权限控制的实现方式是比较灵活多样的。在Spring Security 中,对接口的拦截或放行,有四种常见的权限控制方式:

  • 利用Ant表达式实现权限控制;
  • 利用授权注解结合SpEl表达式实现权限控制;
  • 利用过滤器注解实现权限控制;
  • 利用动态权限实现权限控制。

利用Ant表达式实现权限控制

利用Ant表达式的权限控制方式,是我们之前一直在使用的权限控制方式,在进行代码实现之前,我先对这种方式的底层实现进行简单分析。

1. Spring Security中的权限控制方法

在Spring Security中,有一个SecurityExpressionOperations接口,在该接口中定义了一系列的方法,用于用户权限的设置,如下图:

SecurityExpressionOperations接口中的这些方法作用如下图所示:

isAuthenticated

判断是否认证成功

isRememberMe

判断是否通过记住我登录的

isFullyAuthenticated

判断是否用户名、密码登录

principle

当前用户名

authentication

从SecurityContext中提取出来的用户对象

2. Spring Security中的权限控制粒度

这个接口有一个SecurityExpressionRoot子类,该类提供了基于表达式的权限控制实现方式。而这个SecurityExpressionRoot 又有两个实现子类,分别用于实现 URL Web接口粒度的权限控制和 方法粒度的权限控制,如下图所示:

3. 代码实现

从上面的小节中,我们知道在Spring Security中,支持2种粒度的权限控制,即URL Web接口粒度 和方法粒度,而我们这里所谓的 Ant表达式授权控制方式,就是通过Ant表达式来控制 URL 接口的访问权限。

那么如果我们需要对URL接口粒度进行权限控制,按如下代码即可实现:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/admin/**")
            .hasRole("ADMIN")
            .antMatchers("/user/**")
            .hasRole("USER")
            .antMatchers("/visitor/**")
            .permitAll()
            .anyRequest()
            .authenticated()
            .and()
            .formLogin()
            .permitAll()
            .and()
            //对跨域请求伪造进行防护---->csrf:利用用户带有登录状态的cookie进行攻击的手段
            .csrf()
            .disable();
}

以上代码中,/admin/**** 格式的路径需要 admin 角色才可以访问,/user/** 格式的路径需要 user 角色才可以访问,/visitor/**** 格式的路径可以直接访问,其他接口路径则需要登录后才能访问。

利用授权注解结合SpEl表达式实现权限控制

1. 授权注解

除了可以使用上面的Ant表达式进行授权实现,我们也可以在方法上添加授权注解来权限控制,常用的授权注解有3个:

  • @PreAuthorize:方法执行前进行权限检查;
  • @PostAuthorize:方法执行后进行权限检查;
  • @Secured:类似于 @PreAuthorize。

2. 代码实现

要想利用以上3个授权注解进行权限控制,我们首先需要利用@EnableGlobalMethodSecurity注解开启授权注解功能,代码如下:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig  {
    ...
    ...
}

然后在具体的接口方法上利用授权注解进行权限控制,代码如下:

@RestController
public class UserController {
 
    @Secured({"ROLE_USER"})
    //@PreAuthorize("principal.username.equals('user')")
    //@PreAuthorize("@ph.check('USERx')") ph:是Spring的bean
    @PreAuthorize("permitAll()")
    @PreAuthorize("hasRole('USER')")
    
    @GetMapping("/user/hello")
    public String helloUser() {
 
        return "hello, user";
    }
 
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/admin/hello")
    public String helloAdmin() {
 
        return "hello, admin";
    }
 
    @PreAuthorize("#age>100")
    @GetMapping("/age")
    public String getAge(@RequestParam("age") Integer age) {
 
        return String.valueOf(age);
    }
 
    @GetMapping("/visitor/hello")
    public String helloVisitor() {
 
        return "hello, visitor";
    }
 
}
package com.boot.handler;

import org.springframework.stereotype.Component;

@Component("ph")
public class PermHandler {

    public boolean check(String role){
        return role.equals("USER");
    }

}

可以看出,这种写法明显比利用Ant表达式进行权限控制更灵活方便,所以开发时这种写法很常用。

利用过滤器注解实现数据权限控制

1. 过滤器注解简介

在Spring Security中还提供了另外的两个注解,即@PreFilter和@PostFilter,这两个注解可以对集合类型的参数或返回值进行过滤。使用@PreFilter和@PostFilter时,Spring Security将移除对应表达式结果为false的元素。

2. @PostFilter的用法

@PostFilter注解主要是用于对集合类型的返回值进行过滤,filterObject是@PostFilter中的一个内置表达式,表示集合中的元素对象。

package com.boot.controller;

import com.boot.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PostFilter;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Slf4j
@RestController
public class FilterController {

    /**
     * 只返回结果中id为偶数的user元素。
     * filterObject是@PreFilter和@PostFilter中的一个内置表达式,表示集合中的当前对象。
     */
//    @PreAuthorize("hasRole('ADMIN')")
    //@PostFilter("filterObject.userName == authentication.principal.username")
//    @PostFilter("filterObject.id%2==0")
//    @PostFilter("hasRole('USER') and filterObject.userName == authentication.principal.username")
    
    @GetMapping("/users")
    public List<User> getAllUser() {
        //注意这里配合postFilter过滤 如果直接使用Arrays的数组 则会抛出异常:java.lang.UnsupportedOperationException
        List<User> userList = new ArrayList<>(Arrays.asList(
                new User(1L,"admin","123456"),
                new User(2L,"test","123456"),
                new User(3L,"王五","123456"),
                new User(4L,"赵六","123456"),
                new User(5L,"小王","123456"),
                new User(6L,"小张","123456")
        ));
        return userList;
    }

}

3. @PreFilter的用法

使用@PreFilter也可以对集合类型的参数进行过滤,当@PreFilter标注的方法内拥有多个集合类型的参数时,可以通过@PreFilter的filterTarget属性来指定当前是针对哪个参数进行过滤的;而filterObject是@PreFilter中的一个内置表达式,表示集合中的元素对象。

为了方便测试,我们在Service层中进行过滤操作,然后在Controller层中进行调用。

FilterService类中的方法定义:

package com.boot.service;

import com.boot.entity.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
public class FilterService {

    /**
     * 当@PreFilter标注的方法内拥有多个集合类型的参数时,
     * 可以通过@PreFilter的filterTarget属性来指定当前是针对哪个参数进行过滤的。
     */
    @PreFilter(filterTarget = "ids", value = "filterObject%2==0")
    public List<Integer> doFilter(List<Integer> ids, List<User> users) {
        log.warn("ids=" + ids.toString());
        log.warn("users=" + users.toString());
        return ids;
    }

}

在Controller中定义一个测试接口:

    @GetMapping("/users2")
    public List<Integer> getUserInfos() {
        List<Integer> ids = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            ids.add(i);
        }

        List<User> userList = new ArrayList<>(Arrays.asList(
                new User(1L,"admin","123456"),
                new User(2L,"test","123456"),
                new User(3L,"王五","123456"),
                new User(4L,"赵六","123456"),
                new User(5L,"小王","123456"),
                new User(6L,"小张","123456")
        ));

        return filterService.doFilter(ids, userList);
    }

我们启动浏览器进行测试,可以看到测试接口中只返回id为偶数的元素。

[0,2,4,6,8]

利用过滤器实现动态权限控制

SpringSecurity从5.5之后动态权限控制方式以及改变。

5.5之前需要实现接口:

  • FilterInvocationSecurityMetadataSource
  • AccessDecisionManager

5.5之后,看:AuthorizationFilter中,实现接口:AuthorizationManager

如果没有权限,抛出自定义异常

利用过滤器动态控制权限

在系统中配置角色、权限,之后通过过滤器来动态控制

package com.boot.auth;

import com.boot.exception.MyAccessDeniedException;
import com.boot.mapper.RoleMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Component
public class MyAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    @Autowired
    RoleMapper roleMapper;

    @Override
    public void verify(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
        AuthorizationManager.super.verify(authentication, requestAuthorizationContext);
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
        // 我们可以获取原始request对象
        HttpServletRequest request = requestAuthorizationContext.getRequest();

        String requestURI = request.getRequestURI();

        // 根据这些信息 和业务写逻辑即可 最终决定是否授权 isGranted
        boolean isGranted = true;

        if(requestURI.equals("/login") || requestURI.equals("/logout")){
            return new AuthorizationDecision(isGranted);
        }

        // 当前用户的权限信息 比如角色
        Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
        List<String> userCodes = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        //当前被访问的URL:需要哪些权限才能访问
        List<String> codes = roleMapper.findCodesByUrl(requestURI);

        if(codes == null || codes.size()==0 || !contains(codes,userCodes)){
            throw new MyAccessDeniedException("没有权限");
        }

        return new AuthorizationDecision(isGranted);
    }

    public boolean contains(List<String> srcs,List<String> dests){
        for (String src : srcs) {
            for (String dest : dests) {
                if(src.equals(dest)){
                    return true;
                }
            }
        }
        return false;
    }

}
package com.boot.config;

import com.boot.auth.MyAuthorizationManager;
import com.boot.exception.MyAccessDeniedException;
import jakarta.annotation.Resource;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Resource
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Resource
    private MyAuthorizationManager authorizationManager;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorizeHttpRequests->
                authorizeHttpRequests
                        .requestMatchers("/login").permitAll()
                        .anyRequest().access(authorizationManager)

        );

        http.formLogin(formLogin->
                formLogin
                        .loginPage("/login").permitAll()
                        .loginProcessingUrl("/login")
                        .defaultSuccessUrl("/index")
                        .failureHandler(authenticationFailureHandler)
        );

        http.exceptionHandling(e->e.accessDeniedHandler((request, response, accessDeniedException) -> {
            response.setContentType("text/html;charset=UTF-8");
            if(accessDeniedException instanceof MyAccessDeniedException){
                response.getWriter().write("没有权限异常:"+accessDeniedException.getMessage());
            }else{
                response.getWriter().write("异常:"+accessDeniedException.getMessage());
            }

        }));

        http.csrf(withDefaults());

        http.logout(logout->logout.invalidateHttpSession(true));

        return http.build();
    }

    /**
     * PasswordEncoder:加密编码
     * 实际开发中开发环境一般是明文加密 在生产环境中是密文加密 也就可以可以配置多种加密方式
     *
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        //明文加密
        return NoOpPasswordEncoder.getInstance();
    }

}
标签: spring java 后端

本文转载自: https://blog.csdn.net/zhou9898/article/details/142343398
版权归原作者 黑石课堂 所有, 如有侵权,请联系我们删除。

“Spring Security 权限控制”的评论:

还没有评论