• Shiro认证详解


     

     

    Shiro

    shiro是一个java的安全框架
    官网地址 http://shiro.apache.org/

    Shiro综述

    Shiro
     
     
     
     
     
     
     
    SecurityManager(安全管理器)
    Subject(用户)
    Realm域
    CacheManager
    Realms
    UserDao
    CredentialsMatcher
    • Subject:主体,代表了当前 “用户”
    • SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;是 Shiro 的核心
    • Realm:域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

    Shiro 不提供维护用户 / 权限,而是通过 Realm 让开发人员自己注入。

    参考Shiro提供的JdbcRealm中源码的实现

    //获取用户,其会自动绑定到当前线程
    Subject subject = SecurityUtils.getSubject();
    //构建待认证token
    UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");
    //登录,即身份验证
    subject.login(token);
    //判断是否已经认证
    subject.isAuthenticated()
    //登出
    subject.logout(token);
    
    
    graph TB
    A(Realm)-->B(CachingRealm)
    B-->C(AuthenticatingRealm认证)
    C-->D(AuthorizingRealm授权)
    D-->E(自己实现的Realm)
    D-->E1(Shiro提供的JdbcRealm)
    E1-->F1(参考内部实现)
    E-->F("doGetAuthorizationInfo()")
    E-->G("doGetAuthenticationInfo()")
    style E fill:#f96
    

    过滤器

    认证拦截器

    • anon 匿名拦截器,不需要认证即可访问,如 /static/**=anon,/login=anon
    • authc 需要认证才可以访问,如/**=authc
    • user 用户已经身份验证 / 记住我登录的都可;示例 /**=user
    • logout 退出拦截器,如 /logout=logout

    注意authc和user的区别

    授权拦截器

    • roles 角色授权拦截器,验证用户是否拥有角色;如:/admin/**=roles[admin]
    • perms 权限授权拦截器,验证用户是否拥有所有权限;/user/**=perms["user:create"]

    注解

    • @RequiresPermissions 验证权限
    • @RequiresRoles 验证角色
    • @RequiresUser 验证用户是否登录(包含通过记住我登录的)
    • @RequiresAuthentication 验证是否已认证(不含通过记住我登录的)
    • @RequiresGuest 不需要认证即可访问
    //拥有ADMIN角色同时还要有sys:role:info权限
    @RequiresRoles(value={"ADMIN")
    @RequiresPermissions("sys:role:info")
    

    整合Shiro

    1. 配置SecurityManager

    注入Realm和CacheManager(选)

     @Bean("securityManager")
        public org.apache.shiro.mgt.SecurityManager securityManager(ShrioRealm shrioRealm, PhoneRealm phoneRealm) {
            DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
    //      //注入自定义myRealm
    //      defaultWebSecurityManager.setRealm(shrioRealm);
            //设置多个realm,用户名密码登录realm,手机号短信验证码登录realm
            List<Realm> realms = new ArrayList<>();
            realms.add(shrioRealm);
            realms.add(phoneRealm);
            defaultWebSecurityManager.setRealms(realms);
    
            return defaultWebSecurityManager;
        }
    

    2.实现Realm

    注入密码验证器,设置是否启用缓存

    /**
     *
     * 自定义realm
     * @author yuxf
     * @version 1.0
     * @date 2020/12/21 16:10
     */
    public class ShrioRealm extends AuthorizingRealm {
    
        @Autowired
        TestShiroUserService userService;
    
        /**
         * 获取授权信息
         * @param principals
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            //从数据库取角色
            Set<String> roles = userService.getRoles();
            SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();//权限信息
            simpleAuthorizationInfo.addRoles(roles);
            simpleAuthorizationInfo.addStringPermission("user:create");
            return  simpleAuthorizationInfo;
        }
    
        /**
         * 获取认证信息
         * @param token
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            if(token.getPrincipal()==null)return null;
            String userName=token.getPrincipal().toString();
            //从数据库查询用户名
            String dbUser = userService.loadUserByUserName(userName);
            if(dbUser==null||"".equals(dbUser)) throw  new UnknownAccountException();
            //密码盐
            ByteSource salt = ByteSource.Util.bytes(userName);
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName, "123456", salt,getName());
            return simpleAuthenticationInfo;
        }
    }
    
    
     @Bean
        public ShrioRealm shrioRealm() {
            ShrioRealm shrioRealm = new ShrioRealm();
            //设置密码加密规则
            shrioRealm.setCredentialsMatcher(hashedCredentialsMatcher());
            return shrioRealm;
        }
        
         /**
         * 凭证匹配器
         *
         * @return
         */
        @Bean
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
             HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
             hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
             hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
             return hashedCredentialsMatcher;
        }
    
    /**
     * 注册时需要生成密码和密码盐存入数据库
     * @author yuxf
     * @version 1.0
     * @date 2020/12/22 17:01
     */
    public class PasswordHelper {
        private static String algorithmName = "md5";
        private static final int hashIterations = 2;
        /**
         * 获取随机密码盐
         * @return
         */
        public  static String getSalt()
        {
            RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
            String salt = randomNumberGenerator.nextBytes().toHex();
            return  salt;
        }
    
        /**
         * 生成密码
         * @param plainPassword 明文密码
         * @param salt 密码盐
         * @return
         */
        public  static String getPassowrd(String plainPassword,String salt)
        {
            String newPassword = new SimpleHash(algorithmName, plainPassword, salt, hashIterations).toHex();
            return  newPassword;
        }
    }
    

    3.配置LifecycleBeanPostProcessor

     /**
         * 配置LifecycleBeanPostProcessor 可以自动调用配置在Spring IOC容器中 Shiro Bean的生命周期方法
         * @return
         */
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }
    

    4.启动注解

    /**
         * 配置注解生效
         *
         * @return
         */
        @Bean
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
            defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
            return defaultAdvisorAutoProxyCreator;
        }
    
        /**
         * 配置注解生效
         *
         * @return
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") org.apache.shiro.mgt.SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor sourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            sourceAdvisor.setSecurityManager(securityManager);
            return sourceAdvisor;
        }
    

    5.配置ShiroFilter

    ssm项目中坑

    @Bean("shiroFilterFactoryBean")
        public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") org.apache.shiro.mgt.SecurityManager securityManager) {
            //shiro对象
            ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
            bean.setSecurityManager(securityManager);
            bean.setLoginUrl("/shiro/login");
            bean.setSuccessUrl("/shrio/index");
            LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<String, String>();
            //认证顺序是从上往下执行。
            linkedHashMap.put("/logout", "logout");//在这儿配置登出地址,不需要专门去写控制器。
            linkedHashMap.put("/shiro/phoneLogin", "anon");
            linkedHashMap.put("/demo/**", "anon");
            linkedHashMap.put("/static/**", "anon");
            linkedHashMap.put("/shiro/anon", "anon");
            linkedHashMap.put("/**", "user");//需要进行权限验证
            bean.setFilterChainDefinitionMap(linkedHashMap);
            return bean;
        }
        
        
    

    SSM项目中web.xml中配置shiroFilter

    <!-- shiro过虑器,DelegatingFilterProxy通过代理模式将spring容器中的bean和filter关联起来 -->
    	<filter>
    		<filter-name>shiroFilter</filter-name>
    		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    		 <!-- 设置true由servlet容器控制filter的生命周期 -->
    		<init-param>
    			<param-name>targetFilterLifecycle</param-name>
    			<param-value>true</param-value>
    		</init-param> 
    		<!-- 设置spring容器filter的bean id,如果不设置则找与filter-name一致的bean -->
    		<init-param>
    			<param-name>targetBeanName</param-name>
    			<param-value>shiro</param-value>
    		</init-param>
    	</filter>
    

    缓存

    https://www.cnblogs.com/nuccch/p/8044226.html

    思考:为什么Shiro要设计成既可以在Realm,也可以在SecurityManager中设置缓存管理器呢?

    加密

    https://www.cnblogs.com/cac2020/p/13850318.html

    1. 注入HashedCredentialsMatcher实现(推荐)

    需要自己编写加密帮助类生成密码和盐值,比较灵活

    @Bean
        public ShrioRealm shrioRealm() {
            ShrioRealm shrioRealm = new ShrioRealm();
            //设置密码加密规则
            shrioRealm.setCredentialsMatcher(hashedCredentialsMatcher());
            return shrioRealm;
        }
          /**
         * 凭证匹配器
         *
         * @return
         */
        @Bean
        public HashedCredentialsMatcher hashedCredentialsMatcher() {
            HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
            hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
            hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
            return hashedCredentialsMatcher;
        }
    

    加密帮助类

    /**
     * 注册时需要生成密码和密码盐存入数据库
     *
     * @author yuxf
     * @version 1.0
     * @date 2020/12/22 17:01
     */
    public class PasswordHelper {
        private static String algorithmName = "md5";
        private static final int hashIterations = 2;
    
        /**
         * 获取随机密码盐
         *
         * @return
         */
        public static String getSalt() {
            RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
            String salt = randomNumberGenerator.nextBytes().toHex();
            return salt;
        }
    
        /**
         * 生成密码
         *
         * @param plainPassword 明文密码
         * @param salt          密码盐
         * @return
         */
        public static String getPassowrd(String plainPassword, String salt) {
            String newPassword = new SimpleHash(algorithmName, plainPassword, salt, hashIterations).toHex();
            return newPassword;
        }
    
    }
    

    2. 注入PasswordMatcher实现

    1. Shiro提供的PasswordService 相当于 密码帮助类,可用于生成密码和验证密码
    2. 如果使用公盐(hashService.setGeneratePublicSalt(true)),则必须设置HashFormat为Shiro1CryptFormat或不设置,默认为这个,否则无法保存盐值导致验证失败,密码加密结果如:$shiro1$MD5$3$QvLJZY8JiAJMnK9vRjlG6w==$jbNS0N/3fq2KUXufYwGwWA==,里面包含了加密的方法类型,哈希次数,盐值,加密结果,验证密码时会取出加密密码中的盐值来hash客户端的密码来验证密码是否正确
    3. 盐值保存在密码中,无需额外存储
    @Bean
        public PhoneRealm phoneRealm() {
            PhoneRealm phoneRealm = new PhoneRealm();
            //PasswordMatcher
            PasswordMatcher passwordMatcher = new PasswordMatcher();
            passwordMatcher.setPasswordService(passwordService());
            phoneRealm.setCredentialsMatcher(passwordMatcher);
            return phoneRealm;
        }
        
         @Bean
        public  PasswordService passwordService()
        {
            DefaultHashService hashService = new DefaultHashService();
            hashService.setHashIterations(3);
            hashService.setHashAlgorithmName("MD5");
            hashService.setGeneratePublicSalt(true);
            //设置HashService
            DefaultPasswordService passwordService = new DefaultPasswordService();
            passwordService.setHashService(hashService);
           // passwordService.setHashFormat(new HexFormat());
            return  passwordService;
        }
    

    多身份Realm认证

    1. (推荐)自定义AuthenticationToken并重写Realm的supports方法,来明确Real支持的Token

    注意不要继承UsernamePasswordToken

    public class PhoneVcodeToken implements AuthenticationToken {
        private String phone;
        private String vcode;
        public  PhoneVcodeToken(String phone,String vcode)
        {
            this.phone=phone;
            this.vcode=vcode;
        }
        @Override
        public Object getPrincipal() {
            return phone;
        }
    
        @Override
        public Object getCredentials() {
            return vcode;
        }
    }
    

    Realm

    public class PhoneRealm extends AuthorizingRealm {
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            return null;
        }
    
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            String userName = token.getPrincipal().toString();
            if (userName.equals("admin")) {
                //123456a
                return new SimpleAuthenticationInfo(userName, "$shiro1$MD5$3$j8X4VX1f6T6zGiGEFIW5yA==$ipG89XmDquh++g5xXmV1dQ==", getName());
            } else {
                //123456
                return new SimpleAuthenticationInfo(userName, "$shiro1$MD5$3$QvLJZY8JiAJMnK9vRjlG6w==$jbNS0N/3fq2KUXufYwGwWA==", getName());
            }
        }
    
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof PhoneVcodeToken;
        }
    }
    
    1. 自定义AuthenticationToken并加入类型参数,重写ModularRealmAuthenticator 在doAuthenticate()方法中根据类型来选择Realm
    /**
     * @author chenzhi
     * @Description: 自定义当使用多realm时管理器
     * @Date:Created: in 13:41 2018/8/13
     * @Modified by:
     */
    public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
     
        @Override
        protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) {
            //先判断Realm是否为空
            assertRealmsConfigured();
            //强转为自定义的Token
            MyUsernamePasswordToken myUsernamePasswordToken = (MyUsernamePasswordToken) authenticationToken;
            //拿到登录类型
            String loginType = myUsernamePasswordToken.getLoginType();
            //拿到所有Realm集合
            Collection<Realm> realms = getRealms();
            List<Realm> myrealms = new ArrayList<>();
            //遍历每个realm 根据loginType将对应的Reaml加入到myrealms
            for (Realm realm : realms) {
                //拿到Realm的类名 ,所以在定义Realm时,类名要唯一标识并且包含枚举中loginType某一个Type
                //注意:一个Realm的类名不能包含有两个不同的loginType
                if (realm.getName().contains(loginType)) {
                    myrealms.add(realm);
                }
            }
            //判断是单Reaml还是多Realm
            if (myrealms.size() == 1) {
                return doSingleRealmAuthentication(myrealms.iterator().next(), myUsernamePasswordToken);
            } else {
                return doMultiRealmAuthentication(myrealms, myUsernamePasswordToken);
            }
        }
    }
    

    认证流程

    token=new UsernamePasswordToken(userName,password)
    
     
  • 相关阅读:
    Linux服务器管理: 系统的定时任务crond
    Nmon的安装使用及获取报表
    笔记:LoadRunner性能测试巧匠训练营
    python-解决安装MySQL-python出现的: Python version 2.7 required,which was not found in the registry
    JMeter监控内存及CPU——plugin插件监控被测系统资源方法
    Linux监控
    SSL与TLS的区别以及介绍
    [存]Jmeter 如何实现跨线程组传递参数
    Robot Framework简介
    [转]Appium搭建六:安装Android模拟器
  • 原文地址:https://www.cnblogs.com/ma13461749958/p/14235346.html
Copyright © 2020-2023  润新知