发表于: 2018-09-09 22:54:19

1 433


今天完成的事情:(一定要写非常细致的内容,比如说学会了盒子模型,了解了Margin) 

Shiro 是 Apache 旗下开源的一款强大且易用的Java安全框架,身份验证、授权、加密、会话管理。相比 Spring Security 而言 Shiro 更加轻量级,且 API 更易于理解…

Shiro

Shiro 主要分为 安全认证 和 接口授权 两个部分,其中的核心组件为 SubjectSecurityManagerRealms,公共部分 Shiro 都已经为我们封装好了,我们只需要按照一定的规则去编写响应的代码即可…

  • Subject 即表示主体,将用户的概念理解为当前操作的主体,因为它即可以是一个通过浏览器请求的用户,也可能是一个运行的程序,外部应用与 Subject 进行交互,记录当前操作用户。Subject 代表了当前用户的安全操作,SecurityManager 则管理所有用户的安全操作。
  • SecurityManager 即安全管理器,对所有的 Subject 进行安全管理,并通过它来提供安全管理的各种服务(认证、授权等)
  • Realm 充当了应用与数据安全间的 桥梁 或 连接器。当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。

本章目标

利用 Spring Boot 与 Shiro 实现安全认证和授权….

导入依赖

依赖 spring-boot-starter-web

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<shiro.version>1.4.0</shiro.version>
</properties>

<dependencies>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- shiro 相关包 -->
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-core</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-ehcache</artifactId>
   <version>${shiro.version}</version>
</dependency>
<!-- End  -->
</dependencies>

缓存配置

Shiro 为我们提供了 CacheManager 即缓存管理,将用户权限数据存储在缓存,可以提高它的性能。支持 EhCacheRedis 等常规缓存,这里为了简单起见就用 EhCache 了 , 在resources 目录下创建一个 ehcache-shiro.xml 

        <?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false" name="shiroCache">
<defaultCache
       maxElementsInMemory="10000"
       eternal="false"
       timeToIdleSeconds="120"
       timeToLiveSeconds="120"
       overflowToDisk="false"
       diskPersistent="false"
       diskExpiryThreadIntervalSeconds="120"
/>
</ehcach

实体类

创建一个 User.java ,标记为数据库用户

package com.battcn.entity;

/**
* @author Levin
* @since 2018/6/28 0028
*/
public class User {
  /** 自增ID */
  private Long id;
  /** 账号 */
  private String username;
  /** 密码 */
  private String password;
  /** 角色名:Shiro 支持多个角色,而且接收参数也是 Set<String> 集合,但这里为了简单起见定义成 String 类型了 */
  private String roleName;
  /** 是否禁用 */
  private boolean locked;
  // 省略 GET SET 构造函数...
}

伪造数据

支持 rolespermissions,比如你一个接口可以允许用户拥有某一个角色,也可以是拥有某一个 permission …6

package com.battcn.config;

     import com.battcn.entity.User;

     import java.util.*;

/**
* 主要不想连接数据库..
*
* @author Levin
* @since 2018/6/28 0028
*/
public class DBCache {

  /**
   * K 用户名
   * V 用户信息
   */
  public static final Map<String, User> USERS_CACHE = new HashMap<>();
  /**
   * K 角色ID
   * V 权限编码
   */
  public static final Map<String, Collection<String>> PERMISSIONS_CACHE = new HashMap<>();

  static {
     // TODO 假设这是数据库记录
     USERS_CACHE.put("u1", new User(1L, "u1", "p1", "admin", true));
     USERS_CACHE.put("u2", new User(2L, "u2", "p2", "admin", false));
     USERS_CACHE.put("u3", new User(3L, "u3", "p3", "test", true));

     PERMISSIONS_CACHE.put("admin", Arrays.asList("user:list", "user:add", "user:edit"));
     PERMISSIONS_CACHE.put("test", Collections.singletonList("user:list"));

  }
}

ShiroConfiguration

Shiro 的主要配置信息都在此文件内实现;

package com.battcn.config;


     import org.apache.shiro.cache.ehcache.EhCacheManager;
     import org.apache.shiro.spring.LifecycleBeanPostProcessor;
     import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
     import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
     import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
     import org.springframework.context.annotation.Bean;
     import org.springframework.context.annotation.Configuration;

     import java.util.LinkedHashMap;
     import java.util.Map;

/**
* Shiro 配置
*
* @author Levin
*/
@Configuration
public class ShiroConfiguration {

  private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class);

  @Bean
public EhCacheManager getEhCacheManager() {
     EhCacheManager em = new EhCacheManager();
     em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
     return em;
  }


  @Bean(name = "lifecycleBeanPostProcessor")
  public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
     return new LifecycleBeanPostProcessor();
  }


  /**
   * 加密器:这样一来数据库就可以是密文存储,为了演示我就不开启了
   *
   * @return HashedCredentialsMatcher
   */
//    @Bean
//    public HashedCredentialsMatcher hashedCredentialsMatcher() {
//        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//        //散列算法:这里使用MD5算法;
//        hashedCredentialsMatcher.setHashAlgorithmName("md5");
//        //散列的次数,比如散列两次,相当于 md5(md5(""));
//        hashedCredentialsMatcher.setHashIterations(2);
//        return hashedCredentialsMatcher;
//    }


  @Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
     DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
     autoProxyCreator.setProxyTargetClass(true);
     return autoProxyCreator;
  }

  @Bean(name = "authRealm")
  public AuthRealm authRealm(EhCacheManager cacheManager) {
     AuthRealm authRealm = new AuthRealm();
     authRealm.setCacheManager(cacheManager);
     return authRealm;
  }

  @Bean(name = "securityManager")
  public DefaultWebSecurityManager getDefaultWebSecurityManager(AuthRealm authRealm) {
     DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
     defaultWebSecurityManager.setRealm(authRealm);
     // <!-- 用户授权/认证信息Cache, 采用EhCache 缓存 -->
     defaultWebSecurityManager.setCacheManager(getEhCacheManager());
     return defaultWebSecurityManager;
  }

  @Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(
        DefaultWebSecurityManager securityManager) {
     AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
     advisor.setSecurityManager(securityManager);
     return advisor;
  }

  /**
   * ShiroFilter<br/>
   * 注意这里参数中的 StudentService IScoreDao 只是一个例子,因为我们在这里可以用这样的方式获取到相关访问数据库的对象,
   * 然后读取数据库相关配置,配置到 shiroFilterFactoryBean 的访问规则中。实际项目中,请使用自己的Service来处理业务逻辑。
   *
   * @param securityManager 安全管理器
   * @return ShiroFilterFactoryBean
   */
  @Bean(name = "shiroFilter")
  public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
     ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
     // 必须设置 SecurityManager
     shiroFilterFactoryBean.setSecurityManager(securityManager);
     // 如果不设置默认会自动寻找Web工程根目录下的"/login"页面
     shiroFilterFactoryBean.setLoginUrl("/login");
     // 登录成功后要跳转的连接
     shiroFilterFactoryBean.setSuccessUrl("/index");
     shiroFilterFactoryBean.setUnauthorizedUrl("/denied");
     loadShiroFilterChain(shiroFilterFactoryBean);
     return shiroFilterFactoryBean;
  }

  /**
   * 加载shiroFilter权限控制规则(从数据库读取然后配置)
   */
  private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) {
     /////////////////////// 下面这些规则配置最好配置到配置文件中 ///////////////////////
     // TODO 重中之重啊,过滤顺序一定要根据自己需要排序
     Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
     // 需要验证的写 authc 不需要的写 anon
     filterChainDefinitionMap.put("/resource/**", "anon");
     filterChainDefinitionMap.put("/install", "anon");
     filterChainDefinitionMap.put("/hello", "anon");
     // anon:它对应的过滤器里面是空的,什么都没做
     log.info("##################从数据库读取权限规则,加载到shiroFilter##################");

     // 不用注解也可以通过 API 方式加载权限规则
     Map<String, String> permissions = new LinkedHashMap<>();
     permissions.put("/users/find", "perms[user:find]");
     filterChainDefinitionMap.putAll(permissions);
     filterChainDefinitionMap.put("/**", "authc");
     shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132

AuthRealm

上面介绍过 Realm ,安全认证和权限验证的核心处理就是重写 AuthorizingRealm 中的 doGetAuthenticationInfo(登录认证) 与 doGetAuthorizationInfo(权限验证)

      import com.battcn.entity.User;
     import org.apache.shiro.SecurityUtils;
     import org.apache.shiro.authc.*;
     import org.apache.shiro.authz.AuthorizationInfo;
     import org.apache.shiro.authz.SimpleAuthorizationInfo;
     import org.apache.shiro.realm.AuthorizingRealm;
     import org.apache.shiro.session.Session;
     import org.apache.shiro.subject.PrincipalCollection;
     import org.apache.shiro.util.ByteSource;
     import org.springframework.context.annotation.Configuration;

     import java.util.*;

/**
* 认证领域
*
* @author Levin
* @version 2.5.1
* @since 2018-01-10
*/
@Configuration
public class AuthRealm extends AuthorizingRealm {

  /**
   * 认证回调函数,登录时调用
   * 首先根据传入的用户名获取User信息;然后如果user为空,那么抛出没找到帐号异常UnknownAccountException
   * 如果user找到但锁定了抛出锁定异常LockedAccountException;最后生成AuthenticationInfo信息,
   * 交给间接父类AuthenticatingRealm使用CredentialsMatcher进行判断密码是否匹配,
   * 如果不匹配将抛出密码错误异常IncorrectCredentialsException
   * 另外如果密码重试此处太多将抛出超出重试次数异常ExcessiveAttemptsException
   * 在组装SimpleAuthenticationInfo信息时, 需要传入:身份信息(用户名)、凭据(密文密码)、盐(username+salt),
   * CredentialsMatcher使用盐加密传入的明文密码和此处的密文密码进行匹配。
   */
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
        throws AuthenticationException {
     String principal = (String) token.getPrincipal();
     User user = Optional.ofNullable(DBCache.USERS_CACHE.get(principal)).orElseThrow(UnknownAccountException::new);
     if (!user.isLocked()) {
        throw new LockedAccountException();
     }
     // 从数据库查询出来的账号名和密码,与用户输入的账号和密码对比
     // 当用户执行登录时,在方法处理上要实现 user.login(token)
     // 然后会自动进入这个类进行认证
     // 交给 AuthenticatingRealm 使用 CredentialsMatcher 进行密码匹配,如果觉得人家的不好可以自定义实现
     // TODO 如果使用 HashedCredentialsMatcher 这里认证方式就要改一下 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, "密码", ByteSource.Util.bytes("密码盐"), getName());
     SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, user.getPassword(), getName());
     Session session = SecurityUtils.getSubject().getSession();
     session.setAttribute("USER_SESSION", user);
     return authenticationInfo;
  }

  /**
   * 只有需要验证权限时才会调用, 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用.在配有缓存的情况下,只加载一次.
   * 如果需要动态权限,但是又不想每次去数据库校验,可以存在ehcache.自行完善
   */
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
     Session session = SecurityUtils.getSubject().getSession();
     User user = (User) session.getAttribute("USER_SESSION");
     // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission
     SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
     // 用户的角色集合
     Set<String> roles = new HashSet<>();
     roles.add(user.getRoleName());
     info.setRoles(roles);
     // 用户的角色对应的所有权限,如果只使用角色定义访问权限,下面可以不要
     // 只有角色并没有颗粒度到每一个按钮 或 是操作选项  PERMISSIONS 是可选项
     final Map<String, Collection<String>> permissionsCache = DBCache.PERMISSIONS_CACHE;
     final Collection<String> permissions = permissionsCache.get(user.getRoleName());
     info.addStringPermissions(permissions);
     return info;
  }
}

控制器

在 ShiroConfiguration 中的 shiroFilter 处配置了 /hello = anon,意味着可以不需要认证也可以访问,那么除了这种方式外 Shiro 还为我们提供了一些注解相关的方式…

常用注解

  • @RequiresGuest 代表无需认证即可访问,同理的就是 /path = anon
  • @RequiresAuthentication 需要认证,只要登录成功后就允许你操作
  • @RequiresPermissions 需要特定的权限,没有则抛出AuthorizationException
  • @RequiresRoles 需要特定的橘色,没有则抛出AuthorizationException
  • @RequiresUser 不太清楚,不常用…

LoginController

package com.battcn.controller;

     import com.battcn.config.ShiroConfiguration;
     import org.apache.shiro.SecurityUtils;
     import org.apache.shiro.authc.*;
     import org.apache.shiro.subject.Subject;
     import org.slf4j.Logger;
     import org.slf4j.LoggerFactory;
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.RestController;
     import org.springframework.web.servlet.mvc.support.RedirectAttributes;

/**
* @author Levin
* @since 2018/6/28 0028
*/
@RestController
public class LoginController {

  private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class);

  @GetMapping(value = "/hello")
  public String hello() {
     log.info("不登录也可以访问...");
     return "hello...";
  }

  @GetMapping(value = "/index")
  public String index() {
     log.info("登陆成功了...");
     return "index";
  }

  @GetMapping(value = "/denied")
  public String denied() {
     log.info("小伙子权限不足,别无谓挣扎了...");
     return "denied...";
  }

  @GetMapping(value = "/login")
  public String login(String username, String password, RedirectAttributes model) {
     // 想要得到 SecurityUtils.getSubject() 的对象..访问地址必须跟 shiro 的拦截地址内.不然后会报空指针
     Subject sub = SecurityUtils.getSubject();
     // 用户输入的账号和密码,,存到UsernamePasswordToken对象中..然后由shiro内部认证对比,
     // 认证执行者交由 com.battcn.config.AuthRealm doGetAuthenticationInfo 处理
     // 当以上认证成功后会向下执行,认证失败会抛出异常
     UsernamePasswordToken token = new UsernamePasswordToken(username, password);
     try {
        sub.login(token);
     } catch (UnknownAccountException e) {
        log.error("对用户[{}]进行登录验证,验证未通过,用户不存在", username);
        token.clear();
        return "UnknownAccountException";
     } catch (LockedAccountException lae) {
        log.error("对用户[{}]进行登录验证,验证未通过,账户已锁定", username);
        token.clear();
        return "LockedAccountException";
     } catch (ExcessiveAttemptsException e) {
        log.error("对用户[{}]进行登录验证,验证未通过,错误次数过多", username);
        token.clear();
        return "ExcessiveAttemptsException";
     } catch (AuthenticationException e) {
        log.error("对用户[{}]进行登录验证,验证未通过,堆栈轨迹如下", username, e);
        token.clear();
        return "AuthenticationException";
     }
     return "success";
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

UserController

package com.battcn.controller;

     import org.apache.shiro.authz.annotation.Logical;
     import org.apache.shiro.authz.annotation.RequiresRoles;
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.RequestMapping;
     import org.springframework.web.bind.annotation.RestController;

/**
* @author Levin
* @since 2018/6/28 0028
*/
@RestController
@RequestMapping("/users")
public class UserController {

  @GetMapping
public String get() {
     return "get.....";
  }

  /**
   * RequiresRoles 是所需角色 包含 AND OR 两种
   * RequiresPermissions 是所需权限 包含 AND OR 两种
   *
   * @return msg
   */
  @RequiresRoles(value = {"admin", "test"}, logical = Logical.OR)
  //@RequiresPermissions(value = {"user:list", "user:query"}, logical = Logical.OR)
  @GetMapping("/query")
  public String query() {
     return "query.....";
  }

  @GetMapping("/find")
  public String find() {
     return "find.....";
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

主函数

package com.battcn;

     import org.springframework.boot.SpringApplication;
     import org.springframework.boot.autoconfigure.SpringBootApplication;


/**
* @author Levin
*/
@SpringBootApplication
public class Chapter25Application {

  public static void main(String[] args) {

     SpringApplication.run(Chapter25Application.class, args);

  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

测试

启动 Chapter25Application.java 中的 main 方法,为了更好的演示效果这里打开了 postman 做的测试,只演示其中一个流程,剩下的可以自己复制代码测试…

先登录,由于 u3 在 DBCache 中拥有的角色是 test,只有 user:list 这一个权限

登录

访问 /users/query 成功,因为我们符合响应的角色/权限

访问Query接口

访问 /users/find 失败,并重定向到了 /denied 接口,问题来了为什么 /users/find 没有写注解也权限不足呢?

权限不足

细心的朋友肯定会发现 在 ShiroConfiguration 中写了一句 permissions.put(“/users/find”, “perms[user:find]”); 意味着我们不仅可以通过注解方式,同样可以通过初始化时加载数据库中的权限树做控制,看各位喜好了….


明天计划的事情:(一定要写非常细致的内容) 

1.跑的demo好好学习下

2.准备复盘的资料
遇到的问题:(遇到什么困难,怎么解决的) 
收获:(通过今天的学习,学到了什么知识)


返回列表 返回列表
评论

    分享到