Spring Boot 安全框架 Shiro 入门

2020-08-28  本文已影响0人  梅西爱骑车

一、前言

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

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

1.1 以坐飞机举例子:

1.2 以论坛举例子:

在 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.java

    AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
// AuthorizingRealm.java
    protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

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

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;
}

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;
}

在看 #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

比较常用的过来器有:

下面,让我们回过头来看看 #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;
    }

另外,这里在补充一点,请求在 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>

2.3.2 登录请求

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

所以,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;
}

2.3.3 登录成功

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

// SecurityController.java

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

2.3.4 未授权

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

// SecurityController.java

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

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 "我是普通用户";
 }

}

胖友可以按照如上的说明,进行各种测试。例如说,登录「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+地址下载代码到本地,也可直接点击链接通过浏览器方式查看源代码。

上一篇 下一篇

猜你喜欢

热点阅读