zoukankan      html  css  js  c++  java
  • SpringBoot--集成Shiro

    转:https://blog.csdn.net/bicheng4769/article/details/86668209

    什么是Shiro?

    Apache Shiro是Java的一个安全框架,旨在简化身份验证和授权。Shiro在JavaSE和JavaEE项目中都可以使用。它主要用来处理身份认证,授权,企业会话管理和加密等。

    Shrio使用有什么优点?

    (1)身份认证/登录,验证用户是不是拥有相应的身份; 
    (2)授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限; 
    (3)会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的; 
    (4)加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储; 
    (5)Web支持,可以非常容易的集成到Web环境; 
    Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率; 
    (6)shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去; 
    (7)提供测试支持; 
    (8)允许一个用户假装为另一个用户(如果他们允许)的身份进行访问; 
    (9)记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

    Shiro集成

    • 引入maven依赖包

    <dependency>
          <groupId>org.apache.shiro</groupId>
          <artifactId>shiro-spring</artifactId>
          <version>1.3.2</version>
    </dependency>
    • ShiroConfig类:对shiro的一些配置,相当于之前的xml配置。包括:过滤的文件和权限,密码加密的算法,其用注解等相关功能。

    import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
    import org.apache.shiro.mgt.DefaultSecurityManager;
    import org.apache.shiro.mgt.SecurityManager;
    import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    @Configuration
    public class ShiroConfig {
    
        @Bean(name = "shiroFilter")
        public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            shiroFilterFactoryBean.setLoginUrl("/login");
            shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
            // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
            filterChainDefinitionMap.put("/webjars/**", "anon");
            filterChainDefinitionMap.put("/login", "anon");
            filterChainDefinitionMap.put("/", "anon");
            filterChainDefinitionMap.put("/front/**", "anon");
            filterChainDefinitionMap.put("/api/**", "anon");
    
            filterChainDefinitionMap.put("/admin/**", "authc");
            filterChainDefinitionMap.put("/user/**", "authc");
            //主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
            filterChainDefinitionMap.put("/**", "authc");
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
            return shiroFilterFactoryBean;
    
        }
    
        @Bean
        public SecurityManager securityManager() {
            DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
            defaultSecurityManager.setRealm(customRealm());
            return defaultSecurityManager;
        }
    
        @Bean
        public CustomRealm customRealm() {
            CustomRealm customRealm = new CustomRealm();
            return customRealm;
        }
    }

    shiroConfig 也不复杂,基本就三个方法。再说这三个方法之前,我想给大家说一下shiro的三个核心概念:

    Subject: 代表当前正在执行操作的用户,但Subject代表的可以是人,也可以是任何第三方系统帐号。当然每个subject实例都会被绑定到SercurityManger上。
    SecurityManger:SecurityManager是Shiro核心,主要协调Shiro内部的各种安全组件,这个我们不需要太关注,只需要知道可以设置自定的Realm。
    Realm:用户数据和Shiro数据交互的桥梁。比如需要用户身份认证、权限认证。都是需要通过Realm来读取数据。
    shiroFilter方法:
    这个方法看名字就知道了:shiro的过滤器,可以设置登录页面(setLoginUrl)、权限不足跳转页面(setUnauthorizedUrl)、具体某些页面的权限控制或者身份认证。
    注意:这里是需要设置SecurityManager(setSecurityManager)。
    默认的过滤器还有:anno、authc、authcBasic、logout、noSessionCreation、perms、port、rest、roles、ssl、user过滤器。
    具体的大家可以查看package org.apache.shiro.web.filter.mgt.DefaultFilter。这个类,常用的也就authc、anno。
    securityManager 方法:
    查看源码可以知道 securityManager是一个接口类,我们可以看下它的实现类:

    具体怎么实现的,感兴趣的同学可以看下。由于项目是一个web项目,所以我们使用的是DefaultWebSecurityManager ,然后设置自己的Realm。
    CustomRealm 方法:
    将 customRealm的实例化交给spring去管理,当然这里也可以利用注解的方式去注入。

    • CustomRealm类:自定义的CustomRealm继承AuthorizingRealm。并且重写父类中的doGetAuthorizationInfo(权限相关)、doGetAuthenticationInfo(身份认证)这两个方法

    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.*;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.SimpleAuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.apache.shiro.util.ByteSource;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.stereotype.Component;
    
    import java.util.HashSet;
    import java.util.Set;
    
    public class CustomRealm extends AuthorizingRealm {
    
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            String username = (String) SecurityUtils.getSubject().getPrincipal();
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            Set<String> stringSet = new HashSet<>();
            stringSet.add("user:show");
            stringSet.add("user:admin");
            info.setStringPermissions(stringSet);
            return info;
        }
    
        /**
         * 这里可以注入userService,为了方便演示,我就写死了帐号了密码
         * private UserService userService;
         * <p>
         * 获取即将需要认证的信息
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("-------身份认证方法--------");
            String userName = (String) authenticationToken.getPrincipal();
            String userPwd = new String((char[]) authenticationToken.getCredentials());
            //根据用户名从数据库获取密码
            String password = "123";
            if (userName == null) {
                throw new AccountException("用户名不正确");
            } else if (!userPwd.equals(password )) {
                throw new AccountException("密码不正确");
            }
            return new SimpleAuthenticationInfo(userName, password,getName());
        }
    }

    自定义的Realm类继承AuthorizingRealm类,并且重载doGetAuthorizationInfo和doGetAuthenticationInfo两个方法。
    doGetAuthorizationInfo: 权限认证,即登录过后,每个身份不一定,对应的所能看的页面也不一样。
    doGetAuthenticationInfo:身份认证。即登录通过账号和密码验证登陆人的身份信息。

    controller类:

        @RequestMapping(value = "/login", method = RequestMethod.GET)
        @ResponseBody
        public String defaultLogin() {
            return "首页";
        }
    
        @RequestMapping(value = "/login", method = RequestMethod.POST)
        @ResponseBody
        public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
            // 从SecurityUtils里边创建一个 subject
            Subject subject = SecurityUtils.getSubject();
            // 在认证提交前准备 token(令牌)
            UsernamePasswordToken token = new UsernamePasswordToken(username, password);
            // 执行认证登陆
            try {
                subject.login(token);
            } catch (UnknownAccountException uae) {
                return "未知账户";
            } catch (IncorrectCredentialsException ice) {
                return "密码不正确";
            } catch (LockedAccountException lae) {
                return "账户已锁定";
            } catch (ExcessiveAttemptsException eae) {
                return "用户名或密码错误次数过多";
            } catch (AuthenticationException ae) {
                return "用户名或密码不正确!";
            }
            if (subject.isAuthenticated()) {
                return "登录成功";
            } else {
                token.clear();
                return "登录失败";
            }
        }
    • 测试:可以使用postman进行测试

    ok 身份认证是没问题了,我们再来考虑如何加入权限。

    利用注解配置权限:

    其实,我们完全可以不用注解的形式去配置权限,因为在之前已经加过了:DefaultFilter类中有perms(类似于perms[user:add])这种形式的。但是试想一下,这种控制的粒度可能会很细,具体到某一个类中的方法,那么如果是配置文件配,是不是每个方法都要加一个perms?但是注解就不一样了,直接写在方法上面,简单快捷。
    很简单,主需要在ShiroConfig类中加入如下代码,就能开启注解:

        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
        
     /**
         * *
         * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
         * *
         * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
         * * @return
         */
        @Bean
        @DependsOn({"lifecycleBeanPostProcessor"})
        public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            advisorAutoProxyCreator.setProxyTargetClass(true);
            return advisorAutoProxyCreator;
        }
        
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
            return authorizationAttributeSourceAdvisor;
        }

    新建一个UserController类。如下:

    @RequestMapping("/user")
    @Controller
    public class UserController {
        @RequiresPermissions("user:list")
        @ResponseBody
        @RequestMapping("/show")
        public String showUser() {
            return "这是学生信息";
        }
    }

    重复刚才的登录步骤,登录成功后,postman 输入localhost:8080/user/show

    确实是没有权限。方法上是 @RequiresPermissions("user:list"),而customRealm中是 user:show、user:admin。我们可以调整下方法上的权限改为user:show。调试一下,发现成功了。
    这里有一个问题:当没有权限时,系统会报错,而没有跳转到对应的没有权限的页面,也就是setUnauthorizedUrl这个方法没起作用,这个问题,下一篇会给出解决方案-。-

    密码采用加密方式进行验证:

    其实上面的功能已经基本满足我们的需求了,但是唯一一点美中不足的是,密码都是采用的明文方式进行比对的。那么shiro是否提供给我们一种密码加密的方式呢?答案是肯定。
    shiroConfig中加入加密配置:

        @Bean(name = "credentialsMatcher")
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
            HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
            // 散列算法:这里使用MD5算法;
            hashedCredentialsMatcher.setHashAlgorithmName("md5");
            // 散列的次数,比如散列两次,相当于 md5(md5(""));
            hashedCredentialsMatcher.setHashIterations(2);
            // storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
            hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
            return hashedCredentialsMatcher;
        }

    customRealm初始化的时候耶需要做一些改变:

    @Bean
        public CustomRealm customRealm() {
            CustomRealm customRealm = new CustomRealm();
            // 告诉realm,使用credentialsMatcher加密算法类来验证密文
            customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
            customRealm.setCachingEnabled(false);
            return customRealm;
        }

    流程是这样的,用户注册的时候,程序将明文通过加密方式加密,存到数据库的是密文,登录时将密文取出来,再通过shiro将用户输入的密码进行加密对比,一样则成功,不一样则失败。
    我们可以看到这里的加密采用的是MD5,而且是加密两次(MD5(MD5))。
    shiro提供了SimpleHash类帮助我们快速加密:

    public static String MD5Pwd(String username, String pwd) {
            // 加密算法MD5
            // salt盐 username + salt
            // 迭代次数
            String md5Pwd = new SimpleHash("MD5", pwd,
                    ByteSource.Util.bytes(username + "salt"), 2).toHex();
            return md5Pwd;
        }

    也就是说注册的时候调用一下上面的方法得到密文之后,再存入数据库。
    在CustomRealm进行身份认证的时候我们也需要作出改变:

          System.out.println("-------身份认证方法--------");
            String userName = (String) authenticationToken.getPrincipal();
            String userPwd = new String((char[]) authenticationToken.getCredentials());
            //根据用户名从数据库获取密码
            String password = "2415b95d3203ac901e287b76fcef640b";
            if (userName == null) {
                throw new AccountException("用户名不正确");
            } else if (!userPwd.equals(userPwd)) {
                throw new AccountException("密码不正确");
            }
            //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
            return new SimpleAuthenticationInfo(userName, password,
                    ByteSource.Util.bytes(userName + "salt"), getName());

    这里唯一需要注意的是:你注册的加密方式和设置的加密方式还有Realm中身份认证的方式都是要一模一样的。
    本文中的加密 :MD5两次、salt=username+salt加密。

    总结:

    spirngboot整合shiro其实不难,重点是对shiro的核心概念需要有点了解,不然只知道怎么配,而不知道为什么这么配,就很尴尬了。

  • 相关阅读:
    Spring事务传播特性NOT_SUPPORTED使用演示
    spring配置基于xml的声明式事务
    反射的简单应用
    MyBatis批处理工具类MyBatisBatchHelper.java
    Mybatis分页插件PageHelper的使用
    利用Spring的Profile加载指定数据源
    Nginx状态信息(status)配置及信息详解
    nginx配置基于域名、端口、IP的虚拟主机
    Nginx Linux安装与部署
    rename 批量修改文件名简单用法
  • 原文地址:https://www.cnblogs.com/jvStarBlog/p/12516010.html
Copyright © 2011-2022 走看看