zoukankan      html  css  js  c++  java
  • SpringBoot整合shiro

    详细教程: [点我跳转]

    SpringBoot整合shiro

    Shiro是apache旗下一个开源安全框架(http://shiro.apache.org/),它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。

    简介

    执行流程

    用户在进行资源访问时,要求系统要对用户进行权限控制,其具体流程如图所示:

    三大核心对象(重点)

    在概念层面,Shiro 架构包含三个主要的理念,如图所示:

    其中:

    1. Subject :主体对象,负责提交用户认证和授权信息。(每个用户对应一个Subject)
    2. SecurityManager:安全管理器,负责认证,授权等业务实现。(用于管理Subject)
    3. Realm:领域对象,负责从数据层获取业务数据。(用于实现授权和认证的方法)

    详细架构

    Shiro框架进行权限管理时,要涉及到的一些核心对象,主要包括:认证管理对象,授权管理对象,会话管理对象,缓存管理对象,加密管理对象以及Realm管理对象(领域对象:负责处理认证和授权领域的数据访问题)等,其具体架构如图所示:

    其中:

    • Subject(主体):与软件交互的一个特定的实体(用户、第三方服务等)。
    • SecurityManager(安全管理器) :Shiro 的核心,用来协调管理组件工作。
    • Authenticator(认证管理器):负责执行认证操作。
    • Authorizer(授权管理器):负责授权检测。
    • SessionManager(会话管理):负责创建并管理用户 Session 生命周期,提供一个强有力的 Session 体验。
    • SessionDAO:代表 SessionManager 执行 Session 持久(CRUD)动作,它允许任何存储的数据挂接到 session 管理基础上。
    • CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。
    • Cryptography(加密管理器):提供了加密方式的设计及管理。
    • Realms(领域对象):是shiro和你的应用程序安全数据之间的桥梁。

    拦截实现

    1.导入依赖

    依赖地址: https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring

    实用spring整合shiro时,需要在pom.xml中添加如下依赖:

    <dependency>
       <groupId>org.apache.shiro</groupId>
       <artifactId>shiro-spring</artifactId>
       <version>1.5.3</version>
    </dependency>
    

    2. Shiro核心对象配置

    先配置两个bean

    @Configuration
    public class ShiroConfig {
    
        // ShiroFilterFactoryBean
        @Bean
        public ShiroFilterFactoryBean filterFactoryBean(SecurityManager securityManager) {
            ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
            // 注入SecurityManager
            factoryBean.setSecurityManager(securityManager);
    
            // 拦截实现
            Map<String, String> map = new LinkedHashMap<>();
            // authc代表无法直接访问
            map.put("/user/update", "authc");
            map.put("/user/add", "authc");
            // map.put("/user/*", "authc"); // user/* 表示user下的所有请求, 就不用了一个一个配置了
            factoryBean.setFilterChainDefinitionMap(map);
            // 假设/toLogin是拦截后跳转到的登录页面请求
            factoryBean.setLoginUrl("/toLogin");
            return factoryBean;
        }
    
        // SecurityManager
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            return securityManager;
        }
    
    }
    

    现在无法直接访问/user/add 和 /user/update 请求了

    关于拦截方式, 有如下定义:

    • anon: 无需认证
    • authc: 必须认证才能访问(例如登录后可访问)
    • user: 必须拥有记住我功能才能用
    • perms: 拥有对某个资源的权限才能访问
    • role: 拥有某个角色权限才能访问

    Realm

    (领域对象):是shiro和你的应用程序安全数据之间的桥梁。

    自己手动编写, 需要继承AuthorizingRealm类, 还需要交给spring来管理

    继承这个类后需要重写两个方法, 一个方法负责自定义的授权, 一个方法负责自定义的认证

    public class UserRealm extends AuthorizingRealm {
        // 授权
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            System.out.println("授权");
            return null;
        }
    
        // 认证, token参数相当于令牌
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            System.out.println("认证");
            return null;
        }
    }
    

    交给spring管理

    可以在Realm类上加注解, 或者用下面这种方式配置(在shiro的配置类中)

    @Bean
    public UserRealm realm() {
        return new UserRealm();
    }
    

    配置好后, 我们需要将这个对象注入到SecurityManagersecurityManager.setRealm(userRealm);

    @Bean
    public SecurityManager securityManager(UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        return securityManager;
    }
    

    认证的实现

    身份认证即判定用户是否是系统的合法用户,用户访问系统资源时的认证(对用户身份信息的认证)

    其中认证流程分析如下:

    1. 系统调用subject的login方法将用户信息提交给SecurityManager
    2. SecurityManager将认证操作委托给认证器对象
    3. AuthenticatorAuthenticator将用户输入的身份信息传递给Realm。
    4. Realm访问数据库获取用户信息然后对信息进行封装并返回。
    5. Authenticator 对realm返回的信息进行身份认证。

    1. 控制器controller

    这里实现了2个请求处理, 一个跳转到登录页面, 一个实现登录操作

    // 跳转登录页面
    @RequestMapping("/toLogin")
    public String toLogin() {
        return "login";
    }
    
    // 进行登录操作, 假设用户只输入username和passowrd进行登录
    @RequestMapping("/doLogin")
    public String doLogin(String username, String password, Model model) {
        System.out.println("登录操作");
        // 获取用户数据
        Subject subject = SecurityUtils.getSubject();
        // 封装用户的登录数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
            return "index";
        } catch (UnknownAccountException e) { // 用户不存在
            e.printStackTrace();
            model.addAttribute("msg", "用户不存在");
            return "login";
        } catch (IncorrectCredentialsException e) { // 密码错误
            e.printStackTrace();
            model.addAttribute("msg", "密码错误");
            return "login";
        } catch ( LockedAccountException e ) { // 账户已锁定
            e.printStackTrace();
            model.addAttribute("msg", "账户已锁定");
            return "login";
        }
    }
    

    2. 编写realm

    在正式写认证之前, 确定是否配置了Realm [配置Realm]

    配置好之后, 我们编辑认证的方法doGetAuthenticationInfo

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("认证");
    
        // 假设这里的username和password是从数据库中获取到的
        String username = "root";
        String password = "123";
    
        // 类型转换
        UsernamePasswordToken userToken = (UsernamePasswordToken) token;
        if (!userToken.getUsername().equals(username)) {
                       
        }
    
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo("", password, "");
        return info; //返回值会传递给SecurityManager,此对象基于认证信息进行认证。
    }
    

    到这里, 就写好了, 用户名错误和密码错误都会抛出对应的异常

    关于SimpleAuthenticationInfo构造方法, 当然这是参数比较多的一个构造方法

    new SimpleAuthenticationInfo(
          //(user为数据库中查询出来的用户对象)
          user,//principal 用户身份(传什么,将来取出来就是什么)
          user.getPassword(),//hashedCredentials (已加密的密码)
          credentialsSalt,//credentialsSalt 盐值(加密的盐值, 一般从数据库表中取出来)
          this.getName());//realmName
    

    扩展: Shiro加密验证

    和以上案例无关

    当我们保存密码的时候可以使用shiro进行md5加密, 如下:

    String pwd = "123456"; // 密码
    String salt = UUID.randomUUID().toString(); // 盐
    // MD5加密方式, 加密5次
    SimpleHash sh = new SimpleHash("MD5", pwd, salt, 5); // 定义simpleHash对象
    String hashedPwd = sh.toHex(); // 生成16进制密文
    // ...把hashedPwd保存到数据库
    

    这时我们取出数据库中的密码就是被加密过的, 那么我们怎么进行验证输入的密码呢?

    看我的方法:如下

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.获取用户提交的认证信息
        UsernamePasswordToken upToken=(UsernamePasswordToken)token;
        //2.基于用户名查找用户信息
        SysUser user=sysUserDao.findUserByUserName(upToken.getUsername());
        //3.判定用户是否存在
        if(user==null)
            throw new UnknownAccountException();
        //4.判定用户是否已被禁用(被禁用则不允许登陆)
        if(user.getValid()==0)
            throw new LockedAccountException();
        //5.封装认证信息并返回
        ByteSource credentialsSalt=ByteSource.Util.bytes(user.getSalt());
        SimpleAuthenticationInfo info=
            new SimpleAuthenticationInfo(
            user,//principal 用户身份(传什么,将来取出来就是什么)
            user.getPassword(),//hashedCredentials (已加密的密码)
            credentialsSalt,//credentialsSalt
            this.getName());//realmName
        return info;//返回值会传递给SecurityManager,此对象基于认证信息进行认证。
    }
    

    这样还不够, shiro还不知道我们是怎么加密的, 所以我们需要在Realm中重写一个方法

    @Override
    public CredentialsMatcher getCredentialsMatcher() {
        HashedCredentialsMatcher cMatcher = new HashedCredentialsMatcher("MD5"); // 加密方式
        cMatcher.setHashIterations(5); // 加密次数
        return cMatcher;
    }
    

    授权的实现

    授权即对用户资源访问的授权(是否允许用户访问此资源),用户访问系统资源时的授权流程

    授权流程分析:

    1. 系统调用subject相关方法将用户信息(例如isPermitted)递交给SecurityManager。
    2. SecurityManager将权限检测操作委托给Authorizer对象。
    3. Authorizer将用户信息委托给realm。R
    4. ealm访问数据库获取用户权限信息并封装。
    5. Authorizer对用户授权信息进行判定。

    首先来实现简单的授权, 还以上面的案例为例

    例如我们现在需要 /user/add请求需要授权才能访问

    1. 配置shiro

    在我们的shiro配置ShiroFilterFactoryBean的方法中添加如下代码:

    map.put("/user/add", "perms[user:add]");   // 表明/user/add需要授权才能访问, 授权字符串为: 'user:add'
    
    factoryBean.setUnauthorizedUrl("/unauthorized"); // 没有权限时执行的请求或跳转的url
    

    写到这里, 我们的/user/add 请求就无法访问了, 会提示没有权限, 哪么我们怎么才能授权呢, 我们往下接着写

    2. 编写realm

    我们来编写realm中的AuthorizationInfo方法, 即编写授权的方法

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("授权");
    
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermission("user:add"); // 为用户添加授权
        
        // info.addStringPermissions( [set集合] ); 如果有多个权限, 则使用String类型的set集合进行传参
    
        return info;
    }
    

    这样, 我们的user/add就可以访问了, 注意:因为我这里user:add是写死的, 所以说任何用户都会被授权, 对正常来业务来说, 这个字符串应从数据库中取出, 然后添加到授权

    @RequiresPermissions注解

    一般作用域Service中的方法上, 如果作用于类上, 则类中的所有方法陪授权后才能访问. 使用此注解可不用上面map.put("/user/add", "perms[user:add]");这样一个一个的put

    例如: 例如: @RequiresPermissions({"user:add", "user:update"})

    即要求subject中有注解中的参数权限时, 才能执行被此注解描述的方法, 否则抛出AuthorizationException异常

  • 相关阅读:
    python学习第19天
    python学习第18天
    python 端口扫描
    Linux pthread
    python tornado 入门
    C语言 链表排序
    软件版本中的 符号意思
    connect 链接失败: 查找不到 signal
    类模板 与 模板类
    Qt:正则表达式语法:
  • 原文地址:https://www.cnblogs.com/zpKang/p/13399008.html
Copyright © 2011-2022 走看看