zoukankan      html  css  js  c++  java
  • SpringBoot整合shiro实现用户的认证授权

    • * 项目环境搭建
    • * 配置ShiroConfig,用于shiro的基本配置和注入自定义规则
    • * 实现自定义的realm,继承AuthorizingRealm
    • * 编写测试controller和页面
    1. 基本环境准备
    2. 导入依赖坐标
    3. maven管理、shiro1.4.0 和spring-shiro1.4.0依赖
    4. 导入数据源,配置thymeleaf,redis,等等
    1. shiro配置
    2. 配置shiroConfig
    3. 编写自定义的realm
    4. 实现具体的doGetAuthorizationInfo(授权)方法和doGetAuthenticationInfo(认证)

    具体实现:

    shiroConfig配置

    @Bean(name = "securityManager")
        public DefaultWebSecurityManager securityManager(@Qualifier("myRealm") MyRealm myRealm){
            DefaultWebSecurityManager ds = new DefaultWebSecurityManager();
            ds.setRealm(myRealm);
            return ds;
        }
        @Bean
        public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager){
            ShiroFilterFactoryBean sf = new ShiroFilterFactoryBean();
            //设置安全管理器
            sf.setSecurityManager(defaultSecurityManager);
            sf.setLoginUrl("/login");
            sf.setUnauthorizedUrl("/non");
            sf.setSuccessUrl("/index");
            /**
             * 自定义过滤器
             * anon:
             * authc:
             * user: 只有实现了remberme的操作才能访问
             * perms: 必须得到资源权限才能访问
             * role: 必须得到角色权限的时候才能访问
             */
            Map<String,String> china = new LinkedHashMap<>();
            china.put("/index","authc");
            china.put("/update","authc");
            china.put("/non","authc");
            china.put("/toLogin","anon");
            china.put("/add","perms[user:add]");
            china.put("/**","anon");
            sf.setFilterChainDefinitionMap(china);
            return sf;
        }
        @Bean(name = "myRealm")
        public MyRealm getRealm(){
            return new MyRealm();
        }
    

    自定义编写realm 继承AuthorizingRealm 实现doGetAuthorizationInfo 和doGetAuthenticationInfo方法

    /**
         * 自定义授权逻辑
         * Authorization
         *
         * @param principalCollection
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            //给资源授权
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            //加上授权字符串(当前用户已经授权)
            simpleAuthorizationInfo.addStringPermission("user:add");
            return simpleAuthorizationInfo;
        }
        /**
         * 自定义认证的逻辑
         *  判断用户名和密码
         *
         * @param authenticationToken
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    		//直接强转
            UsernamePasswordToken auth = (UsernamePasswordToken) authenticationToken;
            System.out.println("处理用户登录逻辑");
            char[] password = auth.getPassword();
            String username = auth.getUsername();
            if(!"user".equals(username)){
                //返回null 会抛出 UnknownAccountException
                return null;
            }
    		//用户传入的密码 数据库中加载出来的密码
            return new SimpleAuthenticationInfo(password,"123","");
        }
    

     首先配置shiro的配置类使用@Configuration注解类上,这里面我们需要基本的三个配置类
        @Bean
        ShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager)

        @Bean(name = "securityManager")
        DefaultWebSecurityManager (@Qualifier("myRealm") MyRealm myRealm)

        @Bean(name = "myRealm")
        MyRealm()

        第一个是可以自定义认证授权规则,配置权限拦截规则指定跳转页面
        第二个是将shiro的安全管理器注入,然后返回
        第三种是自定义实现自己认证授权逻辑

    自定义realm
    继承自AuthorizingRealm,实现其中的两个方法
    `protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)`

    1. 此方法是授权逻辑的实现,指定用户可以拥有那些权限,将其set到addStringPermission集合中即可
    2. 其中授权类是其的一个子类
    3. SimpleAuthorizationInfo simpleAuthorizationInfo

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
    

      此方式认证逻辑,用于判断用户是否登录成功,或者异常     
        用户名判断错误返回 null 
        密码使用new SimpleAuthenticationInfo(用户输入的密码,数据库的密码,"");

    举个例子:

    /**
         * 自定义授权逻辑
         * Authorization
         *
         * @param principalCollection
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            //给资源授权
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            //加上授权字符串(当前用户已经授权)
            simpleAuthorizationInfo.addStringPermission("user:add");
            return simpleAuthorizationInfo;
        }
    
        /**
         * 自定义认证的逻辑
         *  判断用户名和密码
         *
         * @param authenticationToken
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            UsernamePasswordToken auth = (UsernamePasswordToken) authenticationToken;
            System.out.println("处理用户登录逻辑");
            char[] password = auth.getPassword();
            String username = auth.getUsername();
            if(!"user".equals(username)){
    		//返回null 会抛出 UnknownAccountException
                return null;
            }
            return new SimpleAuthenticationInfo(password,"123","");
        }
        /**
         * 自定密码判断
         *
         * @param pass
         * @return
         */
        private boolean isPassWord(char[] pass){
            String password = "123";
            if(pass.length != password.length()){
                return false;
            }
            char[] chars = password.toCharArray();
            for(int i = 0; i < pass.length; i++){
                if(chars[i] != pass[i]){
                    return false;
                }
            }
            return true;
        }

    // 当然授权也可以在controller层中通过@RequiresPermissions("user:update")注解在当前用户操作的地址上授权
    <p style="color:red">
        注意: 这里面需要在shiroconfig中配置以下内容:
    </p>
        
        /**
         * 解决@RequiresPermissions("XX:XXX:...")注解无效
         *
         * @return
         */
        @Bean("lifecycleBeanPostProcessor")
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }

        @Bean
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
            proxyCreator.setProxyTargetClass(true);
            return proxyCreator;
        }

        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager defaultSecurityManager) {
            AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
            advisor.setSecurityManager(defaultSecurityManager);
            return advisor;
        }

      并且在controller里面配置的权限检验,用户验证失败,会跳转到/error页面上,需要自己自定义页面

    注意:
    这里面有个大坑,开始使用如下代码发现并未解决问题

        @Bean
        public SimpleMappingExceptionResolver simpleMappingExceptionResolver(){
            SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
            Properties properties = new Properties();
            properties.put("org.apache.shiro.authz.UnauthorizedException","/non");
            simpleMappingExceptionResolver.setExceptionMappings(properties);
            return simpleMappingExceptionResolver;
        }

    最后通过springMVC的异常处理类指定跳转页面,才解决此问题。

        @ControllerAdvice
        public class MyExceptionHandler {
        
            @ExceptionHandler(UnauthenticatedException.class)
            public String nauthenticatedException(Model model){
                model.addAttribute("errorMsg","当前无用户!");
                return "nonauth";
            }
            @ExceptionHandler(UnauthorizedException.class)
            public String nauthorizedException(Model model){
                model.addAttribute("errorMsg","当前用户没有权限");
                return "nonauth";
            }
        }

    不错
    现在已经解决了两个问题:

    1. 在用户未登录的时候,直接访问带有@RequiresPermissions权限的注解时会报错,而不是跳转到指定页面,或者返回相应的信息
    2. 未使用在remal配置的权限过滤器的时候,而是在@Controller上直接带有@RequiresPermissions("user:add") 会报500错误,而不是可控操作


    在doGetAuthenticationInfo方法中,获取用户登录时候的信息操作,验证用户,将用户存入session,以后通过subject对象强转。
        

         SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                    user,
                    user.getPassword(),
                    ByteSource.Util.bytes(salt),
                    //realm name
                    getName());

        user 是开始通过用户名查询到的用户信息
        salt是加盐处理,硬编码加盐处理

    #shiro密码匹配#
        
    这里先来个简单的直接加盐 然后使用MD5加密方式
    密码匹配则时先将用户输入的密码加盐再字符串比较

        实现自定义的Realm 继承自AuthorizingRealm
        
        doGetAuthenticationInfo方法中使用
    
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
            用户对象
                    user,
            数据库用户密码
                    user.getPassword(),
            加盐处理
                    ByteSource.Util.bytes(salt),
                    //realm name
                    getName());    
        
        将用户对象保存至
            String string = MD5Util.encryptString(password+salt);
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(userName,string);

    使用shiro的HashedCredentialsMatcher自定义密码加密    
    先模拟用户和密码数据 使用单元测试:

        @Test
        public void contextLoads() {
            String algorithmName = "md5";
            String username = "admin";
            String password = "123";
            String salt1 = username;
            String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex();
            int hashIterations = 3;
            SimpleHash hash = new SimpleHash(algorithmName, password,
                    salt1 + salt2, hashIterations);
            String encodedPassword = hash.toHex();
            System.out.println(encodedPassword);
            System.out.println(salt2);
        } 

    将生成的数据保存到数据库中,验证的时候将密码和盐拿出来,这时候,用户的注册名和密码在注册的时候保存到数据库中

    在Shiro的配置类中,注入

        /**
         * 凭证匹配器
         * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
         *  所以我们需要修改下doGetAuthenticationInfo中的代码;
         * )
         * @return
         */
        @Bean(name = "hashedCredentialsMatcher")
        public HashedCredentialsMatcher hashedCredentialsMatcher(){
            HashedCredentialsMatcher hashedCredentialsMatcher = new MyHashedCredent();
            //使用指定的散列算法
            hashedCredentialsMatcher.setHashAlgorithmName("MD5");
            //几次散列?
            hashedCredentialsMatcher.setHashIterations(3);
            //设置16进制编码
            hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
            return hashedCredentialsMatcher;
        }
    

      


    在将其自定义的密码加载类注入到自定义的realm中

        @Bean(name = "myRealm")
        public MyRealm getRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher hashedCredentialsMatcher){
            MyRealm myRealm = new MyRealm();
            // 设置自定义加密
            myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
            return myRealm;
        }

    下面实现自定义密码:
        

        
        /**
         * @author zhangyi
         * @date 2018/12/12 20:43
         */
        public class MyHashedCredent extends HashedCredentialsMatcher {
        public MyHashedCredent(){}
        /**
         * 以后放到redis中保存
         */
        private Cache<String, AtomicInteger> passwordRetryCache;
    
        public MyHashedCredent(CacheManager cacheManager) {
            passwordRetryCache = cacheManager.getCache("passwordRetryCache");
        }
        @Override
        public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
            //限制每个用户的请求登录次数(密码错误的时候)
            String username = (String) token.getPrincipal();
            if(!Objects.isNull(passwordRetryCache)) {
                // retry count + 1
                AtomicInteger retryCount = passwordRetryCache.get(username);
                if (retryCount == null) {
                    retryCount = new AtomicInteger(0);
                    passwordRetryCache.put(username, retryCount);
                }
                if (retryCount.incrementAndGet() > 5) {
                    // if retry count > 5 throw
                    throw new ExcessiveAttemptsException();
                }
            }
            boolean matches = super.doCredentialsMatch(token, info);
            if (matches) {
                // clear retry count
        //     passwordRetryCache.remove(username);
            }
            return matches;
        }
        }
    

      


    按照开涛神的方法,计算其密码重试的次数,将其保存到EhCache中,这里我保存到redis,因为好用吧。
    开始使用硬编码加盐,网上说可能会有安全问题,现在采用
        
        用户名+密码+随机数 --> 在N次散列 加密,应该好一点点

    我这里是继承了HashedCredentialsMatcher这中加密方式,还可以继承SimpleCredentialsMatcher,或者继承PasswordMatcher,或者实现CredentialsMatcher接口来加密,不过本质上差不多

    平凡是我的一个标签
  • 相关阅读:
    SQL SERVER 2005添加用户和编辑sa
    数组型参数和数组的区别
    Oracle数据库建库、建表空间,建用户
    oracle表空间操作详解
    Oracle10g的完全卸载(转载)
    Delphi format的用法
    AnImateWindow用法
    文本文件操作
    TStringList的用法
    Delphi网络函数
  • 原文地址:https://www.cnblogs.com/guyanzy/p/10400626.html
Copyright © 2011-2022 走看看