参考
参考

权限管理

  1. 权限管理:控制不同身份的用户拥有不同的权限。
  2. 实现权限管理的方法
    1. 基于主页的权限管理:不同身份的用户对应不同的主页(index.html)。例如在用户表中除了账号密码字段外,还有一个state字段标记不同身份的用户,在用户登录时根据state字段的不同显示不同的主页。适用于权限管理比较单一,用户少,每类用户权限固定。
    2. 基于用户和权限的管理:包含用户表,用户权限表,系统权限表。可以实现权限的动态分配,但是不够灵活。
    3. 基于角色的访问控制(RBAC:Role-Based Access Control):包含用户表、用户角色表、角色表、角色权限表、权限表。

安全框架

  1. 常用的安全框架:
    1. shiro
    2. spring security
    3. OAuth2

JavaSE应用中shiro的基本使用

  1. 创建Maven项目
  2. 导入shiro依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.3.2</version>
</dependency>
  1. 创建shiro配置文件:在resource目录下创建名为shiro.ini的文件,在文件中完成用户、角色、权限的配置
;定义用户
[users]
lisi=123456,seller
zhangsan=543210,manager
admin=131452,admin
;定义角色
[roles]
admin=*
seller=order-add,order-del,order-list
manager=order-add,order-del,order-list
  1. 认证授权
String userName = "admin";
String passWord = "131452";

// 创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 创建Realm
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
// 将realm设置给安全管理器
securityManager.setRealm(iniRealm);
// 将安全管理器设置给SecurityUtils工具
SecurityUtils.setSecurityManager(securityManager);
// 通过SecurityUtils获取subject对象
Subject subject = SecurityUtils.getSubject();

// 认证流程
// 1.将账号密码封装到token对象中
UsernamePasswordToken token = new UsernamePasswordToken(userName, passWord);
// 2.通过subject对象调用login方法进行认证
boolean flag = false;
try {
    subject.login(token);
    flag = true;
} catch (AuthenticationException exception) {
    flag = false;
}
System.out.println(flag ? "登录成功":"登录失败");

// 授权流程
// 判断是否有某个角色
System.out.println(subject.hasRole("manager")); //false
// 判断是否有某个权限
System.out.println(subject.isPermitted("order-del"));//true

springboot整合shiro,使用IniRealm

  1. 导入依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.3.2</version>
</dependency>
  1. shiro配置
@Configuration
public class ShiroConfig {
    @Bean
    public IniRealm getRealm() {
        IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
        return iniRealm;
    }
    @Bean
    public DefaultWebSecurityManager getSecurityManager(Realm iniRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(iniRealm);
        return securityManager;
    }
    @Bean
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
        filter.setSecurityManager(securityManager);

        // 设置shiro的拦截规则
        // anno:匿名用户可以访问
        // authc:认证用户可以访问
        // user:使用remember me的用户可以访问
        // perms:拥有对应权限可以访问
        // role:拥有对应的角色才可以访问
        HashMap<String, String> filterMap = new HashMap<>();
        filterMap.put("/","anon");
        filterMap.put("/login.html","anon");
        filterMap.put("/regist.html","anon");
        filterMap.put("/user/login","anon");
        filterMap.put("/user/regist","anon");
        filterMap.put("/static/**","anon");
        filterMap.put("/**","authc");

        filter.setFilterChainDefinitionMap(filterMap);
        filter.setLoginUrl("/login.html");
        // 设置未授权访问的页面路径
        filter.setUnauthorizedUrl("/login.html");
        return filter;
    }

}
  1. Shiro认证流程的关键:
    1. subject调用login方法,将包含用户名和密码的token传递给SecurityManager
    2. SecurityManager就会调用认证器(Authenticator)进行认证。
    3. Authenticator将token传递给绑定的Realm,在Realm中进行用户的认证检查;如果认证通过则正常执行,否则抛出认证异常。
@Service
public class UserServiceImpl implements UserService{
    @Override
    public void checkLogin(String userName, String password) throws Exception {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
        subject.login(token);
    }
}

springboot整合shiro,使用JdbcRealm

  1. 导入依赖
  2. shiro配置
@Configuration
public class ShiroConfig {
    @Bean
    public JdbcRealm getJdbcRealm(DataSource dataSource) {
        JdbcRealm jdbcRealm = new JdbcRealm();
        // jdbcRealm自行从数据库中查询用户、权限、角色等信息
        jdbcRealm.setDataSource(dataSource);
        // jdbcRealm默认开启认证功能,需要手动开启授权功能
        jdbcRealm.setPermissionsLookupEnabled(true);
        return jdbcRealm;
    }
    @Bean
    public DefaultWebSecurityManager webSecurityManager(JdbcRealm jdbcRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(jdbcRealm);
        return securityManager;
    }
    @Bean
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
        filter.setSecurityManager(securityManager);

        // 设置shiro的拦截规则
        // anno:匿名用户可以访问
        // authc:认证用户可以访问
        // user:使用remember me的用户可以访问(认证用户也可以访问)
        // perms:拥有对应权限可以访问
        // role:拥有对应的角色才可以访问
        // logout:表示指定退出的URL
        HashMap<String, String> filterMap = new HashMap<>();
        filterMap.put("/","anon");
        filterMap.put("/login.html","anon");
        filterMap.put("/regist.html","anon");
        filterMap.put("/user/login","anon");
        filterMap.put("/user/regist","anon");
        filterMap.put("/static/**","anon");
        filterMap.put("/**","authc");

        filter.setFilterChainDefinitionMap(filterMap);
        filter.setLoginUrl("/login.html");
        // 设置未授权访问的页面路径
        filter.setUnauthorizedUrl("/login.html");
        return filter;
    }

}

shiro的标签

1.shiro的标签使用

shiro提供了可以供jsp使用的标签以及Thymeleaf使用的标签。

1.JSP中使用shiro的标签
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
2.Thymeleaf模板中使用shiro的标签
  1. 在pom.xml中导入thymeleaf模板对shiro标签支持的依赖
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.1.0</version>
</dependency>
  1. 在shiro配置中配置shiro的方言
@Configuration
public class ShiroConfig {
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
}
  1. thymeleaf模板中引入shiro的命名空间
<html  xmlns:th="http://www.thymeleaf.org"
       xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
</html>

2.常用的shiro标签

  1. guest:判断用户是否为游客身份,如果是游客身份则显示此标签的内容(根据subject的状态判断)
<shiro:guest>
    欢迎游客访问
</shiro:guest>
  1. user:判断用户是否是认证身份,如果是认证身份则显示此标签内容
  2. principal:获取当前已经登录的用户的用户名
<shiro:user>
    用户[<shiro:principal/>]欢迎您!
</shiro:user>
  1. authenticated/notAuthenticated
  2. hasRole:判断已登录用户是否有角色
<shiro:hasRole name="admin">超级管理员</shiro:hasRole>
  1. hasPermission:判断已登录用户是否有权限
<shiro:hasPermission name="sys:c:delete">
    <a href="#">出库</a>
</shiro:hasPermission>

自定义Realm实现权限管理

上例中使用shiro内置的Realm,数据表结构需要满足JdbcRealm的要求。自定义Realm实现权限管理,数据表结构是自己定义的,需要实现自己的Dao。Dao中主要完成根据用户名查询用户信息用于认证;根据用户名查询用户的角色列表,根据用户名查询用户的权限列表用于授权管理。

  1. 数据库设计
  2. Dao实现
    1. 根据用户名查询用户信息
    2. 根据用户名查询角色列表
    3. 根据用户名查询权限列表
  3. 整合shiro
    1. 导入依赖
    2. shiro配置
    3. 自定义Realm
      1. 创建一个类继承AuthorizingRealm类
      2. 重写doGetAuthorizationInfo方法和doGetAuthenticationInfo方法(分别用于获取授权数据和认证数据)
      3. 重写getName方法返回当前Realm的一个自定义名称

加密

在项目开发中通常使用BASE64和MD5编码方式。BASE64是一种可以反编码的编码方式,MD5是一种不可逆的编码方式。

1.shiro中的加密处理

如果数据库表中用户的密码存储的是密文,则使用shiro提供的加密功能,对输入的密码进行加密后再进行认证。

  1. shiro配置中增加如下内容
@Configuration
public class ShiroConfig {
    @Bean
    public HashedCredentialsMatcher getHashedCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        // 加密方式
        matcher.setHashAlgorithmName("md5");
        // hash次数,这里的循环次数要和用户注册时密码的加密次数一致
        matcher.setHashIterations(1);
        return matcher;
    }
    @Bean
    public JdbcRealm getJdbcRealm(DataSource dataSource, HashedCredentialsMatcher matcher) {
        JdbcRealm jdbcRealm = new JdbcRealm();
        // jdbcRealm自行从数据库中查询用户、权限、角色等信息
        jdbcRealm.setDataSource(dataSource);
        // jdbcRealm默认开启认证功能,需要手动开启授权功能
        jdbcRealm.setPermissionsLookupEnabled(true);
        jdbcRealm.setCredentialsMatcher(matcher);
        return jdbcRealm;
    }
    //....
}
  1. shiro加密
// 注册的时候需要对密码进行加密处理
String userPwd = "123456";
Md5Hash md5Hash = new Md5Hash(userPwd);
  1. shiro中加盐加密,如果密码进行了加盐处理则自定义Realm在返回认证数据时需要返回盐。
String userPwd = "123456";
// 加盐
int num = new Random().nextInt(1000);
Md5Hash md5Hash = new Md5Hash(userPwd,num+"");
  1. shiro中加盐加密,多次hash
String userPwd = "123456";
// 加盐
int num = new Random().nextInt(1000);
// 多次加密
Md5Hash md5Hash = new Md5Hash(userPwd,num+"", 3);

// 或者
SimpleHash hash = new SimpleHash("md5", userPwd, num+"", 3);

shiro中退出登录功能的实现

  1. 在shiro中的过滤器中增加如下配置
filterMap.put("/exit", "logout");
  1. 在页面的退出按钮上,跳转到logout对应的url上
<p><a href="/exit">"退出登录"</a></p>

shiro中授权管理

  1. 权限控制通常有两类做法:
    1. 不同身份的用户登录,显示不同的操作菜单,没有权限的菜单不显示。
    2. 对所有用户显示所有菜单,当用户点击菜单以后再验证当前用户是否有此权限,如果没有则提示权限不足。
  2. 授权管理
    1. 过滤器授权
    // 设置访问delete.html需要有sys:c:delete这个权限名
    filterMap.put("/delete.html", "perms[sys:c:delete]");
    // 设置未授权访问的页面路径
    filter.setUnauthorizedUrl("/login.html");
    
    1. 使用@RequiresPermissions注解。

      1. 前提需要配置spring对shiro注解的支持
      @Configuration
      public class ShiroConfig {
          @Bean
          public DefaultAdvisorAutoProxyCreator getProxyCreator() {
              DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
              proxyCreator.setProxyTargetClass(true); 
              return proxyCreator;
          }
          @Bean
          public AuthorizationAttributeSourceAdvisor getAdvisor(DefaultWebSecurityManager securityManager) {
              AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
              advisor.setSecurityManager(securityManager);
              return advisor;
          }
      
          // ...
      }
      
      1. 测试
      // 如果没有sys:c:delete这个权限则不允许执行该方法
      @RequiresPermissions({"sys:c:delete"})
      @RequestMapping("/test.html")
      public String test() {
          return "delete";
      }
      
      1. 通过全局异常处理当后台抛出AuthorizationException异常时跳转到友好的提示页面。
      @ControllerAdvice
      public class GlobalExceptionHandler {
          @ExceptionHandler
          public String doException(Exception exception) {
              if (exception instanceof AuthorizationException) {
                  return "lesspermission";
              }
              return null;
          }
      }
      
    2. 手动授权

    @RequestMapping("/test.html")
    public String test() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.isPermitted("sys:c:delete")) {
            return "delete";
        }
        else
            return "lesspermission";
    }
    

shiro中使用缓存

使用Shiro进行权限管理过程中,每次授权都会访问Realm中的doGetAuthorizationlnfo方法查询当前用户的角色及权限信息,如果系统的用户量比较大则会对数据库造成比较大的压力。Shiro支持缓存以降低对数据库的访问压力(缓存的是授权信息)

  1. 导入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>2.3.11.RELEASE</version>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.5.0</version>
</dependency>
  1. 配置缓存策略:在resources目录下新建ehcache.xml配置文件
  2. 加入缓存管理
@Configuration
public class ShiroConfig {

    @Bean
    public EhCacheManager getEhCacheCacheManager() {
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return cacheManager;
    }
    @Bean
    public DefaultWebSecurityManager webSecurityManager(JdbcRealm jdbcRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(jdbcRealm);
        // 将缓存管理器设置给SecurityManager
        securityManager.setCacheManager(getEhCacheCacheManager());
        return securityManager;
    }
    //....
}

shiro中的session管理

  1. shiro进行认证和授权是基于session实现的,shiro包含了对session的管理。如果我们需要对session进行管理,则需要
    1. 自定义session管理器:默认的session过期时间为30分钟
    @Bean
    public DefaultWebSessionManager getSessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        // 配置sessionManager
        sessionManager.setGlobalSessionTimeout(15 * 1000); // 设置session过期时间为15秒
        return sessionManager;
    }
    
    1. 将自定义的session管理器设置给SecurityManager
    @Bean
    public DefaultWebSecurityManager webSecurityManager(JdbcRealm jdbcRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(jdbcRealm);
        // 将缓存管理器设置给SecurityManager
        securityManager.setCacheManager(getEhCacheCacheManager());
        securityManager.setSessionManager(getSessionManager());
        return securityManager;
    }
    

shiro中的remember me

  1. 将用户对页面访问的权限分为三个级别:
    1. 未认证可以访问的页面
    2. 记住我可以访问的页面
    3. 已认证可以访问的页面
  2. shiro中实现remember me
    1. 在过滤器中设置“记住我”才可以访问的URL
    // 设置shiro的拦截规则
    // anno:匿名用户可以访问
    // authc:认证用户可以访问
    // user:使用remember me的用户可以访问
    // perms:拥有对应权限可以访问
    // role:拥有对应的角色才可以访问
    HashMap<String, String> filterMap = new HashMap<>();
    filterMap.put("/index.html","user");
    
    1. 在shiro配置中配置基于cookie的rememberMe管理器
    @Bean
    public CookieRememberMeManager cookieRememberMeManager() {
        CookieRememberMeManager rememberMeManager = new CookieRememberMeManager();
        SimpleCookie cookie = new SimpleCookie("remCookie");
        // 设置cookie的存活时间
        cookie.setMaxAge(1 * 24 * 60 * 60);
        rememberMeManager.setCookie(cookie);
        return rememberMeManager;
    }
    @Bean
    public DefaultWebSecurityManager webSecurityManager(JdbcRealm jdbcRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(jdbcRealm);
        // 设置rememberMe管理器
        securityManager.setRememberMeManager(cookieRememberMeManager());
        // 将缓存管理器设置给SecurityManager
         //securityManager.setCacheManager(getEhCacheCacheManager());
        securityManager.setSessionManager(getSessionManager());
        return securityManager;
    }
    
    1. 登录认证时设置token“记住我”
    @Service
    public class UserServiceImpl implements UserService{
        @Override
        public void checkLogin(String userName, String password, boolean rememberMe) throws Exception {
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
            token.setRememberMe(rememberMe);
            subject.login(token);
        }
    }
    

shiro中多realm配置

1.使用场景

当使用shiro进行权限管理,数据来自不同的数据源时,可以给SecurityManager配置多个Realm

2.多个Realm的处理方式
  1. 链式处理:多个Realm依次进行认证。
  2. 分支处理:根据不同的条件从多个Realm中选择一个进行认证处理。
3.多Realm配置(链式处理)
4.多Realm配置(分支处理)
  1. 实现案例:不同身份的用户(一个字段标识登录类型)登录执行不同的Realm
    1. 自定义Realm:例如NormalUserRealm、AdminRealm。当在登陆页面选择普通用户登录,则执行NormalUserRealm的认证;当在登陆页面选择管理员用户登录,则执行AdminRealm的认证;
    2. shiro配置:将Realm交给spring容器管理,设置给SecurityManager
    3. 自定义Token,因为UsernamePasswordToken不能存放自定义属性(登录类型)。因此自定义Token继承自UsernamePasswordToken
    4. 自定义认证器:因为ModularRealmAuthenticator的doAuthenticate方法会执行所有Realm的认证。因此自定义认证器继承ModularRealmAuthenticator,重写doAuthenticate方法
    5. 配置自定义认证器:securityManager设置自定义认证器