在前面的文章中,所有的接口只需要登录就能访问。并没有对每个接口进行权限限制。 在正式的系统中,一个用户会拥有一个或者多个角色,而不同的角色会拥有不同的接口权限。如果要实现这些功能,需要重写 WebSecurityConfigurerAdapter 中的 configure(HttpSecurity http)。HttpSecurity 用于构建一个安全过滤器链 SecurityFilterChain,可以通过它来进行自定义安全访问策略。
配置如下:
@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.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 授权方式。
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;
@Component
public 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";
}
复制代码
登录 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")
@DenyAll
public String denyAll() {
return "denyAll 你好 Spring Security";
}
@GetMapping("/permitAll")
@PermitAll
public 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 ,就开启了以下注解:
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/1
http://localhost:8080/findById/2
http://localhost:8080/findByName/cxyxj
http://localhost:8080/findByName/security
http://localhost:8080/add/security
http://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
来源:稀土掘金
评论