Spring Boot 安全框架 Shiro 入门

一、前言

在所有的开发的系统中,都必须做认证(authentication)和授权(authorization),以保证系统的安全性。考虑到很多读者对认证和授权有点分不清楚。

authentication [ɔ,θɛntɪ'keʃən] 认证
authorization [,ɔθərɪ'zeʃən] 授权

1.1 以坐飞机举例子:

  • 【认证】你要登机,你需要出示你的 passport 和 ticket,passport 是为了证明你张三确实是你张三,这就是 authentication。
  • 【授权】而机票是为了证明你张三确实买了票可以上飞机,这就是 authorization。

1.2 以论坛举例子:

  • 【认证】你要登录论坛,输入用户名张三,密码 1234,密码正确,证明你张三确实是张三,这就是 authentication。
  • 【授权】再一 check 用户张三是个版主,所以有权限加精删别人帖,这就是 authorization 。
    所以简单来说:认证解决“你是谁”的问题,授权解决“你能做什么”的问题。另外,在推荐阅读下《认证、授权、鉴权和权限控制》 文章,更加详细明确

在 Java 生态中,目前有 Spring SecurityApache Shiro 两个安全框架,可以完成认证和授权的功能。本文,我们再来学习下 Apache Shiro 。其官方对自己介绍如下:
Apache Shiro™ 是一个功能强大且易于使用的 Java 安全框架,它可以提供身份验证、授权、加密和会话管理的功能。
通过 Shiro 易于理解的 API ,你可以快速、轻松地保护任何应用程序 —— 从最小的移动端应用程序到大型的的 Web 和企业级应用程序。

二、 快速入门

2.1 引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>Apache-shiro</artifactId>

    <dependencies>
        <!-- 实现对 Spring MVC 的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 实现对 Shiro 的自动化配置 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.3</version>
        </dependency>
    </dependencies>

</project>

shiro-spring-boot-starter依赖对 Shiro 的自动化配置基本没啥用,需要下面的这个类ShiroConfig自己来主动实现对 Shiro 的配置。

2.2 ShiroConfig

实现 Shiro 的自定义配置。代码如下:

package com.erbadagang.springboot.shiro.config;

import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    @Bean
    public Realm realm() {
        // 创建 SimpleAccountRealm 对象
        SimpleAccountRealm realm = new SimpleAccountRealm();
        // 添加两个用户。参数分别是 username、password、roles 。
        realm.addAccount("admin", "admin", "ADMIN");
        realm.addAccount("normal", "normal", "NORMAL");
        return realm;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        // 创建 DefaultWebSecurityManager 对象
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置其使用的 Realm
        securityManager.setRealm(this.realm());
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        // 创建 ShiroFilterFactoryBean 对象,用于创建 ShiroFilter 过滤器
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();

        // 设置 SecurityManager
        filterFactoryBean.setSecurityManager(this.securityManager());

        // 设置 URL 们
        filterFactoryBean.setLoginUrl("/login"); // 登陆 URL
        filterFactoryBean.setSuccessUrl("/login_success"); // 登陆成功 URL
        filterFactoryBean.setUnauthorizedUrl("/unauthorized"); // 无权限 URL

        // 设置 URL 的权限配置
        filterFactoryBean.setFilterChainDefinitionMap(this.filterChainDefinitionMap());

        return filterFactoryBean;
    }

    private Map<String, String> filterChainDefinitionMap() {
        Map<String, String> filterMap = new LinkedHashMap<>(); // 注意要使用有序的 LinkedHashMap ,顺序匹配
        filterMap.put("/test/echo", "anon"); // 允许匿名访问
        filterMap.put("/test/admin", "roles[ADMIN]"); // 需要 ADMIN 角色
        filterMap.put("/test/normal", "roles[NORMAL]"); // 需要 NORMAL 角色
        filterMap.put("/logout", "logout"); // 退出
        filterMap.put("/**", "authc"); // 默认剩余的 URL ,需要经过认证
        return filterMap;
    }

}

一共有三个 Bean 的配置,我们逐个来看看。

2.2.1 Realm

我们先来看看 Realm 的定义。“身份验证”(认证)和“授权”,这个就是 Realm 的职责。

Realm 整体的类图如下:
Realm 类图
  • Realm 接口,主要定义了“认证”方法。代码如下:
// Realm.java

    AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
  • AuthorizingRealm 抽象类,主要额外定义了授权方法。代码如下:
// AuthorizingRealm.java
    protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
  • AuthorizingRealm 同时实现了 Authorizer 接口,提供判断经过认证过的 Subject 是否具有指定的角色、权限的方法。
  • 从图中我们可以看出,Shiro 提供了多种 AuthorizingRealm 的实现类,提供从不同的数据源获取数据。不过一般在项目中,我们会自定义实现 AuthorizingRealm ,从自己定义的表结构中读取用户、角色、权限等数据。虽然说,Shiro 提供了 JdbcRealm 可以访问数据库,但是它的表结构是固定的,所说我们才要自定义定义实现 AuthorizingRealm

本示例中,在 #realm() 方法,我们创建了 SimpleAccountRealm Bean 对象。代码如上所示:

  • SimpleAccountRealm 是使用内存作为数据源,我们可以手动往里面添加用户、角色、权限等数据。毕竟作为一个示例,不想引入数据库,增加复杂性。不过我们在后续文章中,我们会看到我们使用自定义的 AuthorizingRealm 实现类。
  • 在该方法里,我们添加了「admin/admin」和「normal/normal」两个用户,分别对应 ADMIN 和 NORMAL 角色。

2.2.2 SecurityManager

我们再来看看 SecurityManager 的定义,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
本示例中,在 #securityManager() 方法,我们创建了 DefaultWebSecurityManager Bean 对象。代码如下:

// ShiroConfig.java

@Bean
public DefaultWebSecurityManager securityManager() {
 // 创建 DefaultWebSecurityManager 对象
 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
 // 设置其使用的 Realm
 securityManager.setRealm(this.realm());
 return securityManager;
}
  • 不用特别去纠结 SecurityManager ,创建好 DefaultWebSecurityManager Bean 就完事了~等后续我们入门完 Shiro 之后,胖友可以在慢慢细细去研究。

2.2.3 ShiroFilter

通过 AbstractShiroFilter 过滤器,实现对请求的拦截,从而实现 Shiro 的功能。AbstractShiroFilter 整体的类图如下:

AbstractShiroFilter 类图

本示例中,在 #shiroFilterFactoryBean() 方法,我们创建了 ShiroFilterFactoryBean Bean 对象。代码如下:

// ShiroConfig.java

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
 // <1> 创建 ShiroFilterFactoryBean 对象,用于创建 ShiroFilter 过滤器
 ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();

 // <2> 设置 SecurityManager
 filterFactoryBean.setSecurityManager(this.securityManager());

 // <3> 设置 URL 们
 filterFactoryBean.setLoginUrl("/login"); // 登录 URL
 filterFactoryBean.setSuccessUrl("/login_success"); // 登录成功 URL
 filterFactoryBean.setUnauthorizedUrl("/unauthorized"); // 无权限 URL

 // <4> 设置 URL 的权限配置
 filterFactoryBean.setFilterChainDefinitionMap(this.filterChainDefinitionMap());

 return filterFactoryBean;
}
  • <1> 处,创建 ShiroFilterFactoryBean 对象,用于创建 SpringShiroFilter 过滤器。

  • <2> 处,设置其 SecurityManager 属性。

  • <3> 处,设置各种 URL 。

    • #setLoginUrl(String loginUrl) 方法,设置登录 URL 。在 Shiro 中,约定 GET loginUrl 为登录页面,POST loginUrl 为登录请求。
    • #setSuccessUrl(String successUrl) 方法,设置登录成功 URL 。在登录成功时,会重定向到该 URL 上。
    • #etUnauthorizedUrl(String unauthorizedUrl) 方法,设置无权限的 URL 。在请求校验权限不通过时,会重定向到该 URL 上。
    • 上述的 URL 对应的接口,都需要我们自己来实现。具体可见「2.3 SecurityController」小节。
  • <4> 处,调用 #setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) 方法,设置 URL 的权限配置。

在看 #filterChainDefinitionMap() 方法的具体 URL 的权限配置之前,我们先来了解下 Shiro 内置的过滤器们。在 Shiro DefaultFilter 枚举类中,枚举了这些过滤器,以及其配置名。整理表格如下:

Filter Name Class
anon org.apache.shiro.web.filter.authc.AnonymousFilter
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logout org.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
port org.apache.shiro.web.filter.authz.PortFilter
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shiro.web.filter.authz.SslFilter
user org.apache.shiro.web.filter.authc.UserFilter

比较常用的过来器有:

  • anon :AnonymousFilter :允许匿名访问,即无需登录。
  • authc :FormAuthenticationFilter :需要经过认证的用户,才可以访问。如果是匿名用户,则根据 URL 不同,会有不同的处理:
    • 如果拦截的 URL 是 GET loginUrl 登录页面,则进行该请求,跳转到登录页面。
    • 如果拦截的 URL 是 POST loginUrl 登录请求,则基于请求表单的 usernamepassword 进行认证。认证通过后,默认重定向到 GET loginSuccessUrl 地址。
    • 如果拦截的 URL 是其它 URL 时,则记录该 URL 到 Session 中。在用户登录成功后,重定向到该 URL 上。
  • logout :LogoutFilter :拦截的 URL ,执行退出操作。退出完成后,重定向GET loginUrl 登录页面。
  • roles :RolesAuthorizationFilter :拥有指定角色的用户可访问。
  • perms :PermissionsAuthorizationFilter :拥有指定权限的用户可以访问。

下面,让我们回过头来看看 #filterChainDefinitionMap() 方法的具体 URL 的权限配置。代码如下:

    private Map<String, String> filterChainDefinitionMap() {
        Map<String, String> filterMap = new LinkedHashMap<>(); // 注意要使用有序的 LinkedHashMap ,顺序匹配
        filterMap.put("/test/echo", "anon"); // 允许匿名访问
        filterMap.put("/test/admin", "roles[ADMIN]"); // 需要 ADMIN 角色
        filterMap.put("/test/normal", "roles[NORMAL]"); // 需要 NORMAL 角色
        filterMap.put("/logout", "logout"); // 退出
        filterMap.put("/**", "authc"); // 默认剩余的 URL ,需要经过认证
        return filterMap;
    }
  • /test/echo :我们设置为 anon ,允许匿名访问。
  • /test/admin/test/normal :我们设置为 roles[...] ,需要指定角色的用户可以访问。其中 ... 处为需要添加的角色名。
  • /logout :我们设置为 logout ,实现退出操作。
  • /** :剩余的 URL ,我们设置为 authc ,需要登录的用户才可以访问。同时,对于 loginUrl 需要执行登录相关的拦截。

另外,这里在补充一点,请求在 ShiroFilter 拦截之后,会根据该请求的情况,匹配到配置的内置的 Shiro Filter 们,逐个进行处理。也就是说,ShiroFilter 实际内部有一个由 内置的 Shiro Filter 组成的过滤器

至此,我们已经完成了 Shiro 的自定义配置。虽然篇幅有点长,但是可以等我们跑完整个示例之后,再自己回过头来看看,会发现还是比较清晰明了的。

2.3 SecurityController

提供登录、登录成功等接口。代码如下:

package com.erbadagang.springboot.shiro.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

@Controller
@RequestMapping("/")
public class SecurityController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @GetMapping("/login")
    public String loginPage() {
        return "login.html";
    }

    @ResponseBody
    @PostMapping("/login")
    public String login(HttpServletRequest request) {
        // 判断是否已经登陆
        Subject subject = SecurityUtils.getSubject();
        if (subject.getPrincipal() != null) {
            return "你已经登陆账号:" + subject.getPrincipal();
        }

        // 获得登陆失败的原因
        String shiroLoginFailure = (String) request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
        // 翻译成人类看的懂的提示
        String msg = "";
        if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
            msg = "账号不存在";
        } else if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
            msg = "密码不正确";
        } else if (LockedAccountException.class.getName().equals(shiroLoginFailure)) {
            msg = "账号被锁定";
        } else if (ExpiredCredentialsException.class.getName().equals(shiroLoginFailure)) {
            msg = "账号已过期";
        } else {
            msg = "未知";
            logger.error("[login][未知登陆错误:{}]", shiroLoginFailure);
        }
        return "登陆失败,原因:" + msg;
    }

    @ResponseBody
    @GetMapping("/login_success")
    public String loginSuccess() {
        return "登陆成功";
    }

    @ResponseBody
    @GetMapping("/unauthorized")
    public String unauthorized() {
        return "你没有权限";
    }

}

2.3.1 登录页面

GET /login 地址,跳转登录页面。代码如下:

// SecurityController.java

@GetMapping("/login")
public String loginPage() {
 return "login.html";
}
<!DOCTYPE html>
  <html lang="en">
  <head>
   <meta charset="UTF-8">
   <title>登录页面</title>
  </head>
  <body>
   <form action="/login" method="post">
   用户名:<input type="text" name="username"/> <br />
   密码:<input type="password" name="password"/> <br />
   <input type="submit" value="登录"/>
   </form>
  </body>
  </html>
  • 一个简单的登录的表单,POST 提交登录请求到 /login 地址上。

2.3.2 登录请求

对于登录请求,会被我们配置的 Shiro FormAuthenticationFilter 过滤器进行拦截,进行用户的身份认证。整个过程如下:

  • FormAuthenticationFilter 解析请求的 usernamepassword 参数,创建 UsernamePasswordToken 对象。
  • 然后,调用 SecurityManager 的 #login(Subject subject, AuthenticationToken authenticationToken) 方法,执行登录操作,进行“身份验证”(认证)。
  • 在这内部中,调用 Realm 的 #getAuthenticationInfo(AuthenticationToken token) 方法,进行认证。此时,根据认证的是否成功,会有不同的处理:
    • 如果认证通过,则 FormAuthenticationFilter 会将请求重定向GET loginSuccess 地址上。
    • 【重要】如果认证失败,则会将认证失败的原因设置到请求的 attributes 中,后续该请求会继续请求到 POST login 地址上。这样,在 POST loginUrl 地址上,我们可以从 attributes 中获取到失败的原因,提示给用户。

所以,POST loginUrl 的目的,实际是为了处理认真失败的情况。也因此,POST login 地址,实现代码如下:

// SecurityController.java

@ResponseBody
@PostMapping("/login")
public String login(HttpServletRequest request) {
 // <1> 判断是否已经登录
 Subject subject = SecurityUtils.getSubject();
 if (subject.getPrincipal() != null) {
 return "你已经登录账号:" + subject.getPrincipal();
 }

 // <2> 获得登录失败的原因
 String shiroLoginFailure = (String) request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
 // 翻译成人类看的懂的提示
 String msg = "";
 if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
 msg = "账号不存在";
 } else if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
 msg = "密码不正确";
 } else if (LockedAccountException.class.getName().equals(shiroLoginFailure)) {
 msg = "账号被锁定";
 } else if (ExpiredCredentialsException.class.getName().equals(shiroLoginFailure)) {
 msg = "账号已过期";
 } else {
 msg = "未知";
 logger.error("[login][未知登录错误:{}]", shiroLoginFailure);
 }
 return "登录失败,原因:" + msg;
}
  • <1> 处,对于已经登录成功的用户,如果我们再次请求 POST loginUrl 地址,依然会直接跳转到该地址上。此处,我们是提供用户已经的登录。可能会希望重新进行一次登录的逻辑,那么就需要重写 FormAuthenticationFilter 过滤器。
  • <2> 处,从请求的 attributes 中,获取 FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME 对应的值,即登录失败的原因。从代码中,我们可以看出,失败原因为异常的全类名,我们需要进行翻译成人类可读的提示。

2.3.3 登录成功

GET login_success 地址,登录成功响应。代码如下:

// SecurityController.java

@ResponseBody
@GetMapping("/login_success")
public String loginSuccess() {
 return "登录成功";
}
  • 如果是 AJAX 请求的情况下,我们可以返回 JSON 字符串。例如说,用户、角色、权限等等信息。
  • 如果非 AJAX 请求的情况下,重定向到登录成功的页面。例如说,管理后台的 HOME 页面。

2.3.4 未授权

GET unauthorized 地址,未授权响应。代码如下:

// SecurityController.java

@ResponseBody
@GetMapping("/unauthorized")
public String unauthorized() {
 return "你没有权限";
}
  • 如果是 AJAX 请求的情况下,我们可以返回 JSON 字符串。例如说,你没有权限。
  • 如果非 AJAX 请求的情况下,重定向到登录成功的页面。例如说,未授权的页面。

2.4 TestController

在 [controller]包路径下,创建 TestController 类,提供测试 API 接口。代码如下:

// TestController.java

@RestController
@RequestMapping("/test")
public class TestController {

 @GetMapping("/demo")
 public String demo() {
 return "示例返回";
 }

 @GetMapping("/home")
 public String home() {
 return "我是首页";
 }

 @GetMapping("/admin")
 public String admin() {
 return "我是管理员";
 }

 @GetMapping("/normal")
 public String normal() {
 return "我是普通用户";
 }

}
  • 对于 /test/demo 接口,直接访问,无需登录。
  • 对于 /test/home 接口,无法直接访问,需要进行登录。
  • 对于 /test/admin 接口,需要登录「admin/admin」用户,因为需要 ADMIN 角色。
  • 对于 /test/normal 接口,需要登录「user/user」用户,因为需要 USER 角色。

胖友可以按照如上的说明,进行各种测试。例如说,登录「user/user」用户后,去访问 /test/admin 接口,会返回无权限的提示~

2.5 Application

创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:

// Application.java

@SpringBootApplication
public class Application {

 public static void main(String[] args) {
 SpringApplication.run(Application.class, args);
 }

}

至此,我们已经完成了 Shiro 的入门。可以自己多多测试一下。

三、Shiro注解

在 Shiro 中,提供了如下五个注解,可以直接添加在 SpringMVC 的 URL 对应的方法上,实现权限配置。下面,我们来分别看看。

3.1 @RequiresGuest

@RequiresGuest 注解,和 anon 等价。

3.2 @RequiresAuthentication

@RequiresAuthentication 注解,和 authc 等价。

3.3 @RequiresUser

@RequiresUser 注解,和 user 等价,要求必须登录。

3.4 @RequiresRoles

@RequiresRoles 注解,和 roles 等价。代码如下:

// RequiresRoles.java

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRoles {

 /**
 * A single String role name or multiple comma-delimited role names required in order for the method
 * invocation to be allowed.
 */
 String[] value();

 /**
 * The logical operation for the permission check in case multiple roles are specified. AND is the default
 * @since 1.1.0
 * 当有多个角色时,AND 表示要拥有全部角色,OR 表示拥有任一角色即可
 */
 Logical logical() default Logical.AND; 
}

使用示例如下:

// 属于 NORMAL 角色
@RequiresRoles("NORMAL")

// 要同时拥有 ADMIN 和 NORMAL 角色
@RequiresRoles({"ADMIN", "NORMAL"})

// 拥有 ADMIN 或 NORMAL 任一角色即可
@RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.OR)

如果验证权限不通过,则会抛出 AuthorizationException 异常。此时,我们可以基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,实现全局异常的处理。不了解的胖友,可以看看《芋道 Spring Boot SpringMVC 入门》「5. 全局异常处理」小节。

3.5 @RequiresPermissions

@RequiresPermissions 注解,和 perms 等价。代码如下:

// RequiresPermissions.java

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions {

 /**
 * The permission string which will be passed to {@link org.apache.shiro.subject.Subject#isPermitted(String)}
 * to determine if the user is allowed to invoke the code protected by this annotation.
 */
 String[] value();

 /**
 * The logical operation for the permission checks in case multiple roles are specified. AND is the default
 * @since 1.1.0
 * 当有多个权限时,AND 表示要拥有全部权限,OR 表示拥有任一权限即可
 */
 Logical logical() default Logical.AND; 

}

使用示例如下:

// 拥有 user:add 权限
@RequiresPermissions("user:add")

// 要同时拥有 user:add 和 user:update 权限
@RequiresPermissions({"user:add", "user:update"})

// 拥有 user:add 和 user:update 任一权限即可
@RequiresPermissions(value = {"user:add", "user:update"}, logical = Logical.OR)

如果验证权限不通过,则会抛出 AuthorizationException 异常。此时,我们可以基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,实现全局异常的处理。不了解的胖友,可以看看另外一篇文章的全局异常处理小节。

底线


本文源代码使用 Apache License 2.0开源许可协议,这里是本文源码Gitee地址,可通过命令git clone+地址下载代码到本地,也可直接点击链接通过浏览器方式查看源代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343