zl程序教程

您现在的位置是:首页 >  Java

当前栏目

一篇文章快速学会shiro框架!(建议收藏)

2023-02-18 16:38:30 时间

1. 认证与授权

在学习 Shiro 框架之前,我们先来了解一下什么是认证与授权。

认证

认证就是校验身份的正确性,看一下你是不是这个平台的用户。系统一般会将用户输入的账号密码和数据库的信息做比对,从而判断身份是否合法。

授权

授权就是确定你是否有权访问系统的某些资源,比如查看某个页面、点击某个按钮、操作某一行数据等。

2. 邂逅 Shiro

Shiro 是 Apache 基金会下面一个开源的 java 权限管理框架,它可以进行身份验证、授权、密码和会话管理。

官网:

https://shiro.apache.org/

Shiro 核心架构:

对于上面复杂的架构,我们只需要记住以下几个核心模块:

1.Subject

主体,你可以理解为访问系统的用户。

2.SecurityManager

安全管理器。用户进行认证和授权都是通过 securityManager 进行,你可以理解为 shiro 的老大。

3.authenticator

认证器,用户通过 authenticator 进行认证。

4.authorizer

授权器,用户通过 authorizer 进行授权。

5.realm

领域,相当于数据源。

在 realm 中,我们通过查询数据库的信息,然后对用户进行认证和授权。所以 authenticator 和 authorizer 其实是调用了 realm 中 认证和授权的方法。

6.cryptography

密码管理。shiro 提供了一套加密和解密的组件。

3. Springboot 整合 Shiro

1.新建 springboot 项目

2.引入依赖

这里我们主要引入了 web、mybatis-plus、shiro、mysql 的依赖,后面有完整的代码。

<!--web-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<!--mybatis-plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.0</version>
</dependency>
<!--引入 shiro 整合 Springboot 依赖-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.5.3</version>
</dependency>
<!--数据库驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

3.修改配置文件

4.创建用户表

CREATE TABLE `t_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '姓名',
  `username` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '账号',
  `password` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码',
  `salt` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '盐',
  `created_date` datetime DEFAULT NULL COMMENT '创建时间',
  `updated_date` datetime DEFAULT NULL,
  `is_deleted` int DEFAULT '0',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1500052815584731139 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;

5.使用 mybatis-plus 生成实体类、service、dao、mapper文件

4. Shiro 认证

1.文件数据源

因为 shiro 的默认数据源是配置文件数据源,所以我们先用配置文件的方式模拟数据库信息。

配置文件数据源:

/*认证*/
@Test
void authenticator() {
    // 1.创建安全管理器
    DefaultSecurityManager securityManager = new DefaultSecurityManager();
    // 2.设置 realm
    securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
    // 3.设置安全管理器
    SecurityUtils.setSecurityManager(securityManager);
    // 4.获取当前主体对象
    Subject subject = SecurityUtils.getSubject();
    // 5.将用户登录时的账号和密码封账成 UsernamePasswordToken 对象
    UsernamePasswordToken token = new UsernamePasswordToken("zhifou", "123456");
    try {
        log.info("认证前状态:" + subject.isAuthenticated());
        subject.login(token);
        log.info("认证后状态:" + subject.isAuthenticated());
    } catch (UnknownAccountException e) {
        log.error("用户不存在");
    } catch (IncorrectCredentialsException e) {
        log.error("账号或密码不正确");
    }
}

认证结果:

认证流程讲解:

首先,你需要创建安全管理器 SecurityManager,然后为安全管理器设置 Realm,Realm 的数据源是以配置文件的方式进行配置。

用户在前端输入账号和密码,shiro 框架获取当前要登录的主体(用户)对象,然后将账号和密码封装成 UsernamePasswordToken 对象。

接着安全管理器通知 Realm 认证一下当前用户,Realm 将用户输入的账号密码信息和配置文件中的信息做比对,比对通过则认证成功,比对不通过则返回错误信息。

2.数据库数据源

使用数据库作为数据源需要我们自定义 Realm,其中自定义的 Realm 需要继承 AuthorizingRealm 类并实现两个方法。

其中一个是认证,一个是授权,这里我们实现认证方法。

新建自定义 Realm,并实现认证方法:

public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    public MyRealm( UserService userService) {
        this.userService = userService;
    }

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 用户登录名称
        String principal = (String) authenticationToken.getPrincipal();
        User user = userService.getOne(new QueryWrapper<User>().eq("username", principal));
        //用户不为空
        if (null != user) {
            // 1.账号 2.密码 3.当前 realm 名字
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),
                    this.getName());
            return simpleAuthenticationInfo;
        }
        return null;
    }
}

修改安全管理器的 Realm 为自定义的 Realm:

数据库中用户信息:

启动测试类,查看认证结果:

讲解:

用户登录的账号和密码被封装成了 token 对象,调用 subject.login() 方法之后,自定义 Realm 的认证方法会接收传递的 token 信息。

其中 UsernamePasswordToken 是 AuthenticationToken 接口的实现类,在 AuthenticationToken 接口中,getPrincipal() 方法返回的是用户的账号信息,getCredentials() 返回的是密码信息。

将用户填写的账号和密码与数据库中的信息做比对,比对通过则返回 SimpleAuthenticationInfo 对象,其中需要传递三个参数:

  • 数据库中的账号
  • 数据库中的密码
  • 当前的 realm 对象

3.Shiro 认证完整案例

这里我们采用 shiro 配置类和登录接口的方式来实现 shiro 的认证。

1.在 UserController 中新建登录接口:

@Controller
@RequestMapping("/user")
@Slf4j
public class UserController {

    @RequestMapping("/login")
    public String login(String username, String password) {
        try {
            //获取主体对象
            Subject subject = SecurityUtils.getSubject();
            subject.login(new UsernamePasswordToken(username, password));
            return "redirect:/index.jsp";
        } catch (UnknownAccountException e) {
            log.error("用户不存在");
        } catch (IncorrectCredentialsException e) {
            log.error("账号或者密码错误");
        } catch (Exception e) {
            log.error("服务出错");
        }
        return "redirect:/login.jsp";
    }
}

2.新建自定义 Realm,实现认证方法。

3.新建 shiro 的配置类

shiro 的配置类主要包含三个 bean:

  • shiroFilter:用来拦截所有请求
  • SecurityManager:安全管理器
  • Realm:自定义 realm

完整配置类:

@Configuration
public class ShiroConfig {

    // 1.shiroFilter:负责拦截所有请求
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        // 默认跳转页面---认证不通过时跳转
        shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        return shiroFilterFactoryBean;
    }

    // 2.创建安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }

    // 3.创建自定义 realm
    @Bean("realm")
    public Realm getRealm() {
        CustomerRealm customerRealm = new CustomerRealm();
        return customerRealm;
    }
}

4.发起登录请求

4.密码加密

我们经常使用 MD5 进行加密,MD5 加密是一种不可逆的加密算法。使用 MD5 加密后的结果是一个 32 位的字符串。

使用 MD5 加密需要三个参数:

  • 密码
  • 随机盐:其实就是一串复杂的字符串,可以使加密后的密码更复杂
  • 散列次数:一般是 1024

在 Shiro 中,需要修改自定义 Realm 来完成 MD5 的加密解密:

在认证方法中,需要返回随机盐。

案例:

1.随机盐工具类

public class SaltUtils {
  public static String getSalt(int n) {
      String str = "01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz(#$^@%&*!)";
      char[] chars = str.toCharArray();
      StringBuilder randomSalt = new StringBuilder();
      Random random = new Random();
      for (int i = 0; i < n; i++) {
          int number = random.nextInt(chars.length);
          char aChar = chars[number];
          randomSalt.append(aChar);
      }
      return randomSalt.toString();
  }
}

2.新增用户信息

@Test
void registerUser() {
    String salt = SaltUtils.getSalt(6);
    User user = new User();
    user.setName("张无忌").setUsername("zhangwuji").setPassword("123456");
    // 设置盐值
    user.setSalt(salt);
    // 明文密码进行md5 + salt + hash散列
    Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
    user.setPassword(md5Hash.toHex());
    userService.save(user);
}

3.登录认证

5. Shiro 授权

我们一般会基于 RBAC 模型设计权限管理系统,在 RBAC 模型里面,有 3 个基础组成部分,分别是:用户、角色和权限。

我们先根据 RBAC 设计数据库表:

修改用户实体类:

修改角色实体类:

在 Shiro 中有三种授权方式:

权限注解

@RequiresRoles(value={"admin","user"})
@RequestMapping("save")
public String save(){
   // 有权限
}
@RequiresPermissions("user:update:01")
@RequestMapping("save")
public String save(){
   // 有权限
}

编程判断

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
    // 有权限
} else {
    // 无权限
}

权限标签

<shiro:hasRole name="admin">
  <!— 有权限 —>
</shiro:hasRole>

完整授权案例:

1.初始化数据库

2.修改 ShiroFilter

因为 ShiroFilter 负责拦截所有请求,所以需要设置哪些资源要拦截,哪些资源可以放开。

在 ShiroFilter 中,需要授权和不需要授权的资源存放在 map 集合里面。其中 anon 表示不需要授权,authc 表示需要授权。

我们还需要配置认证不通过默认跳转页面以及授权不通过跳转页面。

3.修改自定义 Realm

我们需要在自定义的 Realm 中通过查询数据库的角色和权限,然后为主体授权。

4.演示授权

1)注解式授权

新建 login.jsp

新建保存订单接口,拥有 admin 和 user 角色的用户可以访问:

浏览器直接访问保存订单接口,由于没有登录所以默认会跳转到登录页面

使用 zhouzhiruo 这个账户登录,然后再访问保存订单接口

因为 zhouzhiruo 没有添加订单的权限,所以会跳转到 403 页面。

这里使用了全局异常处理器拦截无权限的异常。

2)权限标签

上面系统主页菜单的显示使用了 shiro 的权限标签。

如果用户拥有此角色或者权限资源,就展示相应的菜单。

注意:jsp 页面需要引入 shiro 相关的 tag 资源。

5.退出登录

/**
 * 退出登录
 */
@RequestMapping("/logout")
public String logout() {
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "redirect:/login.jsp";
}

6. 完整代码

链接: https://pan.baidu.com/s/1Fd0KVFiuRW9a-OY01uTBKw?pwd=1234 
提取码: 1234 

-END-