Java

SpringBoot 整合 Shiro

勤劳的小蜜蜂 · 11月28日 · 2019年 ·

使用 Shiro 之前,我们必须要了解 Shiro 对外提供的 API

Shiro API

  • Subject:主体,代表当前”用户”,所有的 Subject 都需要绑定到 SecurityManager 上
  • SecurityManager:安全管理器,管理着所有的 Subject,类似 SpringMVC 中的 DispatcherServlet
  • Reaml:域,其中定义着该用户是否可以通过认证,是否可以被授权

一个最简单的 Shiro 应用流程:

  1. 应用通过 Subject 来认证和授权,Subject 将这个工作交给 SecurityManager
  2. SecurityManager 需要 Reaml,来判断该用户是否可以通过认证和被授权

SpringBoot 依赖

<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

代码实现

主要实现以下功能点

  • 无须认证授权,可访问 /hello,/login
  • 须认证,才可访问 /home
  • 须认证授权,才可访问 /add

整合 Shiro 三步走:

  1. 自定义 Realm 文件
  2. 创建 SecurityManager,注入 Realm
  3. 创建 ShiroFilterFactoryBean,注入 SecurityManager

自定义 Realm

/**
 * 自定义 realm 需要继承 AuthorizingRealm,实现 doGetAuthorizationInfo 和 doGetAuthenticationInfo
 * doGetAuthorizationInfo:执行授权逻辑
 * doGetAuthenticationInfo:执行认证逻辑
 * */

@Slf4j
public class CustomRealm extends AuthorizingRealm {

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        /**
         * @Description: 执行授权操作
         * @Param: [principals] 认证方法 doGetAuthenticationInfo 中 SimpleAuthenticationInfo 的 principal 参数,即 username
         * @return: org.apache.shiro.authz.AuthorizationInfo
         * @Author: Ye
         * @Date: 2019/11/28
         */
        System.out.println("principals:" + principals);
        System.out.println("principals.getPrimaryPrincipal():" + principals.getPrimaryPrincipal());



        System.out.println("执行授权操作");
        log.info("执行授权操作");

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        Subject subject = SecurityUtils.getSubject();
        //  subject.getPrincipal(); 获得认证方法中 SimpleAuthenticationInfo 的第一个参数
        String username = (String) subject.getPrincipal();
        System.out.println("需要授权的用户名:" + username);

        /**
         * 添加资源的授权字符串
         * 授权字符串可以自定义
         * user:add 应从数据库中查询
         * */
        if(username.equals("zhang")) {
            simpleAuthorizationInfo.addStringPermission("user:add");
        }
        return simpleAuthorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        /**
        * @Description:  认证逻辑操作
        * @Param: [token] Controller 那边封装用户名密码的 UsernamePasswordToken 对象
        * @return: org.apache.shiro.authc.AuthenticationInfo
        * @Author: Ye
        * @Date: 2019/11/28
        */

        System.out.println("执行认证操作");
        log.info("执行认证操作");

        //  模拟数据库的用户名和密码,"zhang" 和 "li" 密码都是 123
        String username = "zhang";
        String password = "123";

        String username2 = "li";

        //  编写 shiro 判断逻辑,判断用户名和密码

        //  1.判断用户名
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        //  设置两个用户的目的只是区分权限
        if (!usernamePasswordToken.getUsername().equals(username) && !usernamePasswordToken.getUsername().equals(username2)) {
            //  则用户名不存在
            //  返回 null,shiro 会自动抛出 UnknownAccountException 的异常
            return null;
        }

        //  2.判断密码
        /**
         * 三个参数解释
         * 1.principal 建议使用 username 也可以使用 User 实体,同时可以通过 Securityutils.getSubject().getPrincipal();取出
         * 2.password 应是数据库查询出的密码,shior 自动将 password 和 usernamePasswordToken.getPassword 做对比,不一致则抛出异常
         * 3.当前 realm 的名字
         */
        return new SimpleAuthenticationInfo(usernamePasswordToken.getUsername(), password, "");
    }
}

用户 “zhang” 有访问 /add 的权限

装配 Bean,设置路由权限

@Configuration
public class ShiroConfig {

    /**
     * 创建 ShiroFilterFactoryBean
     * */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //  设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //  添加 shiro 内置过滤器
        /**
         * Shiro 内置过滤器,可以实现权限相关的拦截
         * 常用的过滤器:
         *  anon:无需认证(登陆)可以访问
         *  authc:必须认证才能访问
         *  user:如果使用 rememberMe 的功能可以直接访问
         *  perms:该资源必须得到资源权限才可以访问
         *  role:该资源必须得到角色权限才可以访问
         * */
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/holle", "anon");
        filterMap.put("/login", "anon");
        filterMap.put("/home", "authc");

        //  授权过滤器
        filterMap.put("/add", "perms[user:add]");

        //  修改未认证的重定向的页面
        shiroFilterFactoryBean.setLoginUrl("/hello");

        //  修改未授权的重定向的页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;

    }

    /**
     * 创建 DefaultWebSecurityManager
     * */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("customRealm") CustomRealm customRealm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(customRealm);
        return defaultWebSecurityManager;
    }

    /**
     * 创建 Realm
     * */
    @Bean(name = "customRealm")
    public CustomRealm getRealm () {
        return new CustomRealm();
    }
}

Shiro 内置常用的过滤器:

  • anon:无需认证(登陆)可以访问
  • authc:必须认证才能访问
  • user:如果使用 rememberMe 的功能可以直接访问
  • perms:该资源必须得到资源权限才可以访问
  • role:该资源必须得到角色权限才可以访问

Controller

@RestController
@Slf4j
public class UserController {

    @GetMapping("/hello")
    public String hello() {
        return "hello shiro";
    }

    @GetMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password) {
        /**
         * 使用 shiro 编写认证操作
         * */
        //  1.获取 Subject
        Subject subject = SecurityUtils.getSubject();

        //  2.封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        //  3.执行登陆方法
        try {
            subject.login(token);
            log.info("用户登陆成功");
        } catch (UnknownAccountException e) {
            log.error("用户名不存在");
            return "用户名不存在";
        } catch (IncorrectCredentialsException e) {
            log.error("密码错误");
            return "密码错误";
        }
        return "用户登陆成功";
    }

    @GetMapping("/home")
    public String home() {
        return "已登陆,欢迎来到 home";
    }

    @GetMapping("/add")
    public String add() {
        return "拥有 add 权限";
    }

    @GetMapping("/unauth")
    public String unanth() {
        return "没有权限";
    }
}

测试

  • 访问无须认证授权 /hello
  • 未经认证,访问须认证路由 /home,如预期一样跳转到了 /hello
  • 未经认证授权,访问须认证授权路由,认证之后才是授权,所以跳转到了未认证的默认路由
  • 已认证但是未授权,首先登陆 li 用户,然后访问 /add
  • 已认证已授权,首先登陆 zhang 用户,然后当问 /add

0 条回应