SpringBoot 安全框架 Shiro 使用


一. 概述

基本上,在所有的开发的系统中,都必须做认证(authentication)和授权(authorization),以保证系统的安全性。

认证 (authentication) 和授权 (authorization) 的区别:

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

以坐火车举例子:

  • 【认证】你要上车,你需要出示你身份证,身份证是为了证明你张三确实是你张三,这就是 authentication。
  • 【授权】而车票是为了证明你张三确实买了票可以上车,这就是 authorization。

所以简单来说:认证解决“你是谁”的问题,授权解决“你能做什么”的问题。

而在 Java 生态中,目前有 Spring SecuritApache Shiro 两个安全框架,可以完成认证和授权的功能。

Apache Shiro ,其官方对自己介绍如下:

FROM 《Apache Shiro 官网》

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. Apache Shiro™ 是一个功能强大且易于使用的 Java 安全框架,它可以提供身份验证、授权、加密和会话管理的功能。

With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications. 通过 Shiro 易于理解的 API ,你可以快速、轻松地保护任何应用程序 —— 从最小的移动端应用程序到大型的的 Web 和企业级应用程序。

二. 快速入门

使用 Shiro 进行快速的入门,实现一个最小化的使用示例。

2.1 引入依赖

创建一个maven项目,并在pom.xml文件中,引入相关依赖。

<!-- spring-boot启动器 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.10.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>


<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.4.2</version>
        </dependency>
</dependencies>

2.2 ShiroConfig

在 com.xsh.config 包下,创建 ShiroConfig 抽象类,实现 Shiro 的自定义配置。代码如下:

// ShiroConfig.java

@Configuration
public class ShiroConfig {

    @Bean
    public Realm realm() { /**省略代码**/ }

    @Bean
    public DefaultWebSecurityManager securityManager() { /**省略代码**/ }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() { /**省略代码**/ }

}

一共有三个 Bean 的配置,逐个分析:

2.2.1 Realm

先来看看 Realm 的定义。

[《Apache Shiro 1.2.x 参考手册 —— Realms》](https://waylau.com/apache-shiro-1.2.x-reference/II. Core 核心/7. Realms.html)

Realm 是可以访问程序特定的安全数据如用户、角色、权限等的一个组件。Realm 会将这些程序特定的安全数据转换成一种 Shiro 可以理解的形式,Shiro 就可以依次提供容易理解的 [Subject](https://waylau.com/apache-shiro-1.2.x-reference/IV. Auxiliary Support 辅助支持/14. Custom Subjects 自定义 Subject.html) 程序API而不管有多少数据源或者程序中你的数据如何组织。

Realm 通常和数据源是一对一的对应关系,如关系数据库,LDAP 目录,文件系统,或其他类似资源。因此,Realm 接口的实现使用数据源特定的API 来展示授权数据(角色,权限等),如JDBC,文件IO,Hibernate 或JPA,或其他数据访问API。

Realm 实质上就是一个特定安全的 DAO

因为这些数据源大多通常存储身份验证数据(如密码的凭证)以及授权数据(如角色或权限),每个 Shiro Realm 能够执行身份验证和授权操作。

  • 好长一段描述,抓重点,最后一句的“身份验证”(认证)和“授权”,这个就是 Realm 的职责。

Realm 整体的类图如下:

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

AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;

  • AuthorizingRealm 抽象类,主要额外定义了“ 授权 ”方法。代码如下:
// AuthorizingRealm.java

protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

AuthorizingRealm 同时实现了 Authorizer 接口,提供判断经过认证过的 Subject 是否具有指定的角色、权限的方法。

从Realm 整体的类图中可以看出,Shiro 提供了多种 AuthorizingRealm 的实现类,提供从不同的数据源获取数据。不过一般在项目中,我们会自定义实现 AuthorizingRealm ,从自己定义的表结构中读取用户、角色、权限等数据。虽然说,Shiro 提供了 JdbcRealm可以访问数据库,但是它的表结构是固定的,所说才要自定义定义实现 AuthorizingRealm

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

// ShiroConfig.java

@Bean
public Realm realm() {
    // 创建 SimpleAccountRealm 对象
    SimpleAccountRealm realm = new SimpleAccountRealm();
    // 添加两个用户。参数对应 username、password、roles 。
    realm.addAccount("admin", "123456", "ADMIN");
    realm.addAccount("user", "123456", "USER");
    return realm;
}
  • SimpleAccountRealm 是使用内存作为数据源,我们可以手动往里面添加用户、角色、权限等数据。毕竟作为一个示例,可以不引入数据库,增加复杂性。
  • 在该方法里,添加了「admin/123456」和「user/123456」两个用户,分别对应 ADMIN 和 USER角色。

2.2.2 SecurityManager

先看看 SecurityManager 的定义。

[《Apache Shiro 1.2.x 参考手册 —— Session Management》](https://waylau.com/apache-shiro-1.2.x-reference/II. Core 核心/8. Session Management.html)

SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。

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

// ShiroConfig.java

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

2.2.3 ShiroFilter

通过 AbstractShiroFilter 过滤器,实现对请求的拦截,从而实现 Shiro 的功能。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;
}
  • <4> 处,调用 setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) 方法,设置 URL 的权限配置。

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

Filter NameClass
anonorg.apache.shiro.web.filter.authc.AnonymousFilter
authcorg.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logoutorg.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreationorg.apache.shiro.web.filter.session.NoSessionCreationFilter
permsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
portorg.apache.shiro.web.filter.authz.PortFilter
restorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
rolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFilter
sslorg.apache.shiro.web.filter.authz.SslFilter
userorg.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 的权限配置。代码如下:

// ShiroConfig.java

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/user", "roles[USER]"); // 需要 USER 角色
    filterMap.put("/logout", "logout"); // 退出
    filterMap.put("/**", "authc"); // 默认剩余的 URL ,需要经过认证
    return filterMap;
}
  • /test/echo :设置为 anon ,允许匿名访问。
  • /test/admin/test/user :我们设置为 roles[...] ,需要指定角色的用户可以访问。其中 ... 处为需要添加的角色名。
  • /logout :我们设置为 logout ,实现退出操作。
  • /** :剩余的 URL ,我们设置为 authc ,需要登陆的用户才可以访问。同时,对于 loginUrl 需要执行登陆相关的拦截规则,设置为不拦截允许匿名访问。

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

2.3 SecurityController

在 com.xsh.controller包路径下,创建 SecurityController 类,提供登陆、登陆成功等接口。代码如下:

// SecurityController.java

@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 "你没有权限";
    }

}
  • 登陆页面

GET /login 地址,跳转登陆,返回 resources/static/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 地址上。

  • 登陆请求

对于登陆请求,会被我们配置的 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 loginUrl 地址,依然会请求转发到POST loginUrl上。此处,我们是提供用户已经的登陆的提示。 如果希望重新进行一次登陆的逻辑,那么就需要重写 FormAuthenticationFilter 过滤器。

  • 获得登陆失败的原因,从请求的 attributes 中,获取 FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME 对应的值,即登陆失败的原因。从代码中可以看出,失败原因为异常的全类名,需要进行翻译成人类可读的提示。

  • 登陆成功

GET login_success 地址,登陆成功响应。

  • 如果是 AJAX 请求的情况下,我们可以返回 JSON 字符串。例如说,用户、角色、权限等等信息。

  • 如果非 AJAX 请求的情况下,重定向到登陆成功的页面。例如说,管理后台的 HOME 页面。

  • 未授权

GET unauthorized 地址,未授权响应。

  • 如果是 AJAX 请求的情况下,我们可以返回 JSON 字符串。例如说,你没有权限。
  • 如果非 AJAX 请求的情况下,重定向到登陆成功的页面。例如说,未授权的页面。

2.4 TestController

com.xsh.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("/user")
    public String user() {
        return "我是普通用户";
    }

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

登陆「user/user」用户后,去访问 /test/admin 接口,会返回无权限的提示~

2.5 Application

springBoot启动类,在com.xsh目录下新建Application.java,与controller目录和config目录平级

// Application.java

@SpringBootApplication
public class Application {

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

启动后,端口默认8080,使用「admin/123456」登录后出现spring图标则登录成功

三. Shiro 注解

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

@RequiresGuest 注解,和 anon 等价。

@RequiresAuthentication注解,和 authc 等价。

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

@RequiresRoles 注解,和 roles 等价。

其中@RequiresRoles使用示例如下:

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

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

// 同时拥有 ADMIN 和 NORMAL 两个角色才允许访问
@RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.AND)

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

如果验证权限不通过,则会抛出 AuthorizationException 异常。此时,我们可以基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,实现全局异常的处理。

@RequiresPermissions 注解,和 perms 等价。使用示例如下:

// 拥有 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.AND)

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

如果验证权限不通过,则会抛出 AuthorizationException 异常。此时,我们可以基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,实现全局异常的处理。

  • 使用示例

在TestController的基础上,增加 Shiro 注解的使用。

com.xsh.controller 包路径下,创建 DemoController 类,提供示例 API 接口。代码如下:

// DemoController.java

@RestController
@RequestMapping("/demo")
public class DemoController {

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

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

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

    @RequiresRoles("USER")
    @GetMapping("/user")
    public String user() {
        return "我是普通用户";
    }
}
  • 每个 URL 的权限验证,和TestController是一 一对应的。

四. Shiro前端标签

<shiro:guest>
    游客访问 <a href = "login.jsp"></a>
</shiro:guest>
 
user 标签:用户已经通过认证或者记住我 登录后显示响应的内容
<shiro:user>
    欢迎[<shiro:principal/>]登录 <a href = "logout">退出</a>
</shiro:user>
 
authenticated标签:用户身份验证通过,即 Subjec.login 登录成功 不是记住我登录的
<shiro:authenticted>
    用户[<shiro:principal/>] 已身份验证通过
</shiro:authenticted>
 
notAuthenticated标签:用户未进行身份验证,即没有调用Subject.login进行登录,包括"记住我"也属于未进行身份验证
<shiro:notAuthenticated>
    未身份验证(包括"记住我")
</shiro:notAuthenticated>
 
 
principal 标签:显示用户身份信息,默认调用
Subjec.getPrincipal()获取,即Primary Principal
<shiro:principal property = "username"/>
 
hasRole标签:如果当前Subject有角色将显示body体内的内容
<shiro:hashRole name = "admin">
    用户[<shiro:principal/>]拥有角色admin
</shiro:hashRole>
 
hasAnyRoles标签:如果Subject有任意一个角色(或的关系)将显示body体里的内容
<shiro:hasAnyRoles name = "admin,user">
    用户[<shiro:pricipal/>]拥有角色admin 或者 user
</shiro:hasAnyRoles>
 
lacksRole:如果当前 Subjec没有角色将显示body体内的内容
<shiro:lacksRole name = "admin">
    用户[<shiro:pricipal/>]没有角色admin
</shiro:lacksRole>
 
hashPermission:如果当前Subject有权限将显示body体内容
<shiro:hashPermission name = "user:create">
    用户[<shiro:pricipal/>] 拥有权限user:create
</shiro:hashPermission>
 
lacksPermission:如果当前Subject没有权限将显示body体内容
<shiro:lacksPermission name = "org:create">
    用户[<shiro:pricipal/>] 没有权限org:create
</shiro:lacksPermission>
Spring shiro
  • 作者:管理员(联系作者)
  • 发表时间:2020-03-12 23:50
  • 版权声明:自由转载-非商用-非衍生-保持署名(null)
  • undefined
  • 评论