写点什么

面试官:你了解 Spring Security 权限控制吗?

  • 2023-03-03
    湖南
  • 本文字数:8981 字

    阅读完需:约 29 分钟

在前面的文章中,所有的接口只需要登录就能访问。并没有对每个接口进行权限限制。 在正式的系统中,一个用户会拥有一个或者多个角色,而不同的角色会拥有不同的接口权限。如果要实现这些功能,需要重写 WebSecurityConfigurerAdapter 中的 configure(HttpSecurity http)。HttpSecurity 用于构建一个安全过滤器链 SecurityFilterChain,可以通过它来进行自定义安全访问策略。


配置如下:

@BeanPasswordEncoder passwordEncoder() {    return NoOpPasswordEncoder.getInstance();}
@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("cxyxj") .password("123").roles("admin", "user") .and() .withUser("security") .password("security").roles("user");}
@Overrideprotected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() //开启配置 .antMatchers("/cxyxj/**").hasRole("admin") //访问/cxyxj/**下的路径,必须具备admin身份 .antMatchers("/security/**").hasRole("user") //访问/security/**下的路径,必须具备user身份 .antMatchers("/permitAll").permitAll() // 访问/permitAll路径,不需要登录 .anyRequest() //其他请求 .authenticated()//验证 表示其他请求只需要登录就能访问 .and() .formLogin(); // 开启表单登陆}
复制代码

上述配置含义如下:

  • antMatchers("/cxyxj/**").hasRole("admin"):表示访问/cxyxj/路径的必须要有 admin 角色。

  • antMatchers("/security/**").hasRole("user"):表示访问/security/路径的必须要有 user 角色。

  • .antMatchers("/permitAll").permitAll():表示访问/permitAll 路径不需要认证

  • .anyRequest().authenticated():表示除了前面定义的 url,其余 url 访问都得认证后才能访问(登录)

  • and:表示结束当前标签,回到上下文 HttpSecurity,开启新一轮的配置

  • formLogin:开启表单登陆


根据上述的配置,我们新增三个接口,代码如下:

@GetMapping("/hello") public String hello(){     return "你好 Spring Security"; }
@GetMapping("/cxyxj/hello")public String cxyxj() { return "cxyxj 你好 Spring Security";}
@GetMapping("/security/hello")public String user() { return "user 你好 Spring Security";}

@GetMapping("/permitAll")public String permitAll() { return "permitAll 你好 Spring Security";}
复制代码

按照我们的配置,permitAll 接口不需要登录就能访问,cxyxj 接口只有 admin 角色才能访问,security 接口 admin 角色和 user 角色都能访问,而 hello 接口登录就能访问!

这里就不演示了,各位可以将上述代码 copy 到本地运行一下。

授权方式

授权的方式包括 web 授权 和 方法授权,web 授权是通过 url 拦截进行授权,方法授权是通过方法拦截进行授权。他们都会使用 AccessDecisionManager 接口进行授权决策。若为 web 授权则拦截器为 FilterSecurityInterceptor;若为方法授权则拦截器为 MethodSecurityInterceptor。如果同时使用 web 授权和方法授权,则先执行 web 授权,再执行方法授权,最后决策都通过,则允许访问资源,否则将禁止访问。 文章开头使用的方式就是 web 授权方式


  • FilterSecurityInterceptor:底层是 Filter。


  • MethodSecurityInterceptor:底层是 AOP。

web 授权

Spring Security 可以通过 http.authorizeRequests() 开启对 web 请求进行授权保护。

http.formLogin();http.authorizeRequests()        .antMatchers("/permitAll").permitAll() // 访问/permitAll路径,不需要认证        .anyRequest()   //其他请求        .authenticated(); //需要认证才能访问
复制代码
url 匹配


antMatchers()


方法定义如下:

antMatchers(String... antPatterns) {}
复制代码

参数是可变长参数,每个参数是一个 ant 表达式,用于匹配 URL 规则。


ANT 通配符有三种:


// 访问 /cxyxj/** 路径,任意目录下 .js 文件 可以直接访问.antMatchers("/cxyxj/**","/**/*.js").permitAll()
复制代码

使用 antMatchers 方法需要注意配置规则的顺序,配置顺序会影响授权的效果,越是具体的应该放在前面,越是笼统的应该放到后面。 因为 Spring Security 在匹配的时候是按照从上往下的顺序来匹配,一旦匹配成功就不继续匹配了。


如下错误示例:

.antMatchers("/cxyxj/**").hasRole("ADMIN") .antMatchers("/cxyxj/login").permitAll()
复制代码

如上配置会导致访问/cxyxj/login接口时需要拥有ADMIN角色才能访问。


regexMatchers


使用正则表达式进行匹配。

//所有以.js 结尾的文件都被放行 .regexMatchers( ".+[.]js").permitAll()
复制代码

无论是 antMatchers() 还是 regexMatchers() 都具有两个参数的方法,其中第一个参数都是 HttpMethod ,表示请求方式,当设置了 HttpMethod 后表示只有设定的请求方式才执行对应的权限验证。

.antMatchers(HttpMethod.GET,"/cxyxj/hello").permitAll() .regexMatchers(HttpMethod.GET,".+[.]jpg").permitAll()
复制代码

anyRequest


匹配所有的请求。该方法一般会放在最后。结合 authenticated使用,表示所有请求需要认证才能访问。

.anyRequest().authenticated()
复制代码

mvcMatchers


适用于配置了 mvcServletPath 的情况。 mvcServletPath 就是所有的 URL 的统一前缀。在 application.properties 中添加下面内容。

spring.mvc.servlet.path= /role
复制代码
  • 正例


在 Spring Security 的配置类中配置 .servletPath() 是 mvcMatchers() 特有的方法,antMatchers()和 regexMatchers() 没有这个方法。在 servletPath() 中配置了 servlet.path 后,mvcMatchers() 直接写 @RequestMapping()中设置的路径即可。

.mvcMatchers("/cxyxj/**").servletPath("/role").permitAll()
复制代码
  • 反例

.mvcMatchers("/role/cxyxj/**").permitAll()
复制代码

是不是发现使用了 mvcMatchers() 更麻烦了,所以也可以使用 antMatchers()。

.antMatchers("/role/cxyxj/**").permitAll()
复制代码

如果你在项目中还配置了项目根路径。

server.servlet.context-path=/ctx-path
复制代码

SecurityConfig中不需要理会,你只需要修改你的请求接口路径。

  • 配置

.mvcMatchers("/cxyxj/**").servletPath("/role").permitAll()
复制代码
  • 请求接口路径

http://localhost:8080/ctx-path/role/cxyxj/hello
复制代码

调试完成之后请将properties文件的配置进行注释!以下示例代码不使用配置文件。

RequestMatcher 接口

上述的几种匹配方式都是 RequestMatcher 接口的子实现。接口定义了matches方法,方法如果返回 true 表示提供的请求与提供的匹配规则匹配,如果返回的是 false 则不匹配。Spring Security 内置提供了一些 RequestMatcher 实现类:


内置操作

上文只是将请求接口路径与配置的规则进行匹配,那匹配成功之后应该进行什么操作呢?Spring Security 内置了一些控制操作。

  • permitAll() 方法,所有用户可访问。

  • denyAll() 方法,所有用户不可访问。

  • authenticated() 方法,登录用户可访问。

  • anonymous() 方法,匿名用户可访问。

  • rememberMe() 方法,通过 remember me 登录的用户可访问。

  • fullyAuthenticated() 方法,非 remember me 登录的用户可访问。

  • hasIpAddress(String ipaddressExpression) 方法,来自指定 IP 表达式的用户可访问。

  • hasRole(String role) 方法, 拥有指定角色的用户可访问,传入的角色将被自动增加 “ROLE_” 前缀。

  • hasAnyRole(String... roles) 方法,拥有指定任意角色的用户可访问。传入的角色将被自动增加 “ROLE_” 前缀。

  • hasAuthority(String authority) 方法,拥有指定权限( authority )的用户可访问,需要手动传入“ROLE_” 前缀。

  • hasAuthority(String... authorities) 方法,拥有指定任意权限( authority )的用户可访问,需要手动传入“ROLE_” 前缀。

  • access(String attribute) 方法,上面所有方法的底层实现,当 Spring EL 表达式的执行结果为 true 时,可以访问。


使用用法如下:

http.formLogin();http.authorizeRequests()  // 如果用户具备 admin 权限,就允许访问。  .antMatchers("/cxyxj/**").hasAuthority("ROLE_admin")  // 如果用户具备给定权限中某一个,就允许访问。  .antMatchers("/admin/demo").hasAnyAuthority("ROLE_admin", "ROLE_System")  // 如果用户具备 user 权限,就允许访问。注意不需要手动写 ROLE_ 前缀,写了会报错  .antMatchers("/security/**").hasRole("user")  //如果请求是指定的 IP 就允许访问。  .antMatchers("/admin/demo").hasIpAddress("192.168.64.5").anyRequest() //其他请求  .authenticated(); //需要认证才能访问
复制代码

这里单独介绍一下 access(表达式)。表达式的基类是SecurityExpressionRoot,提供了一些通用表达式,如下:

可以使用 access() 实现相同的功能。

.antMatchers("/cxyxj/**").access("hasAuthority('admin')").antMatchers("/permitAll").access("isAnonymous")
复制代码
自定义方法(重要)

虽然内置了很多的表达式,但是在实际项目中,用的很少,而且很有可能不能满足需求。所以需要自定义逻辑的情况。比如判断当前登录用户是否具有访问当前 URL 的权限!

import org.springframework.security.core.Authentication;import org.springframework.security.core.userdetails.User;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;
import javax.servlet.http.HttpServletRequest;import java.util.HashMap;import java.util.HashSet;import java.util.Set;
@Componentpublic class MyAccess {// 只需要登录就能访问的接口地址 private static final Set<String> URL = new HashSet<>(); // 需要区分角色的接口地址 用户名称:接口地址 private static final HashMap<String,Set<String>> URL_MAP = new HashMap(); static { URL.add("/hello");
Set<String> cxyxjSet = new HashSet<>(); cxyxjSet.add("/cxyxj/hello"); cxyxjSet.add("/security/hello"); URL_MAP.put("cxyxj",cxyxjSet); Set<String> securitySet = new HashSet<>(); securitySet.add("/security/hello"); URL_MAP.put("security",securitySet); }
public boolean hasPermit(HttpServletRequest req, Authentication auth){ Object principal = auth.getPrincipal(); String servletPath = req.getRequestURI(); AntPathMatcher matcher = new AntPathMatcher(); // 有一些接口是不需要权限,只要登录就能访问的,比如一些省市区接口 boolean result = URL.stream().anyMatch(url -> matcher.match(url, servletPath)); if(result){ return true; } //这里使用的是定义在内存的用户信息 if(principal instanceof User){ User user = (User) principal; // 可以根据用户id或者用户名从redis中获得用户拥有的菜单权限url String username = user.getUsername(); Set<String> urlSet = URL_MAP.get(username); return urlSet.stream().anyMatch(u -> matcher.match(u, servletPath)); } return false; }
}
复制代码
  • 配置

 @Override   protected void configure(HttpSecurity http) throws Exception {        http.authorizeRequests()  //开启配置                .antMatchers("/permitAll").permitAll()  // 访问/permitAll路径,不需要登录                .anyRequest().access("@myAccess.hasPermit(request,authentication)")                //.anyRequest() //其他请求                //.authenticated()//验证   表示其他请求只需要登录就能访问                .and()                .formLogin();    }
复制代码

使用这种方式,authenticated(),就不需要加了。access 表达式的写法为 @符号 + Bean 名称.方法(参数...)

  • 增加方法

 @GetMapping("/denyAll") public String denyAll() {     return "denyAll 你好 Spring Security"; }
复制代码
  • 测试 登录 cxyxj 用户:可以访问/cxyxj/hello、/security/hello、hello、permitAll 接口。不能访问 denyAll 接口。


登录 security 用户:可以访问/security/hello、hello、permitAll 接口。不能访问 denyAll、/cxyxj/hello 接口。

方法授权

Spring Security在方法的权限控制上支持三种类型的注解,JSR-250 注解、@Secured 注解、支持表达式注解。这三种注解默认都是没有启用的,需要使用@EnableGlobalMethodSecurity来进行启用。

1、JSR250E

@EnableGlobalMethodSecurity 设置 jsr250Enabled 为 true ,就开启了以下三个安全注解:

  • @RolesAllowed:表示访问对应方法时应该具备所指定的角色。示例:@RolesAllowed({"user", "admin"}),表示该方法只要具有"user", "admin"任意一种权限就可以访问。可以省略前缀 ROLE_不写。该注解可以标注在类上,也可以标注在方法上,当标注在类上时表示类中所有方法的执行都需要对应的角色,当标注在方法上表示执行该方法时所需要的角色,当方法和类上都标注了 @RolesAllowed,则方法上的 @RolesAllowed 将覆盖类上的 @RolesAllowed。

  • @PermitAll 表示允许所有的角色进行访问。@PermitAll 可以标注在方法上也可以标注在类上,当标注在方法上时则只对对应方法不进行权限控制,而标注在类上时表示对类里面所有的方法都不进行权限控制。

  • (1)当 @PermitAll 标注在类上,而 @RolesAllowed 标注在方法上时,@RolesAllowed 将覆盖 @PermitAll,即需要 @RolesAllowed 对应的角色才能访问。

  • (2)当 @RolesAllowed 标注在类上,而 @PermitAll 标注在方法上时则对应的方法不进行权限控制。

  • (3)当在类和方法上同时使用了 @PermitAll 和 @RolesAllowed 时,先定义的将发生作用。

  • @DenyAll 表示什么角色都不能访问。@DenyAll 可以标注在方法上也可以标注在类上,当标注在方法上时则只对对应方法进行权限控制,而标注在类上时表示对类里面所有的方法都进行权限控制。


提供测试接口

将原本处于 HelloController 类中的接口注释掉,copy 一份到 HelloController2 类中,并作如下修改:

@GetMapping("/hello")public String hello(){    return "你好 Spring Security";}
@GetMapping("/cxyxj/hello")@RolesAllowed({"admin"})public String cxyxj() { return "cxyxj 你好 Spring Security";}

@GetMapping("/security/hello")@RolesAllowed({"user"})public String user() { return "user 你好 Spring Security";}

@GetMapping("/denyAll")@DenyAllpublic String denyAll() { return "denyAll 你好 Spring Security";}

@GetMapping("/permitAll")@PermitAllpublic String permitAll() { return "permitAll 你好 Spring Security";}
复制代码

SecurityConfig2

@Configuration@EnableGlobalMethodSecurity(jsr250Enabled = true)public class SecurityConfig2 extends WebSecurityConfigurerAdapter {
@Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("cxyxj") .password("123").roles("admin", "user") .and() .withUser("security") .password("security").roles("user"); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin(); }}
复制代码

/cxyxj/hello 接口需要有 admin角色才能访问,/security/hello接口需要有 user角色才能访问,denyAll接口不允许访问,permitAll 接口不需要登录就能访问。由于 hello接口没有做什么处理,所以hello接口不需要登录也能访问。


各位是不是发现 configure(HttpSecurity http)方法中只配置了 http.formLogin()。我为什么没有配置其他的呢?这是因为当配置 .authorizeRequests().anyRequest().authenticated()之后,所有方法需要认证之后才能访问。该配置与 @PermitAll注解发生了冲突。


开篇提到过:若为 web 授权则拦截器为 FilterSecurityInterceptor;若为方法授权则拦截器为MethodSecurityInterceptor。如果同时使用 web 授权和方法授权,则先执行 web 授权,再执行方法授权,最后决策通过,则允许访问资源,否则将禁止访问。当配置.authorizeRequests().anyRequest().authenticated()之后,相当于同时使用 web 授权和方法授权,然后被 web 授权拦截了,所以 @PermitAll 并没有生效。当然实际项目中, @PermitAll一般不会使用,可以使用 web 授权方式进行配置放行。

2、secured

@EnableGlobalMethodSecurity 设置 securedEnabled 为 true ,就开启了以下注解:

  • @Secured 是由 Spring Security 定义的,用来支持方法权限控制的注解。@Secured 专门用于判断是否具有指定角色的,可以写在方法或类上。参数需要手动指定 "ROLE_" 前缀。


SecurityConfig

@EnableGlobalMethodSecurity(jsr250Enabled = true,securedEnabled = true)
复制代码

提供测试接口

@GetMapping("/secured")@Secured({"ROLE_admin"})public String secured() {    return "secured 你好 Spring Security";}
复制代码
3、表达式

Spring Security 中定义了四个支持使用表达式的注解,分别是 @PreAuthorize、@PostAuthorize、 @PreFilter 和 @PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。接下来演示一下使用方式,首先我们先来开启注解,在 @EnableGlobalMethodSecurity 设置 prePostEnabled 为 true 。


SecurityConfig

@EnableGlobalMethodSecurity(jsr250Enabled = true,securedEnabled = true,prePostEnabled = true)
复制代码

@PreAuthorize

最被常用的注解为@PreAuthorize。在方法调用前进行权限检查,结果为 true 则可以执行!可以在类或者方法上进行标注。注解参数与 access方法参数一致,也就是说可以使用内置方法和 Spring-EL 表达式。

// 只有角色为  admin 或者 user才能访问@PreAuthorize("hasRole('admin') or hasRole('user')")@GetMapping("/preAuthorize")public String preAuthorize(){    return "PreAuthorize 你好 Spring Security";}
//Id大于1才能查询// 按名称访问任何方法参数作为表达式变量@PreAuthorize("#id>1")@GetMapping("/findById/{id}")public Integer findById(@PathVariable("id") Integer id) { return id;}
// 限制只能查询自己的信息// principal 值通常是 UserDetails 实例@PreAuthorize("principal.username.equals(#username)")@GetMapping("/findByName/{username}")public String findByName(@PathVariable("username") String username) { return username;}//限制只能新增用户名称为abc的用户@PreAuthorize("#username.equals('abc')")@GetMapping("/add/{username}")public String add(@PathVariable("username") String username) { return username;}
复制代码

使用以下接口测试:

http://localhost:8080/preAuthorize
http://localhost:8080/findById/1http://localhost:8080/findById/2
http://localhost:8080/findByName/cxyxjhttp://localhost:8080/findByName/security
http://localhost:8080/add/securityhttp://localhost:8080/add/abc
复制代码

@PostAuthorize

这个注解使用的很少,当然可能某些需求需要使用。比如需要校验该方法的返回值,这可以使用 @PostAuthorize 注解来实现。要访问方法的返回值,请使用内置表达式 returnObject。

@PostAuthorize("returnObject.id%2==0")    @GetMapping("/find/{id}")    public SysUser find(@PathVariable("id") Integer id) {        SysUser sysUser = new SysUser();        sysUser.setId(id);        User principal = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();        sysUser.setUsername(principal.getUsername());        return sysUser;}

public class SysUser { private Integer id; private String username;
// 省略。。。}
复制代码

在方法调用完成后进行权限检查,如果返回值的 id 是偶数则表示校验通过,否则表示校验失败,将抛出 AccessDeniedException。@PostAuthorize 是在方法调用完成后进行权限检查,它不能控制方法是否能被调用,只能在方法调用完成后,检查权限然后决定是否要抛出 AccessDeniedException 异常。


@PreFilter

对集合、数组类型的请求参数进行过滤,移除结果为 false 的元素。该过程发生在接口接收参数之前,可以使用内置表达式 filterObjec 进行数据过滤,filterObjec 表示集合中的当前对象。 如果有多个集合参数需要通过 filterTarget=<参数名> 来指定过滤的集合。

// id 大于 1 的才进行查询@PreFilter("filterObject > 1")@PostMapping("/find")public List<Integer> batchGetInfo(@RequestParam("ids") ArrayList<Integer> ids) {    return ids;}
复制代码


多个集合参数需要通过 filterTarget=<参数名> 来指定。如下:

// id 大于 1 的才进行查询@PreFilter(filterTarget = "ids",value = "filterObject > 1")@PostMapping("/batchGetInfo")public List<Integer> batchGetInfo(@RequestParam("ids") ArrayList<Integer> ids,@RequestParam("ids2")ArrayList<Integer> ids2) {    return ids;}
复制代码

@PostFilter

@PostFilter 可以对集合、数组类型的返回值进行过滤!

// 将结果集为偶数的值进行返回@PostFilter("filterObject % 2 == 0")@GetMapping("/findAll")public List<Integer> findAll() {    List<Integer> userList = new ArrayList<>();    for (int i=0; i<10; i++) {        userList.add(i);    }    return userList;}
复制代码


作者:程序员小杰

链接:https://juejin.cn/post/7120394782651711502

来源:稀土掘金

用户头像

还未添加个人签名 2021-07-28 加入

公众号:该用户快成仙了

评论

发布
暂无评论
面试官:你了解Spring Security 权限控制吗?_Java_做梦都在改BUG_InfoQ写作社区