1. 简介

1.1 了解Shiro

Shiro 是 apache 旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。

核心架构如下:

  • Subject:主体,可以理解为当前操作的用户,或者是一个浏览器请求。
  • SecurityManager:安全管理器,负责对所有的 subject 进行安全管理,完成 subject 的认证和授权。实质上 SecurityManager 是通过 Authenticator 进行认证,通过 Authorizer 进行授权,通过 SessionManager 进行会话管理等。
  • Authenticator:认证器,对用户身份进行认证,Authenticator 是一个接口,shiro 提供 ModularRealmAuthenticator 实现类,通过 ModularRealmAuthenticator 基本上可以满足大多数需求,也可以自定义认证器。
  • Authorizer:授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
  • Realm:领域,相当于datasource数据源,securityManager 进行安全认证需要通过 Realm 获取用户权限数据,比如:如果用户身份数据在数据库那么 realm 就需要从数据库获取用户身份信息。
  • SessionManager:会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以 shiro 可以使用在非 web 应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
  • SessionDAO:即会话 dao ,是对 session 会话操作的一套接口,比如要将 session 存储到数据库,可以通过 jdbc 将会话存储到数据库。
  • CacheManager:缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
  • Cryptography:密码管理,shiro 提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。

Shiro - 图1

1.2 了解认证

判断一个用户是否为合法用户的处理过程,通过就是校验用户名和密码。在认证过程中,有三个关键对象:

  • Subject:主体,访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
  • Principal:身份信息,是主体(subject)进行身份认证的标识,标识必须具有 唯一性 ,如用户名、手机号、邮箱地址等,一 个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
  • credential:凭证信息,只有主体自己知道的安全信息,如密码、证书等。

    1.3 了解授权

    即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源。在授权过程中,有三个关键对象,授权过程可以简单理解为 who 对 what 进行 how 的操作。

  • Who,即主体(Subject),主体需要访问系统中的资源。

  • What,即资源(Resource) ,如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。
  • How,权限/许可(Permission) ,规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
  • 授权有两种方式,一种是基于角色授权,赋予用户角色,另一种是基于资源的授权。可参考:https://juejin.cn/post/6941734947551969288#heading-1

    2. 快速入门

    2.1 引入依赖

    ```xml <?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

    1. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    4.0.0

    1. <groupId>org.springframework.boot</groupId>
    2. <artifactId>spring-boot-starter-parent</artifactId>
    3. <version>2.2.2.RELEASE</version>
    4. <relativePath/> <!-- lookup parent from repository -->

    com.ljnt blog 0.0.1-SNAPSHOT blog

    Demo project for Spring Boot 1.8 org.apache.shiro shiro-spring-boot-starter 1.8.0 com.auth0 java-jwt 3.11.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test com.baomidou mybatis-plus-boot-starter 3.5.1 mysql mysql-connector-java 5.1.47 runtime org.springframework.boot spring-boot-maven-plugin

  1. <a name="XnHMq"></a>
  2. ## 2.2 本地运行
  3. 1、resources 目录下自定义 shiro.ini 文件。
  4. ```properties
  5. # username = password, role1, role2, ..., roleN
  6. [users]
  7. weishao = 123456, admin
  8. guest = 123456, guest, goodguy
  9. # roleName = perm1, perm2, ..., permN
  10. [roles]
  11. admin = *
  12. common = dept:*
  13. goodguy = winnebago:drive:eagle5

2、自定义认证授权。

  1. public class ShiroTest {
  2. @Test
  3. public void testShiroIni() {
  4. // 创建安全管理器,获取用户权限数据
  5. DefaultSecurityManager securityManager = new DefaultSecurityManager();
  6. securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
  7. // 在安装工具类中设置默认安全管理器
  8. SecurityUtils.setSecurityManager(securityManager);
  9. // 获取主体,可能是用户,也可能是一段程序
  10. Subject subject = SecurityUtils.getSubject();
  11. // 创建用户登录凭证,从controller传递过来的
  12. UsernamePasswordToken token = new UsernamePasswordToken("guest", "123456");
  13. // 登录校验,这里进行认证
  14. subject.login(token);
  15. // 判断用户已经认证
  16. System.out.println(subject.isAuthenticated()); // true
  17. // 判断权限
  18. System.out.println(subject.hasRole("admin")); // false
  19. System.out.println(subject.isPermitted("winnebago:drive:eagle5")); // true
  20. }
  21. }

2.3 自定义配置类

核心有三个 Bean 配置

  1. @Configuration
  2. public class ShiroConfig {
  3. /**
  4. * 定义Realm
  5. * @return
  6. */
  7. @Bean
  8. public Realm realm() {}
  9. /**
  10. * 定义SecurityManager
  11. * @return
  12. */
  13. @Bean
  14. public DefaultWebSecurityManager securityManager() {}
  15. /**
  16. * 定义ShiroFilter,实现对请求拦截
  17. * @return
  18. */
  19. @Bean
  20. public ShiroFilterFactoryBean shiroFilterFactoryBean() {}
  21. }

2.3.1 Realm

Realm,通过和数据源是一对一的关系,主要进行用户认证和授权。看一下 Realm 的类图:
IniRealm.png
具体来看下 AuthorizingRealm 源码,我们自定义 Realm 一般都继承这个类,重写两个方法:

  1. // 认证方法,Realm接口自定义
  2. AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
  3. // 授权方法,AuthorizingRealm 新增
  4. protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);

这里采用了 SimpleAccountRealm,是使用内存作为数据源,代码如下:

  1. public Realm realm() {
  2. // 创建 SimpleAccountRealm 对象(使用内存作为数据源)
  3. SimpleAccountRealm realm = new SimpleAccountRealm();
  4. // 添加两个用户,参数是username、password、roles
  5. realm.addAccount("admin", "123456", "ADMIN");
  6. realm.addAccount("normal", "123456", "NORMAL");
  7. return realm;
  8. }

2.3.2 SecurityManager

  1. @Bean
  2. public DefaultWebSecurityManager securityManager() {
  3. DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
  4. // 设置其使用的Realm
  5. securityManager.setRealm(this.realm());
  6. return securityManager;
  7. }

2.3.3 ShiroFilter

通过 AbstractShiroFilter 过滤器,实现对请求的拦截,从而实现 Shiro 的功能。比较常用的过滤器有:

  • anon :AnonymousFilter :允许匿名访问,即无需登陆。
  • authc :FormAuthenticationFilter :需要经过认证的用户,才可以访问。如果是匿名用户,则根据 URL 不同,会有不同的处理:
    • 如果拦截的 URL 是 GET loginUrl 登陆页面,则进行该请求,跳转到登陆页面。
    • 如果拦截的 URL 是 POST loginUrl 登陆请求,则基于请求表单的 username、password 进行认证。认证通过后,默认重定向到 GET loginSuccessUrl 地址。
    • 如果拦截的 URL 是其它 URL 时,则记录该 URL 到 Session 中。在用户登陆成功后,重定向到该 URL 上。
  • logout :LogoutFilter :拦截的 URL ,执行退出操作。退出完成后,重定向到 GET loginUrl 登陆页面。
  • roles :RolesAuthorizationFilter :拥有指定角色的用户可访问。
  • perms :PermissionsAuthorizationFilter :拥有指定权限的用户可以访问。 ```java /**
    • 定义ShiroFilter,实现对请求拦截
    • @return */ @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; }

/**

  • 设置URL权限配置
  • @return / private Map filterChainDefinitionMap() { Map 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; } ```

    2.4 常用Shiro注解

    在 Shiro 中,提供了如下五个注解,可以直接添加在 SpringMVC 的 URL 对应的方法上,实现权限配置。
  • @RequiresGuest 注解,和 anon 等价。
  • @RequiresAuthentication 注解,和 authc 等价。
  • @RequiresUser 注解,和 user 等价,要求必须登陆。
  • @RequiresRoles 注解,和 roles 等价。如果验证权限不通过,则会抛出 AuthorizationException 异常。此时,我们可以基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,实现全局异常的处理。
  • @RequiresPermissions 注解,和 perms 等价。如果验证权限不通过,则会抛出 AuthorizationException 异常。

举例说明:

  1. // 属于 NORMAL 角色
  2. @RequiresRoles("NORMAL")
  3. // 要同时拥有 ADMIN 和 NORMAL 角色
  4. @RequiresRoles({"ADMIN", "NORMAL"})
  5. // 拥有 ADMIN 或 NORMAL 任一角色即可
  6. @RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.OR)
  7. // 拥有 user:add 权限
  8. @RequiresPermissions("user:add")
  9. // 要同时拥有 user:add 和 user:update 权限
  10. @RequiresPermissions({"user:add", "user:update"})
  11. // 拥有 user:add 和 user:update 任一权限即可
  12. @RequiresPermissions(value = {"user:add", "user:update"}, logical = Logical.OR)

2.5 定义Controller

1、登录校验。

  1. @Controller
  2. public class LoginController {
  3. @GetMapping("/login")
  4. public String loginPage() {
  5. return "login.html";
  6. }
  7. @ResponseBody
  8. @PostMapping("/login")
  9. public String login(HttpServletRequest request) {
  10. // 1、判断是否已经登录
  11. Subject subject = SecurityUtils.getSubject();
  12. if (subject.getPrincipal() != null) {
  13. return "你已经登录账户:" + subject.getPrincipal();
  14. }
  15. // 2、获得登录失败的原因
  16. String shiroLoginFailure = (String) request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
  17. String msg = "";
  18. if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
  19. msg = "账号不存在";
  20. } else if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
  21. msg = "密码不正确";
  22. } else if (LockedAccountException.class.getName().equals(shiroLoginFailure)) {
  23. msg = "账号被锁定";
  24. } else if (ExpiredCredentialsException.class.getName().equals(shiroLoginFailure)) {
  25. msg = "账号已过期";
  26. } else {
  27. msg = "未知";
  28. }
  29. return "登陆失败,原因:" + msg;
  30. }
  31. @ResponseBody
  32. @GetMapping("/login_success")
  33. public String loginSuccess() {
  34. return "登录成功";
  35. }
  36. @ResponseBody
  37. @GetMapping("/unauthorized")
  38. public String unauthorized() {
  39. return "你没有权限";
  40. }
  41. }

2、测试请求是否有权限访问。

  1. @RequestMapping("/test")
  2. @RestController
  3. public class TestController {
  4. @GetMapping("/echo")
  5. public String demo() {
  6. return "示例返回";
  7. }
  8. @GetMapping("/home")
  9. public String home() {
  10. return "我是首页";
  11. }
  12. @GetMapping("/admin")
  13. public String admin() {
  14. return "我是管理员";
  15. }
  16. @GetMapping("/normal")
  17. public String normal() {
  18. return "我是普通用户";
  19. }
  20. }

3、使用注解指定访问权限。

  1. @RequestMapping("/demo")
  2. @RestController
  3. public class RequiresController {
  4. @RequiresGuest
  5. @GetMapping("/echo")
  6. public String demo() {
  7. return "示例返回";
  8. }
  9. @GetMapping("/home")
  10. public String home() {
  11. return "我是首页";
  12. }
  13. @RequiresRoles("ADMIN")
  14. @GetMapping("/admin")
  15. public String admin() {
  16. return "我是管理员";
  17. }
  18. @RequiresRoles("NORMAL")
  19. @GetMapping("/normal")
  20. public String normal() {
  21. return "我是普通用户";
  22. }
  23. }

3. 加密解密

shiro 框架中使用 SimpleHash 加盐进行密码加密,这个过程不可逆,使用的是其构造函数加密。

  • algorithmName 代表进行加密的算法名称。
  • source 代表需要加密的元数据,如密码。
  • salt 代表盐,需要加进一起加密的数据。
  • hashIterations 代表hash迭代次数,可以不写,默认为 1。

    1. public SimpleHash(String algorithmName, Object source, Object salt, int hashIterations) throws CodecException, UnknownAlgorithmException {
    2. this.hexEncoded = null;
    3. this.base64Encoded = null;
    4. if (!StringUtils.hasText(algorithmName)) {
    5. throw new NullPointerException("algorithmName argument cannot be null or empty.");
    6. } else {
    7. this.algorithmName = algorithmName;
    8. this.iterations = Math.max(1, hashIterations);
    9. ByteSource saltBytes = null;
    10. if (salt != null) {
    11. saltBytes = this.convertSaltToBytes(salt);
    12. this.salt = saltBytes;
    13. }
    14. ByteSource sourceBytes = this.convertSourceToBytes(source);
    15. this.hash(sourceBytes, saltBytes, hashIterations);
    16. }
    17. }

举例:使用构造函数对密码为 123456 进行加密。

  1. public static void main(String[] args) {
  2. String password = "123456";
  3. String Shpassword = "c7acd09454c858e257aa5cecb1cdb904d6f6b0ac"; // 加密结果
  4. String salt = "f182eba3b28945afc50c19e1ececb80e"; // 盐
  5. SimpleHash hash = new SimpleHash("sha-1", password, salt);
  6. System.out.println(hash.toHex());
  7. }

4. 参考资料