• Springboot-shiro-redis实现登录认证和权限管理


    Springboot-shiro-redis实现登录认证和权限管理

      在学习之前:

      首先进行一下Apache Shiro和Shiro比较:

      Apache Shiro是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。Apache Shiro的首要目标是易于使用和理解。安全通常很复杂,甚至让人感到很痛苦。

      但是Shiro却不是这样子的。一个好的安全框架应该屏蔽复杂性,向外暴露简单、直观的API,来简化开发人员实现应用程序安全所花费的时间和精力。

      Shiro能做什么呢?

    • 验证用户身份

    • 用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限

    • 在非 web 或 EJB 容器的环境下可以任意使用Session API

    • 可以响应认证、访问控制,或者 Session 生命周期中发生的事件

    • 可将一个或以上用户安全数据源数据组合成一个复合的用户 "view"(视图)

    • 支持单点登录(SSO)功能

    • 支持提供“Remember Me”服务,获取用户关联信息而无需登录。

      开始代码:

      pom包依赖:

      <properties>
        <shiro.version>1.4.0</shiro.version>
      </properties>
      <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>${shiro.version}</version>
      </dependency>
      <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-ehcache</artifactId>
        <version>${shiro.version}</version>
      </dependency>
      <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-web</artifactId>
        <version>${shiro.version}</version>
      </dependency>
      <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-cas</artifactId>
        <version>${shiro.version}</version>
      </dependency>
      <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>${shiro.version}</version>
      </dependency>
      <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-quartz</artifactId>
        <version>${shiro.version}</version>
      </dependency>

      /**
      * 自定义认证器,区分ajax请求
      * @author zxs 2018年1月22日21:45:55
      */

      public class RoleAuthorizationFilter extends AuthorizationFilter {
    • public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
                throws IOException {

            Subject subject = getSubject(request, response);
            String[] rolesArray = (String[]) mappedValue;

            if (rolesArray == null || rolesArray.length == 0) {
                // n
                // o roles specified, so nothing to check - allow access.
                return true;
            }

            Set<String> roles = CollectionUtils.asSet(rolesArray);
            for (String role : roles) {
                if (subject.hasRole(role)) {
                    return true;
                }
            }
            return false;
        }

        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {

            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            Subject subject = getSubject(request, response);

            if (subject.getPrincipal() == null) {
                if ("XMLHttpRequest".equalsIgnoreCase(httpServletRequest.getHeader("X-Requested-With"))) {
                    httpServletResponse.setCharacterEncoding("UTF-8");
                    httpServletResponse.setHeader("Charset","UTF-8");
                    PrintWriter out = httpServletResponse.getWriter();

                    CommonResult result = new CommonResult(false);
                    result.setCode("401");
                    result.setMsg("请重新登录");

                    out.write(JSON.toJSONString(result));
                    out.flush();
                    out.close();
                } else {
                    String unauthorizedUrl = getUnauthorizedUrl();
                    WebUtils.issueRedirect(request, response, unauthorizedUrl);
                }
            } else {
                if ("XMLHttpRequest".equalsIgnoreCase(httpServletRequest.getHeader("X-Requested-With"))) {
                    httpServletResponse.setCharacterEncoding("UTF-8");
                    httpServletResponse.setHeader("Charset","UTF-8");
                    PrintWriter out = httpServletResponse.getWriter();

                    CommonResult result = new CommonResult(false);
                    result.setCode("403");
                    result.setMsg("没有足够的权限: "+((HttpServletRequest) request).getServletPath());

                    out.println(JSON.toJSONString(result));
                    out.flush();
                    out.close();
                } else {
                    String unauthorizedUrl = getUnauthorizedUrl();
                    if (StringUtils.hasText(unauthorizedUrl)) {
                        WebUtils.issueRedirect(request, response, unauthorizedUrl);
                    } else {
                        WebUtils.toHttp(response).sendError(403);
                    }
                }
            }
            return false;
        }
      }

      ShiroConf:shiro配置类,Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。/** * shiro配置

       * @author zxs 2018年1月22日21:10:37
      */
      @Configuration
      public class ShiroConf {
        @Bean
        public FilterRegistrationBean filterRegistrationBean() {
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
            filterRegistrationBean.setFilter(new DelegatingFilterProxy("shiroFilter"));
            filterRegistrationBean.addInitParameter("targetFilterLifecycle", "true");
            filterRegistrationBean.setEnabled(true);
            filterRegistrationBean.addUrlPatterns("/*");
            return filterRegistrationBean;
        }
        //密码验证方式 数据库保存的密码是使用sha算法加密的,所以这里需要配置一个密码匹配对象
        @Bean
        public RetryLimitHashedCredentialsMatcher credentialsMatcher() {
            RetryLimitHashedCredentialsMatcher credentialsMatcher = new RetryLimitHashedCredentialsMatcher();
            credentialsMatcher.setHashAlgorithmName("sha");
            credentialsMatcher.setHashIterations(2);
            credentialsMatcher.setStoredCredentialsHexEncoded(true);//是否存储散列后的密码为16进制,需要和生成密码时的一样
            credentialsMatcher.setRetryCount(5);
            credentialsMatcher.setRetryTime(1800000);
            return credentialsMatcher;
        }
        //根据用户名和密码校验登陆
        @Bean
        public UsernameRealm usernameRealm(RetryLimitHashedCredentialsMatcher credentialsMatcher) {
            UsernameRealm usernameRealm = new UsernameRealm();
            usernameRealm.setCredentialsMatcher(credentialsMatcher);
            usernameRealm.setCachingEnabled(true);
            return usernameRealm;
        }
        //配置 Bean 后置处理器: 会自动的调用和 Spring 整合后各个组件的生命周期方法. -->
        @Bean
        public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
            return new LifecycleBeanPostProcessor();
        }

        @Bean
        public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
            DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator();
            daap.setProxyTargetClass(true);
            return daap;
        }
        // 调用我们配置的权限管理器  
        @Bean
        public DefaultWebSecurityManager securityManager(UsernameRealm usernameRealm) {
            DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
            dwsm.setRealm(usernameRealm);
            return dwsm;
        }

        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager defaultWebSecurityManager) {
            AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor();
            aasa.setSecurityManager(defaultWebSecurityManager);
            return aasa;
        }

        @Bean(name = "shiroFilter")
        public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager, ApplicationContext context) {
            ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
            shiroFilterFactoryBean.setSecurityManager(securityManager);
            shiroFilterFactoryBean.setLoginUrl("/sys/auth");//这里是设置登录路径
            shiroFilterFactoryBean.setUnauthorizedUrl("/sys/auth/logout");//您请求的资源不再您的权限范围,则跳转到这里

            Map<String, Filter> filters = new LinkedHashMap();
      //       filters.put("logout", logoutFilter);
            filters.put("roles", new RoleAuthorizationFilter());

            shiroFilterFactoryBean.getFilters().putAll(filters);//加载自定义拦截器

            SysResService resService = context.getBean(SysResService.class);//只有通过这种方式才能获得resService,因为此处会优先于resService实例化
            loadShiroFilterChain(shiroFilterFactoryBean,resService);//加载拦截规则
            return shiroFilterFactoryBean;
        }

        private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean,SysResService resService) {
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
            //默认拦截规则
            //authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
            filterChainDefinitionMap.put("/logout", "logout");
            filterChainDefinitionMap.put("/sys/auth/login", "anon");
            filterChainDefinitionMap.put("/assets/**", "anon");
            filterChainDefinitionMap.put("/data/**", "anon");
            filterChainDefinitionMap.put("/images/**", "anon");
            filterChainDefinitionMap.put("/js/**", "anon");
            filterChainDefinitionMap.put("/plugins/**", "anon");
            filterChainDefinitionMap.put("/winline/**", "anon");

            filterChainDefinitionMap.put("/sys/auth/**", "anon");
            filterChainDefinitionMap.put("/file/**", "anon");

            filterChainDefinitionMap.put("/error/403", "anon");
            filterChainDefinitionMap.put("/error/404", "anon");
            filterChainDefinitionMap.put("/error/500", "anon");

            //用户自定义拦截规则
            filterChainDefinitionMap = resService.loadFilterChainDefinitions(filterChainDefinitionMap);
            //过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;

            //都不满足的时候,需要超级管理员权限才能访问
            filterChainDefinitionMap.put("/**", "roles[ROLE_SUPER]");
      shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        }

        /*@Bean
        public ShiroDialect shiroDialect() {//thymeleaf 集成shiro使用,如果没有可以删除
            return new ShiroDialect();
        }*/
      }

      定义用户过滤器:


      /**
      * 自定义用户过滤器
      * @author zy 2018年1月22日21:10:40
      *
      *
      */
      public class SysUserFilter extends PathMatchingFilter {
        @Autowired
        private SysUserService sysUserService;

        @Override
        protected boolean onPreHandle(ServletRequest req, ServletResponse rep, Object mappedValue)
                throws Exception {
            String username = (String) SecurityUtils.getSubject().getPrincipal();
             
            HttpServletRequest request = (HttpServletRequest) req;
            HttpSession session = request.getSession();
            //在session域中加入当前登陆的用户信息
            SysUser sysUser = (SysUser) session.getAttribute(SystemConstant.SYS_CURRENT_USER);
            if(sysUser == null){
                session.setAttribute(SystemConstant.SYS_CURRENT_USER, sysUserService.getByUsername(username));
            }

            return super.onPreHandle(req, rep, mappedValue);
        }
      }

      根据用户名和密码校验登陆:

      我们的应用程序中要做的就是自定义一个Realm类,继承AuthorizingRealm抽象类,重载doGetAuthenticationInfo(),重写获取用户信息的方法。在这个方法中主要是使用类:SimpleAuthorizationInfo进行角色的添加和权限的添加。


    /**
    * 根据用户名和密码校验登陆
    *
    * @author zxs 2018年1月22日21:18:39
    */
    public class UsernameRealm extends AuthorizingRealm {
      /* 实现Realm类MyShiro继承自AuthorizingRealm,AuthorizingRealm实现它的抽象方法doGetAuthorizationInfo权限角色进行配置,AuthorizingRealm又继承自AuthenticatingRealm,AuthenticatingRealm也有一个抽象方法doGetAuthenticationInfo,实现doGetAuthenticationInfo方法对登录的令牌等信息进行验证。*/

      /**
        * 系统用户service
        */
      @Autowired
      @Lazy
      private SysUserService sysUserService;

      /**
        * 加载用户授权信息, 包括权限资源和角色用户组资源
        *
        * @author zxs 2018年1月22日21:31:39
        */
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

          // 登陆名
          String username = (String) principals.getPrimaryPrincipal();
          if (username != null) {


              //权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
              SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

              Set<String> roles = sysUserService.loadEnabledRolesByUsername(username);

              if (roles.contains(SystemConstant.ROLE_SUPER)) {
                  authorizationInfo.addStringPermission("*");
              } else {
              // 加载权限资源
             
                  authorizationInfo.setStringPermissions(sysUserService.loadEnabledPermissionsByUsername(username));
              }

              // 加载角色/用户组
              authorizationInfo.setRoles(roles);

              return authorizationInfo;
          }
          return null;
      }

      /**通过SimpleAuthenticationInfo将盐值以及用户名和密码信息封装到AuthenticationInfo中,进入证书凭证类中进行校验
        * 加载用户身份认证信息
        *
        * @author zxs 2018年1月22日21:31:39
        */
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
          String username = (String) token.getPrincipal();

          // 获取账号信息
          SysUser sysUser = sysUserService.getByUsername(username);

          if (sysUser == null) {
              throw new UnknownAccountException();   // 没找到帐号
          }

          if (sysUser.getStatus() == DataStatus.LOGIC_DELETE.getValue()) {
              throw new UnknownAccountException();   // 没找到帐号
          }

          if (sysUser.getStatus() == DataStatus.DISABLE.getValue()) {
              throw new LockedAccountException();   // 帐号锁定
          }

          SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(sysUser.getUsername(),
                  sysUser.getPwd(), ByteSource.Util.bytes(sysUser.getSalt()),
                  getName());
          // 此处无需比对,比对的逻辑Shiro会获取到数据库的用户名和密码
          //我们只需返回一个和令牌相关的正确的验证信息,
       
          return authenticationInfo;
      }

    }

    登录控制器:登录过程其实只是处理异常的相关信息,具体的登录验证交给shiro来处理.

    @RequestMapping("login")
    public ModelAndView login(String username, String password, boolean rememberMe, HttpSession session, Model model, HttpServletRequest request) {
      ModelAndView mv = new ModelAndView("redirect:/my/index");
      try {
          Subject subject = SecurityUtils.getSubject();
          UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
          //提交申请 调用到Subjectsubject = securityManager.login(this, token);方法后,则跳转到自定义Realm中
          subject.login(token);

          SysUser user = userService.getByUsername(username);
          user.setLastLoginTime(new Date());
          user = userService.save(user);
          //在session中保存当前用户的个人信息
          session.setAttribute(SystemConstant.SYS_CURRENT_USER, user);
          //记录登录信息
          operLogService.login(username, IpUtils.getRemoteHost(request));
          //获取登陆前访问的页面
          SavedRequest savedRequest = WebUtils.getSavedRequest(request);
          System.out.println("获取登陆前访问的页面"+savedRequest);
          if (savedRequest != null) {
              String requestUrl = savedRequest.getRequestUrl();
              if (StringUtils.isNoneBlank(requestUrl)) {
                  mv.setViewName("redirect:"+requestUrl);
              }
          }
      } catch (Exception e) {
          mv.setViewName("forward:/sys/auth");
          mv.addObject("errMsg","用户名或密码错误");
          mv.addObject("username",username);
          mv.addObject("password","password");
          mv.addObject("rememberMe","rememberMe");
          Logger.error(AuthController.class,e.getMessage(), e.getStackTrace());
      }
      return mv;
    }

    密码验证方式:实现了在五分钟内 用户五次输入密码的机会。

    /**
    * 密码验证方式
    * @author zxs 2018年1月22日21:28:55
    */
    public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {

      @Autowired
      private RedisTemplate<Serializable,AtomicInteger> redisTemplate;

      private final String PREFIX_USER_RETRY_COUNT = "_RETYR_";

      /**在单位时间内连续尝试登录的限制次数*/
      private Integer retryCount = 5;
      /**redisTemplateg过期时间300s*/
      private Integer retryTime = 300 ;

      private Cache<String, AtomicInteger> passwordRetryCache;

      @Override
      public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
          String username = (String)token.getPrincipal();
          //retry count + 1 线程安全,多个线程共享变量
          AtomicInteger retryCount = redisTemplate.opsForValue().get(PREFIX_USER_RETRY_COUNT+username);

          //如果登录成功,那么这个count就会从缓存中移除,从而实现了如果登录次数超出指定的值就锁定。
          if(retryCount != null) {
              if(retryCount.incrementAndGet() > 5) {//连续尝试+1登录次数异常(incrementAndGet返回新值,而getAndIncrement返回旧值)
              throw new ExcessiveAttemptsException();
              }

              redisTemplate.opsForValue().set(PREFIX_USER_RETRY_COUNT+username,retryCount,retryTime, TimeUnit.SECONDS);
              System.out.println("redisTemplate.opsForValue():"+redisTemplate.opsForValue().get(PREFIX_USER_RETRY_COUNT+username));


          }else{
              retryCount = new AtomicInteger(1);
              redisTemplate.opsForValue().set(PREFIX_USER_RETRY_COUNT+username,retryCount,300, TimeUnit.SECONDS);

          }
          boolean matches = super.doCredentialsMatch(token, info);
          System.out.println("matches:"+matches);
          if(matches) {
              //clear retry count 验证成功即删除
              redisTemplate.delete(PREFIX_USER_RETRY_COUNT+username);
              System.out.println("redisTemplate.opsForValue()清空了:"+redisTemplate.opsForValue().get(PREFIX_USER_RETRY_COUNT+username));

          }
          return matches;
      }

      public Integer getRetryCount() {
          return retryCount;
      }

      public void setRetryCount(Integer retryCount) {
          this.retryCount = retryCount;
      }

      public Integer getRetryTime() {
          return retryTime;
      }

      public void setRetryTime(Integer retryTime) {
          this.retryTime = retryTime;
      }
    }

    账号加密:

    /**
    * 账号加密
    * @author zxs 2018年1月22日21:29:26
    */
    public class Encryp {

    /** 随机字符生产工具 */
    private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();

    /** 加密方式 */
    @Value("${shiro.password.algorithmName}")
    private String algorithmName = "sha";

    /** 多重加密次数 */
    @Value("${shiro.password.hashIterations}")
    private int hashIterations = 2;

    /**
    * 配置随机字符生产工具
    * @param randomNumberGenerator
    *
    * @author zxs 2018年1月22日21:30:00
    */
    public void setRandomNumberGenerator(RandomNumberGenerator randomNumberGenerator) {
    this.randomNumberGenerator = randomNumberGenerator;
    }

    /**
    * 配置加密方式
    * @param algorithmName
    *
    * @author zxs 2018年1月22日21:30:00
    */
    public void setAlgorithmName(String algorithmName) {
    this.algorithmName = algorithmName;
    }

    /**
    * 配置重复加密次数
    * @param hashIterations
    *
    * @author zxs 2018年1月22日21:30:00
    */
    public void setHashIterations(int hashIterations) {
    this.hashIterations = hashIterations;
    }

    /**
    * 密码加密
    * @param user 用户信息
    *
    * @author zxs 2018年1月22日21:30:00
    */
    public void encryptPassword(SysUser user) {
    user.setSalt(randomNumberGenerator.nextBytes().toHex());

    String newPassword = new SimpleHash(algorithmName, user.getPwd(), ByteSource.Util.bytes(user.getSalt()), hashIterations).toHex();

    user.setPwd(newPassword);
    }
    /**
    * 根据私钥加密
    * @param value 要加密字段
    * @param salt 密钥
    * @author zxs 2018年1月22日21:30:00
    * @return 加密后字段
    */
    public String encrypt(String value , String salt) {
    return new SimpleHash(algorithmName, value, ByteSource.Util.bytes(salt), hashIterations).toHex();
    }

    /**
    * 获取加密后的新密码
    *
    * @param pwd 密码
    * @param salt 盐
    * @return 新密码
    * @author zxs 2018年1月22日21:30:00
    */
    public String getEncryptPassword(String pwd,String salt){
    String newPassword = new SimpleHash(algorithmName, pwd, ByteSource.Util.bytes(salt), hashIterations).toHex();

    return newPassword;
    }
    }

     

  • 相关阅读:
    IntelliJ IDEA AndroidStudio SVN无法使用
    三极管封装
    STC等单片机一开机就停电模式烧写程序办法
    CC2541设置中断输入模式
    C# WinForm 多线程 应用程序退出的方法 结束子线程
    CorelDrawX8安装时提示已安装另一个版本
    Win10下Prolific USB-to-Serial Comm Port驱动提示不能使用
    Keil5创建GPIO
    SQL行列转置
    Excel复制粘贴假死
  • 原文地址:https://www.cnblogs.com/zyxs/p/8331363.html
Copyright © 2020-2023  润新知