shiro是一个Java安全(权限框架)

它可以完成一些安全操作,比如认证、授权、加密、会话管理、Web集成、缓存等等……

Apache Shiro | Simple. Java. Security.

十分钟入门

在官网上,它有一个十分钟入门的小案例,这个是它的github地址。

shiro/Quickstart.java at main · apache/shiro (github.com)

我们来看看它都有些什么东西。

下面展示一些常用的API(其实这个案例就是为了展示常用API……)

  • 获取当前用户对象

    Subject currentUser = SecurityUtils.getSubject();

  • 通过当前用户拿到shiro的session

    Session session = currentUser.getSession();

    • 设值

      session.setAttribute("someKey", "aValue");

    • 取值

      String value = (String) session.getAttribute("someKey");

  • 判断当前用户是否被认证

    if (!currentUser.isAuthenticated())

  • 获得当前用户认证信息

    currentUser.getPrincipal()

  • 判断是否有这个角色

    if (currentUser.hasRole("schwartz"))

  • 注销

    currentUser.logout();

上面是核心!需要多注意。

Spring Boot集成Shiro

写一下步骤吧,也方便以后回顾。

导入依赖

【前方有坑,请注意!】

上面说十分钟入门的那里,其实没太在意这些,毕竟官网的肯定能跑。

然后就中招了,现在才回想起来,一定要注意导入的依赖,不要漏了!

首先搭建起来一个能跑的Spring Boot项目

@RequestMapping({"/","/index"})
public String toIndex(Model model){
    model.addAttribute("msg", "hello shiro");
    return "index";
}

首页就是在resources下的template中,创建一个index.html,里面展示一下msg信息,就这么简单,结果失败了!

<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p> 
</body>
</html>

给我报了这么一个错误

Circular view path [index]: would dispatch back to the current handler URL [/index] again

想想好有道理,我从index进来,然后又从index出去,这不就造成循环了吗?很对【x】

但是那个return,很明显就是想视图解析器去跳转到对应的html页面中啊,而不是再跳回去……

原因是,我导错了依赖……我用了thymeleaf模版,它不大全?

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
    <version>3.0.12.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

我应该用starter,或者直接加一个Framework Support,而不是自己找找这些包……谁也不知道它缺了啥。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

问题解决,成功看到hello world。

shiro也是一样的,导入一个starter就行了,错误率最小的(我居然在官网上找不到直接的maven依赖,看来它不用依赖很多东西吧……)

自定义Realm

我终于知道为什么那个项目里面会有一个我看不懂的XXXRealm,这相当于一个实体类?(翻译出来是叫做“领域”的东西)

它主要是做授权认证的工作,我们写一个UserRealm

它需要继承AuthorizingRealm。

public class UserRealm extends AuthorizingRealm {
    /**
     * 授权 
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("执行了授权方法。。。。。。。。。。doGetAuthorizationInfo");
        return null;
    }

    /**
     * 认证 
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("执行了认证方法。。。。。。。。。。doGetAuthenticationInfo");
        return null;
    }
}

自定义配置ShiroConfig

它分为三步:

  1. 自定义realm对象(上面已做)
  2. 设置一个DefaultWebSecurityManager,里面需要注入1的realm对象
  3. 设置一个ShiroFilterFactoryBean,里面需要注入2的DefaultWebSecurityManager对象

感觉就是一层套一层的,好像有点懂又好像不大懂。

@Configuration
public class ShiroConfig {

    /**
     *  3.ShiroFilterFactoryBean
     *  为什么起getShiroFilterFactoryBean直接报错...不行呢?
     *  (应该是和springboot的自动装配原理有关)
      */ 
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        return bean;
    }

    /**
     * 2. DefaultWebSecurityManager 
     * @Qualifier("userRealm") UserRealm userRealm ,就是把下面注入的bean给传到这里来
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 关联userRealm
        // 利用bean进行注入
        securityManager.setRealm(userRealm);
        return securityManager;
    }


    /**
     * 1.创建realm对象,需要自定义
     * 相当于。。实体那些?翻译说它是领域,不就是domain、entity这些……
      */
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

}

然后再在页面那里做些小补充,一些跳转,这样就能接下来的验证了。

登录拦截

在上面的shiroConfigshiroFilterFactoryBean方法中,在中间可以插入一些过滤的选项!

内置过滤器

  • anon:无需认证就能访问
  • authc:必须认证才能访问
  • user:必须拥有“记住我”功能才能使用
  • perms:拥有对某个资源的权限才能访问
  • role:拥有某个角色权限才能访问

代码的编写的话,其实就是写一个Map,然后往里面丢需要被过滤的url以及权限控制。

        // 设置一个Map,然后往里面丢值设置……
        Map<String, String> filterMap = new LinkedHashMap<>();
        // 按照上面的过滤器,下面这两句代码的意思是
        // 任何人都能访问add
        filterMap.put("/user/add", "anon");
        // 只有认证了才能访问update
        filterMap.put("/user/update", "authc");
        // 通配符也是支持的
//        filterMap.put("/user/*", "authc");
        bean.setFilterChainDefinitionMap(filterMap);

这样的话,访问/user/add就随意,但是访问/user/update则需要认证。

没有认证怎么办?可以设置一个跳转!

// 设置登录的请求,即如果没有权限,就会跳转到登录页
bean.setLoginUrl("/toLogin");

如果没有认证的话,它会自动跳转到/toLogin中,然后Controller就会匹配它。

用户认证

它主要是在realm中编写的,回想上面的步骤,它继承了一个AuthorizingRealm,重写了两个方法。

所以接下来在用户认证这块,我们会在Controller和doGetAuthenticationInfo这个方法中编写。

(Controller拿到数据呀,就在那里马上处理了,我还想着会只在doGetAuthenticationInfo处理)

在Controller里面组合用户信息,然后执行它的登录subject.login(token)

(要是你问我它是怎么登录的,我只能说无可奉告!)

@RequestMapping("/login")
public String login(String username, String password, Model model){
    // 获取当前的用户
    Subject subject = SecurityUtils.getSubject();
    // 封装用户的登录数据,得到令牌
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {
        // 执行登录方法,如果没有异常,就ok了(????它自己就给我验证了)
        // 自动login
        subject.login(token);
        // 返回首页
        return "index";
    } catch (UnknownAccountException e) {
        // 用户名不存在,这样就能返回前端,让他们再作处理了
        model.addAttribute("msg", "用户名不存在");
        return "login";
    }catch (IncorrectCredentialsException e) {
        model.addAttribute("msg", "密码错误");
        return "login";
    }
}

在认证这块,就……跟着它做就是了。暂时不大懂怎么搞

/**
 * 认证
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("执行了认证方法。。。。。。。。。。doGetAuthenticationInfo");
    // 假设它是从数据库中取出来的用户名、密码
    String name = "root";
    String password = "admin"; 
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;
    if (!userToken.getUsername().equals(name)){
        // 会抛出异常 UnknownAccountException用户名不存在
        return null;
    }
    // 密码是它自己比对的。。。我们只要把它丢进去就行了
    return new SimpleAuthenticationInfo("", password, "");
}

用户授权

用户授权,那就应该在UserRealm里面做了,它分为两个方法,一个认证,一个授权。

那我只要在授权那个方法里面写就行了吧?但是有个问题,用户的信息(或者说你要验证的用户密码……)不在这里,它在认证那里接受了。

虽然说你也可以继续用service重新获取信息,但还有一个方法是把信息存到pricipal 中。

return new SimpleAuthenticationInfo(user, user.getPwd(), "");

(上面的第一个参数)

最后在授权那里,使用SecurityUtils.getSubject()拿到对象,然后再取出来就行了!

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("执行了授权方法。。。。。。。。。。doGetAuthorizationInfo");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//        info.addStringPermissions(Collections.singleton("user:add"));
        // 拿到当前登录的对象
        Subject subject = SecurityUtils.getSubject();
        // 取出来,拿到user对象,然后强转
        User currentUser = (User) subject.getPrincipal();
        info.addStringPermission(currentUser.getPerms());
        // 下面的方法效果一样!
//        Set<String> abc = new HashSet<>();
//        abc.add(currentUser.getPerms());
//        info.setStringPermissions(abc);
        // 最后返回它……
        return info;
    }

看到要返回东西,那就是要实现AuthorizationInfo子类,然后往里面加东西吧。

这里展示的需求就是,从数据库中查用户出来,看看它的权限是不是满足(通常是一些字符串,比如上面的user:add)如果有这个权限,就能进行XXX操作,不然就权限不够。

ShiroFilterFactoryBean中可以设置这种操作。

// 授权
// 就是要有user:add才能授权通过它
filterMap.put("/user/add", "perms[user:add]");
filterMap.put("/user/update", "perms[user:update]");
// 设置未授权的请求
bean.setUnauthorizedUrl("/noauth");

小结

这里展示了一些shiro的基本操作,认证和授权,还有一些过滤器,使用的话应该还好,看看文档再看看这里应该能解决一些很简单的需求吧。

项目实战