ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Shiro

2022-07-27 20:35:13  阅读:151  来源: 互联网

标签:缓存 return Shiro new public shiro user


Shiro

Maven依赖:

<!-- 项目依赖,根据情况二选一 -->
<!-- 普通项目 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</dependency>
<!-- SpringBoot整合 -->
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring-boot-starter</artifactId>
</dependency>

<!-- 缓存依赖,根据情况二选一,第二种是在springboot -->
<!-- shiro的默认缓存ehcache -->
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-ehcache</artifactId>
</dependency>
<!-- redis整合springboot -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

1.核心架构

核心为Security Manager,几乎所有功能均为其实现。

  • Authenticator:认证。

  • Authorizer:授权。

  • Session Manager:Web环境下,管理shiro会话。

  • Session DAO:对会话数据进行CRUD(对Session Manager中的数据)。

  • Cache Manager:缓存认证、授权的数据。

  • Realms:域。具体做认证、授权的标配

Cryptography:密码生成器,封装好了相应算法,用于对密码加密。SHA256、MD5等。一般采用MD5做哈希散列。

2.认证流程

主体(Subject)将身份信息(Principal)和凭证信息(Credential)传给shiro,shiro将其封装为token,token通过Security Manager进行验证(调用Authenticator,Authenticator调用Realm)。

2.1.基本Demo示例

核心代码:

// 1.创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();

// 2.给安全管理器设置Realm
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));

// 3.SecurityUtils 给全局安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);

// 4.关键对象 subject 主体
Subject subject = SecurityUtils.getSubject();

// 5.创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123456");

// 6.用户认证
try {
//System.out.println("用户认证前状态:" + subject.isAuthenticated());
subject.login(token);
//System.out.println("用户认证后状态:" + subject.isAuthenticated());
} catch (Exception e) {
e.printStackTrace();
}

ps:login认证失败时,若为无当前账户会报UnknownAccountException,否则密码错误会报IncorrectCredentialsException。

3.2.主要源码解析

最终执行用户名比较是SimpleAccountRealm的doGetAuthenticationInfo方法,完成用户名校验。最终密码校验是在AuthenticatingRealm中的assertCredentialsMatch方法(SimpleCredentialsMatcher的doCredentialsMatch方法,拿到token和account的credentials后默认equal比较,后面需要修改为密码比较方法)。(重写Realm,需要我们实现doGetAuthenticationInfo方法,而密码不需要我们实现。)

AuthenticatingRealm中doGetAuthenticationInfo方法认证realm,AuthorizingRealm中doGetAuthorizationInfo方法授权realm。

3.3.自定义realm

实现AuthorizingRealm抽象类doGetAuthenticationInfo方法和doGetAuthorizationInfo方法。

  1. doGetAuthenticationInfo方法实现

可返回SimpleAuthenticationInfo或者SimpleAccount。

返回null,login时报UnknownAccountException;返回空参SimpleAuthenticationInfo,login时报AuthenticationException。

举例实现:

/**
* 认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

   // 在token中获取用户名
   String principal = (String) authenticationToken.getPrincipal();

   // 根据身份信息使用jdbc mybatis查询相关数据库
   if ("xiaochen".equals(principal)) {
       // 参数1数据库中正确的用户名 参数2数据库中的正确密码 参数3当前realm的名字
       SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, "123", this.getName());
       return simpleAuthenticationInfo;
  }
   
   return null;
}
  1. 密码加密

AuthenticatingRealm的credentialsMatcher属性为比较方法,默认equals,可以通过realm的setCredentialsMatcher方法进行设置,可设置以下凭证匹配器:

以下,实现对密码进行MD5+salt+hash散列存储(salt默认拼接在原字符串后面;hash散列默认1次,一般设置为1024或者2048次):

  • 修改凭证匹配器

// 新建hash凭证匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 设置算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 设置realm使用hash凭证匹配器
realm.setCredentialsMatcher(hashedCredentialsMatcher);
  • 添加随机盐salt

添加随机盐(随机盐保存至数据库,以下举例耦合死)需要在自定义realm当中,对返回的AuthenticationInfo设置credentialsSalt,即在第三个参数添加盐值,添加方法举例如下:

SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,
                   "c15be9a15a0a238084e0c5a846f3a7b4",
                   ByteSource.Util.bytes("x0*7ps"), // 盐值
                   this.getName());
  • 添加散列次数

调用hash凭证匹配器的setHashIterations方法进行设置。

hashedCredentialsMatcher.setHashIterations(1024);

ps:shiro集成了获得md5加密后的值的算法,如下:

// 参数1加密明文,参数2盐,参数3散列次数
Md5Hash md5Hash = new Md5Hash("123", "x0*7ps", 1024);
// toHex 转为String
System.out.println(md5Hash.toHex());

3.授权流程

在完成shiro鉴权之后,就可进行授权了。

  1. 授权的关键对象:

  • 主体(Subject)

  • 资源(Resource):包括资源类型(一类资源)和资源实例(一类资源中的某一个)。

  • 权限/许可(Permission)

  1. 授权方式:

  • RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制

if (subject.hasRole("admin")) {
// 操作什么资源
}
  • RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制

if (subject.isPermission("user:update:01")) { // 资源实例
// 对01用户进行修改
}
if (subject.isPermission("user:update:*")) { // 资源类型
// 对所有用户进行修改
}

ps:权限字符串规则是资源标识符:操作:资源实例标识符,意思是对哪个资源的那个实例有什么操作。

  1. 实现方式

  • 编程式

if (subject.hasRole("admin")) {
// 有权限
} else {
   // 无权限
}
  • 注解式

@RequiredRoles("admin")
public void hello() {
// TODO 实现方法
}
  • 标签式

<!- JSP/GSP标签:在JSP/GSP页面通过相应的标签完成 ->
<shiro:hasRole name="admin">
<!- 有权限 ->
</shiro:hasRole>
<!- ps:Thymeleaf中使用shiro需要额外集成!->

3.1.Demo示例

  1. realm授权

在realm的doGetAuthorizationInfo方法中,返回AuthorizationInfo。

示例代码:

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
   String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();

   // 根据身份信息获取当前的角色信息,以及权限信息
   SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

   // 将数据库中查询角色信息赋值给权限对象
   // 假设这里xiaochen有admin、user角色信息
   simpleAuthorizationInfo.addRole("admin");
   simpleAuthorizationInfo.addRole("user");

   simpleAuthorizationInfo.addStringPermission("user:*:01");
   simpleAuthorizationInfo.addStringPermission("product:create");
   
   return simpleAuthorizationInfo;

}
  1. 核心代码鉴权

// 认证用户进行授权
if (subject.isAuthenticated()) {
   // 1.基于角色权限控制
   // 单角色
   System.out.println(subject.hasRole("admin"));
   // 多角色所有
   System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));
   // 多角色某个
   boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "super", "user"));
   for (boolean aBoolean : booleans) {
       System.out.println(aBoolean);
  }

   // 2.基于权限字符串的访问控制 资源标识符:操作:资源类型
   // 单个
   System.out.println(subject.isPermitted("user:update:01"));
   System.out.println(subject.isPermitted("product:update"));
   // 同时
   System.out.println(subject.isPermittedAll("user:*:01", "order:*:10"));
   // 分别
   boolean[] permitted = subject.isPermitted("user:*:01", "order:*:10");
   for (boolean b : permitted) {
       System.out.println(b);
  }
}

4.SpringBoot整合

4.1.项目权限管理流程

SpringBoot项目关键加入拦截器ShiroFilter。请求发出,ShiroFilter进行拦截,依赖SecurityManager进行认证,对资源进行相应访问。

 

 

4.2.Demo示例

首选需要创建自定义Realm,在这里面做我们的认证(登录鉴权)授权规则。随后,设置Shiro配置,主要完成ShiroFilter、SecurityManager、Realm的创建并交给Spring工厂管理。以后的应用就可以设定相应规则来进行鉴权。

具体权限一般保存至数据库之中,一般按照用户<->角色<->权限<->资源进行设计(以下代码示例均通过数据库访问来赋权/鉴权/认证)。

4.2.1.自定义Realm

示例如下:

/**
* 自定义realm
*/
public class CustomerRealm extends AuthorizingRealm {
   @Override
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
       // 获取身份信息
       String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();

       UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
       List<Role> roles = userService.findRolesByUsername(primaryPrincipal);

       // 赋值授权角色
       if (!CollectionUtils.isEmpty(roles)) {
           SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
           roles.forEach(role -> {
               
               // 赋值角色信息
               simpleAuthorizationInfo.addRole(role.getName());

               // 赋值权限信息
               List<Perms> perms = userService.findPermsByRoleId(role.getId());
               if (!CollectionUtils.isEmpty(perms)) {
                   perms.forEach(perm -> {
                       simpleAuthorizationInfo.addStringPermission(perm.getName());
                  });
              }

          });
           return simpleAuthorizationInfo;
      }
       
       return null;
  }

   @Override
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
       String principal = (String) authenticationToken.getPrincipal();

       // 在工厂中获取service对象
       UserService userService = (UserService) ApplicationContextUtils.getBean("userService");

       User user = userService.findByUsername(principal);

       if (!ObjectUtils.isEmpty(user)) {
           return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), new MySimpleByteSource(user.getSalt()), this.getName());
      }

       return null;
  }
}

示例与普通项目类似,但存在两点区别:1.自定义realm并未交给Spring工厂管理,获取bean时自己实现了ApplicationContextUtils.getBean(),详情参见其它。2.原本的shiro自带的用于加盐的ByteSource牵涉到使用Redis缓存时序列化问题,不可用,自己实现了MySimpleByteSource,详情参见其它。

4.2.2.ShiroConfig

示例代码:

/**
* 用来整合shiro框架相关配置类
*/
@Configuration
public class ShiroConfig {

   // 1.创建ShiroFilter,负责拦截所有请求
   @Bean
   public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
       ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

       // 给filter设置安全管理器
       shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

       // 配置系统受限资源
       Map<String, String> map = new HashMap<>();
       map.put("/user/login", "anon");
       map.put("/user/register", "anon");
       map.put("/register.jsp", "anon");
       map.put("/**", "authc");

       // 默认认证界面路径
       shiroFilterFactoryBean.setLoginUrl("/login.jsp");

       shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

       return shiroFilterFactoryBean;
  }

   // 2.创建安全管理器
   @Bean
   public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
       DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();

       // 给安全管理器设置realm
       defaultWebSecurityManager.setRealm(realm);

       return defaultWebSecurityManager;
  }

   // 3.创建自定义realm
   @Bean
   public Realm getRealm() {
       CustomerRealm customerRealm = new CustomerRealm();
       
       // 修改凭证校验匹配器
       HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
       hashedCredentialsMatcher.setHashAlgorithmName("Md5");
       hashedCredentialsMatcher.setHashIterations(1024);
       customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);

       // 开启缓存管理 Redis
       customerRealm.setCacheManager(new RedisCacheManager());

       customerRealm.setCachingEnabled(true); // 开启全局缓存
       customerRealm.setAuthenticationCachingEnabled(true); // 开启认证缓存
       customerRealm.setAuthorizationCachingEnabled(true); // 开启授权缓存
       customerRealm.setAuthenticationCacheName("authenticationCache"); // 设置缓存在内存的名字,不设置默认为当前realm+.authentication
       customerRealm.setAuthorizationCacheName("authorizationCache");

       return customerRealm;
  }


}

spring-boot-shiro的ShiroFilter依赖SecurityManager,SecurityManager依赖Realm。

对于Realm,配置为自定义CustomerRealm,与凭证校验匹配器修改普通项目类似,不同的是使用了缓存管理。shiro默认的缓存管理为EhCache,为是本地缓存,即在应用内部缓存,当前应用停止/宕机等,依然会重查数据库,不符合现实需求。所以,这里我使用了Redis(如果依旧使用EhCache只需将customerRealm.setCacheManager(new RedisCacheManager())改为customerRealm.setCacheManager(new EhCacheManager()))。RedisCacheManager为自己实现,需要实现CacheManager接口,其中getCache方法,需要返回Cache,这里也是自己实现RedisCache。RedisCache需要实现Cache接口,其中get方法实现从redis中获取值;put方法实现填入值到redis;remove在用户logout时调用,清理缓存;clear为清理所有缓存……

/**
* 自定义shiro缓存管理器
*/
public class RedisCacheManager implements CacheManager {

   /**
    *
    * @param cacheName 认证或者授权缓存的统一名称
    * @return
    * @param <K>
    * @param <V>
    * @throws CacheException
    */
   @Override
   public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
       return new RedisCache<>(cacheName);
  }
}

/**
* 自定义redis缓存的实现
*/
public class RedisCache<K, V> implements Cache<K, V> {

   private String cacheName;

   public RedisCache() {
  }

   public RedisCache(String cacheName) {
       this.cacheName = cacheName;
  }

   @Override
   public V get(K k) throws CacheException {
       return (V) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
  }

   @Override
   public V put(K k, V v) throws CacheException {
       getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), v);
       return null;
  }

   @Override
   public V remove(K k) throws CacheException {
       return (V) getRedisTemplate().opsForHash().delete(this.cacheName, k.toString());
  }

   @Override
   public void clear() throws CacheException {
       getRedisTemplate().delete(this.cacheName);
  }

   @Override
   public int size() {
       return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
  }

   @Override
   public Set<K> keys() {
       return getRedisTemplate().opsForHash().keys(this.cacheName);
  }

   @Override
   public Collection<V> values() {
       return getRedisTemplate().opsForHash().values(this.cacheName);
  }

   private RedisTemplate getRedisTemplate() {
       RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
       // 修改key的序列化方式为String方式
       redisTemplate.setKeySerializer(new StringRedisSerializer());
       redisTemplate.setHashKeySerializer(new StringRedisSerializer());
       return redisTemplate;
  }

}

ps:Ehcache为应用缓存,导入依赖即可使用,Redis需要开启服务并做相应配置才可使用。

对于安全管理器SecurityManager,这里是Web项目,实现的是DefaultWebSecurityManager。

对于ShiroFilter,设置安全管理器、配置系统受限资源。

其它

  1. shiro配置文件以.ini结尾(.ini文件一般作为Windows操作系统中的配置文件),.ini结尾文件类似.txt结尾文件。.ini文件可以写一些复杂数据格式。shiro配置文件放在resources下及其子目录下都行。在学习shiro中,这里.ini文件用来写权限信息等,真实场景在数据库中,这里是减少学习成本才使用.ini文件的。

  2. 常用shiro默认过滤器简写

    • anon(AnonymousFilter):过滤器允许立即访问路径而不执行任何类型的安全检查,用于公共资源。

    • authc(FormAuthenticationFilter):需要对请求用户进行身份验证才能继续请求,如果不是,则重定向到配置的URL强制用户登录。

    • perms(PermissionsAuthorizationFilter):如果当前用户具有映射值指定的权限,则过滤器允许访问,如果用户没有指定的所有权限,则拒绝访问。

    • roles(RolesAuthorizationFilter):如果当前用户具有映射值指定的角色,则允许访问;如果用户没有指定的所有角色,则拒绝访问。

  3. shiro项目重启,shiro缓存依然会重新加载,上次未退出的用户依然未退出。

  4. 默认认证界面URL为/login.jsp,可通过ShiroFilterFactoryBean的setLoginUrl方法进行设置。

  5. 设置/**为authc时,需要在其前面设置认证请求的url为anon,如/user/login(除认证页面,其他页面访问不到),否则永远无权。

  6. 可以在方法上添加@RequiresRoles("admin") 或@RequiresPermissions("user:update:01")来限制主体对方法的调用,值可设为value = {“admin”, "user"}多个值,都具有才能调用。@RequiresRoles与@RequiresPermissions同时标注时需要同时具有权限。(上述无权会报AuthorizationException)

  7. shiro在每次鉴权时都要调用doGetAuthorizationInfo,可设置缓存防止多次从数据库查询权限影响效率。

  8. Redis缓存相关问题

    1. 在对密码加盐时使用了ByteSource,其使用了SimpleByteSource,无法序列化,需要我们自己实现。实现方法,照搬SimpleByteSource并实现接口Serializable,并添加无参构造(反序列化需要使用)。

    2. 在对Redis缓存时,SimpleAuthenticationInfo和AuthorizationInfo都会缓存,认证时先缓存SimpleAuthenticationInfo,授权时缓存AuthorizationInfo,需要使用hash存而不是string,否则后者覆盖前者,会报类型无法转换错误。

    3. (待解决)AuthorizingRealm.setAuthorizationCacheName()使用无效。

标签:缓存,return,Shiro,new,public,shiro,user
来源: https://www.cnblogs.com/meyok/p/16526213.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有