zyxs

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;
}
}

 

posted on 2018-01-22 22:01 张先生~ 阅读(...) 评论(...) 编辑 收藏

相关文章: