权限管理系统:Shiro

2020.06.18 00:06:13
75
阅读约 14 分钟

什么是 Apache Shiro #

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于
理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

一句话:Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理等功能

Shiro核心知识之架构图交互和四大模块讲解 #

直达Apache Shiro官网 http://shiro.apache.org/introduction.html

什么是身份认证
Authentication,身份证认证,一般就是登录
什么是授权
Authorization,给用户分配角色或者访问某些资源的权限
什么是会话管理
Session Management, 用户的会话管理员,多数情况下是web session
什么是加密
Cryptography, 数据加解密,比如密码加解密等

img

用户访问Shrio权限控制运行流程和常见概念讲解 #

Subject
我们把用户或者程序称为主体(如用户,第三方服务,cron作业),主体去访问系统或者资源
SecurityManager
安全管理器,Subject的认证和授权都要在安全管理器下进行
Authenticator
认证器,主要负责Subject的认证
Realm
数据域,Shiro和安全数据的连接器,好比jdbc连接数据库; 通过realm获取认证授权相关信息
Authorizer
授权器,主要负责Subject的授权, 控制subject拥有的角色或者权限
Cryptography
加解密,Shiro的包含易于使用和理解的数据加解密方法,简化了很多复杂的api
Cache Manager
缓存管理器,比如认证或授权信息,通过缓存进行管理,提高性能
img

SpringBoot2.x整合Shiro权限认证项目搭建 #

创建springboot项目

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>

整合Shiro相关jar包

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>

Shiro认证和授权流程实操 #

认证:用户身份识别,俗称为用户“登录”
单元测试

package cn.siques.siquesshiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.jupiter.api.Test;

public class QuickText {
    private SimpleAccountRealm accountRealm = new SimpleAccountRealm();
    private DefaultSecurityManager defaultSecurityManager =new DefaultSecurityManager();

    @Before
    public void init(){

        // 初始化数据源
        accountRealm.addAccount("xdclass","123");

        accountRealm.addAccount("jack","456");
        //构建环境
        defaultSecurityManager.setRealm(accountRealm);
    }

    @Test
    public void test() {
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        // 当前操作主体,application user
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken xdclass = new UsernamePasswordToken("xdclass", "123");
        subject.login(xdclass);
        System.out.println("认证结果:"+subject.isAuthenticated());

    }
}

Shiro认证和授权流程和常用API #

//是否有对应的角色
subject.hasRole("root")
//获取subject名
subject.getPrincipal()
//检查是否有对应的角色,无返回值,直接在SecurityManager里面进行判断
subject.checkRole("admin")
//检查是否有对应的角色
subject.hasRole("admin")
//退出登录
subject.logout();
    @Test
    public void test() {
        // 初始化数据源
        accountRealm.addAccount("xdclass","123","root","admin");

        accountRealm.addAccount("jack","456","user");
        //构建环境
        defaultSecurityManager.setRealm(accountRealm);

        SecurityUtils.setSecurityManager(defaultSecurityManager);
        // 当前操作主体,application user
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken xdclass = new UsernamePasswordToken("xdclass", "123");
        subject.login(xdclass);
        System.out.println("认证结果:"+subject.isAuthenticated());
        System.out.println("是否有对应的角色:"+subject.hasRole("root"));

        System.out.println("getPrincipal:"+subject.getPrincipal());
        subject.logout();
        System.out.println("认证结果:"+subject.isAuthenticated());

    }

Shiro安全数据来源之Realm讲解 #

realm作用:Shiro 从 Realm 获取安全数据
默认自带的realm:idae查看realm继承关系,有默认实现和自定义继承的realm
两个概念
principal : 主体的标示,可以有多个,但是需要具有唯一性,常见的有用户名,手机号,邮箱等

credential:凭证, 一般就是密码
所以一般我们说 principal + credential 就账号 + 密码
开发中,往往是自定义realm , 即集成 AuthorizingRealm
继承关系
img

Shiro内置IniRealm实操和权限验证api #

# 格式 name=password,role1,role2,..roleN
[users]
# user 'root' with password 'secret' and the 'admin' role,
jack = 456, user
# user 'guest' with the password 'guest' and the 'guest' role
xdcalss = 123, root
# 格式 role=permission1,permission2...permissionN 也可以用通配符
# 下面配置user的权限为所有video:find,video:buy,如果需要配置video全部操作crud 则 user = video:*
[roles]
user = video:find,video:buy
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *

测试权限

    @Test
    public void text(){
        // 创建securitymanager工厂,通过配置文件ini创建
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");

        SecurityManager instance = factory.getInstance();
        // 设置环境

        SecurityUtils.setSecurityManager(instance);

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken xdclass = new UsernamePasswordToken("xdcalss", "123");
        subject.login(xdclass);

        System.out.println("认证结果:"+subject.isAuthenticated());
        System.out.println("是否有对应的角色:"+subject.hasRole("root"));

        System.out.println("getPrincipal:"+subject.getPrincipal());
        System.out.println("是否有find权限:"+subject.isPermitted("video:find"));
//        subject.checkPermission("video:find");
//        subject.checkPermission("video:delete");
    }

Shiro内置JdbcRealm实操 #

初始化一个测试数据库 #

# ************************************************************
# Sequel Pro SQL dump
# Version 4541
#
# http://www.sequelpro.com/
# https://github.com/sequelpro/sequelpro
#
# Host: 120.76.62.13 (MySQL 5.7.17)
# Database: xdclass_shiro
# Generation Time: 2019-04-28 16:23:31 +0000
# ************************************************************


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;


# Dump of table roles_permissions
# ------------------------------------------------------------

DROP TABLE IF EXISTS `roles_permissions`;

CREATE TABLE `roles_permissions` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(100) DEFAULT NULL,
  `permission` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_roles_permissions` (`role_name`,`permission`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `roles_permissions` WRITE;
/*!40000 ALTER TABLE `roles_permissions` DISABLE KEYS */;

INSERT INTO `roles_permissions` (`id`, `role_name`, `permission`)
VALUES
	(4,'admin','video:*'),
	(3,'role1','video:buy'),
	(2,'role1','video:find'),
	(5,'role2','*'),
	(1,'root','*');

/*!40000 ALTER TABLE `roles_permissions` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table user_roles
# ------------------------------------------------------------

DROP TABLE IF EXISTS `user_roles`;

CREATE TABLE `user_roles` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL,
  `role_name` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_roles` (`username`,`role_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user_roles` WRITE;
/*!40000 ALTER TABLE `user_roles` DISABLE KEYS */;

INSERT INTO `user_roles` (`id`, `username`, `role_name`)
VALUES
	(1,'jack','role1'),
	(2,'jack','role2'),
	(4,'xdclass','admin'),
	(3,'xdclass','root');

/*!40000 ALTER TABLE `user_roles` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table users
# ------------------------------------------------------------

DROP TABLE IF EXISTS `users`;

CREATE TABLE `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL,
  `password` varchar(100) DEFAULT NULL,
  `password_salt` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_users_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `users` WRITE;
/*!40000 ALTER TABLE `users` DISABLE KEYS */;

INSERT INTO `users` (`id`, `username`, `password`, `password_salt`)
VALUES
	(1,'jack','123',NULL),
	(2,'xdclass','456',NULL);

/*!40000 ALTER TABLE `users` ENABLE KEYS */;
UNLOCK TABLES;



/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

img

使用jdbcrealm.ini #

#声明Realm,指定realm类型
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
#配置数据源
#dataSource=com.mchange.v2.c3p0.ComboPooledDataSource
dataSource=com.alibaba.druid.pool.DruidDataSource
# mysql-connector-java 5 用的驱动url是com.mysql.jdbc.Driver,mysql-connector-java6以后用的是
# com.mysql.cj.jdbc.Driver
dataSource.driverClassName=com.mysql.cj.jdbc.Driver
#避免安全警告
dataSource.url=jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
dataSource.username=root
dataSource.password=root
#指定数据源
jdbcRealm.dataSource=$dataSource
#开启查找权限, 默认是false,不会去查找角色对应的权限,坑!!!!!
jdbcRealm.permissionsLookupEnabled=true
#指定SecurityManager的Realms实现,设置realms,可以有多个,用逗号隔开
securityManager.realms=$jdbcRealm

测试类与上一节相同
img
默认不查询权限

方法二:配置 #

// 设置数据源
        DefaultSecurityManager securityManager = new DefaultSecurityManager();
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-" +
                "8&serverTimezone=UTC&useSSL=false");
        ds.setUsername("root");
        ds.setPassword("root");

        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setPermissionsLookupEnabled(true);
        jdbcRealm.setDataSource(ds);


        securityManager.setRealm(jdbcRealm);

        SecurityUtils.setSecurityManager(securityManager);

        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken xdclass = new UsernamePasswordToken("jack", "123");
        subject.login(xdclass);

        System.out.println("认证结果:"+subject.isAuthenticated());
        System.out.println("是否有对应的角色:"+subject.hasRole("admin"));

        System.out.println("getPrincipal:"+subject.getPrincipal());
        System.out.println("是否有find权限:"+subject.isPermitted("video:delete"));

Apache Shiro 自定义Realm实战 #

  • 步骤:
    • 创建一个类 ,继承AuthorizingRealm->AuthenticatingRealm->CachingRealm->Realm
    • 重写授权方法 doGetAuthorizationInfo
    • 重写认证方法 doGetAuthenticationInfo
  • 方法:
    • 当用户登陆的时候会调用 doGetAuthenticationInfo
    • 进行权限校验的时候会调用: doGetAuthorizationInfo
  • 对象介绍
    • UsernamePasswordToken : 对应就是 shiro的token中有Principal和Credential
      • UsernamePasswordToken-》HostAuthenticationToken-》AuthenticationToken
    • SimpleAuthorizationInfo:代表用户角色权限信息
    • SimpleAuthenticationInfo :代表该用户的认证信息

自定义登陆验证

package cn.siques.siquesshiro;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.junit.platform.commons.util.StringUtils;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 自定义realm
 */
public class CustomRealm extends AuthorizingRealm {
  private final Map<String,String>  userInfo = new HashMap<>();
    {
        userInfo.put("jack","123");
        userInfo.put("xdclass","456");
    }

    // role -->permission
    private final Map<String,Set<String>>  permissionMap = new HashMap<>();
    {
        Set<String> set1 = new HashSet<>();
        Set<String> set2 = new HashSet<>();
        set1.add("video:find");
        set1.add("video:buy");

        set2.add("video:add");
        set2.add("video.delete");

        permissionMap.put("jack",set1);
        permissionMap.put("xdclass",set2);
    }

    // user -->role
    private final Map<String,Set<String>>  roleMap = new HashMap<>();
    {
        Set<String> set1 = new HashSet<>();
        Set<String> set2 = new HashSet<>();
        set1.add("role1");
        set1.add("role2");

        set2.add("root");


        permissionMap.put("jack",set1);
        permissionMap.put("xdclass",set2);
    }




    // 权限验证调用
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("授权 doGetAuthorizationInfo");
        String name = (String) principals.getPrimaryPrincipal();
        Set<String> permission = getPermissionByNameFromDB(name);
        Set<String> roles = getRolesByNameFromDB(name);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setRoles(roles);
        simpleAuthorizationInfo.setStringPermissions(permission);

        return simpleAuthorizationInfo;
    }

    /**
     * 模拟从数据库获取用户角色集合
     * @param name
     * @return
     */
    private Set<String> getRolesByNameFromDB(String name) {
        return roleMap.get(name);
    }

    /**
     * 模拟从数据库获取权限集合
     * @param name
     * @return
     */
    private Set<String> getPermissionByNameFromDB(String name) {
        return permissionMap.get(name);
    }

    //登陆验证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("认证 doGetAuthenticationInfo");

        // 从token中获取身份信息,代表用户信息
        String principal = (String) token.getPrincipal();
        // 模拟去数据中拿账号密码
        String pwd  =  getPwdByUsernameFromDB(principal);

        if(pwd ==null || "".equals(pwd)){
            return null;
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, pwd, this.getName());

        return simpleAuthenticationInfo;
    }

    private String getPwdByUsernameFromDB(String name) {
        return userInfo.get(name);
    }
}

深入Shiro源码解读认证授权流程 #

认证流程解读:subject.login(usernamePasswordToken);
DelegatingSubject->login()
DefaultSecurityManager->login()
AuthenticatingSecurityManager->authenticate()
AbstractAuthenticator->authenticate()
ModularRealmAuthenticator->doAuthenticate()
ModularRealmAuthenticator->doSingleRealmAuthentication()
AuthenticatingRealm->getAuthenticationInfo()
授权流程解读:subject.checkRole("admin")
DelegatingSubject->checkRole()
AuthorizingSecurityManager->checkRole()
ModularRealmAuthorizer->checkRole()
AuthorizingRealm->hasRole()
AuthorizingRealm->doGetAuthorizationInfo()

Shiro内置的Filter过滤器 #

核心过滤器类:DefaultFilter, 配置哪个路径对应哪个拦截器进行处理

  • authc: org.apache.shiro.web.filter.authc.FormAuthenticationFilter

    • 需要认证登录才能访问
  • user: org.apache.shiro.web.filter.authc.UserFilter

    • 用户拦截器,表示必须存在用户。
  • anon: org.apache.shiro.web.filter.authc.AnonymousFilter

    • 匿名拦截器,不需要登录即可访问的资源,匿名用户或游客,一般用于过滤静态资源。
  • roles:org.apache.shiro.web.filter.authz.RolesAuthorizationFilter

    • 角色授权拦截器,验证用户是或否拥有角色。
    • 参数可写多个,表示某些角色才能通过,多个参数时写 roles[“admin,user”],当有多个参数时必须每个参数都通过才算通过
  • perms:org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter

    • 权限授权拦截器,验证用户是否拥有权限
    • 参数可写多个,表示需要某些权限才能通过,多个参数时写 perms[“user, admin”],当有多个参数时必须每个参数都通过才算可以
  • authcBasic:org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

    • httpBasic 身份验证拦截器。
  • logout:org.apache.shiro.web.filter.authc.LogoutFilter

    • 退出拦截器,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url
  • port:org.apache.shiro.web.filter.authz.PortFilter

    • 端口拦截器, 可通过的端口。
  • ssl:org.apache.shiro.web.filter.authz.SslFilter

    • ssl拦截器,只有请求协议是https才能通过。

img

img

Shiro的Filter配置路径讲解 #

路径通配符支持 ?、*、**,注意通配符匹配不 包括目录分隔符“/”
* 可以匹配所有,不加*可以进行前缀匹配,但多个冒号就需要多个 * 来匹配
URL权限采取第一次匹配优先的方式
? : 匹配一个字符,如 /user? , 匹配 /user3,但不匹配/user/;
* : 匹配零个或多个字符串,如 /add* ,匹配 /addtest,但不匹配 /user/1
** : 匹配路径中的零个或多个路径,如 /user/** 将匹 配 /user/xxx 或 /user/xxx/yyy
例子
/user/**=filter1
/user/add=filter2
请求 /user/add 命中的是filter1拦截器

Shiro 数据安全之数据加解密 #

  • 为啥要加解密
    • 明文数据容易泄露,比如密码明文存储,万一泄露则会造成严重后果
  • 什么是散列算法
    • 一般叫hash,简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数,适合存储密码,比如MD5
  • 什么是salt(盐) 667788——》aabbcc
    • 如果直接通过散列函数得到加密数据,容易被对应解密网站暴力破解,一般会在应用程序里面加特殊的自动进行处理,比如用户id,例子:加密数据 = MD5(明文密码+用户id), 破解难度会更大,也可以使用
    • 多重散列,比如多次md5
  • Shiro里面 CredentialsMatcher,用来验证密码是否正确,
    • 源码:AuthenticatingRealm -> assertCredentialsMatch()
一般会自定义验证规则
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new
HashedCredentialsMatcher();
//散列算法,使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//散列的次数,比如散列两次,相当于 md5(md5("xxx"));
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}

Shiro权限控制注解和编程方式讲解 #

配置文件的方式:使用ShiroConfig

注解方式
@RequiresRoles(value={“admin”, “editor”}, logical= Logical.AND)
需要角色 admin 和 editor两个角色 AND表示两个同时成立
@RequiresPermissions (value={“user:add”, “user:del”}, logical= Logical.OR)
需要权限 user:add 或 user:del权限其中一个,OR是或的意思。
@RequiresAuthentication
已经授过权,调用Subject.isAuthenticated()返回true
@RequiresUser
身份验证或者通过记 住我登录的

编程方式

Subject subject = SecurityUtils.getSubject();
//基于角色判断
if(subject.hasRole(“admin”)) {
//有角色,有权限
} else {
//无角色,无权限
}
//或者权限判断
if(subject.isPermitted("/user/add")){
//有权限
}else{
//无权限
}
  • 常见API

  • subject.hasRole(“xxx”);

  • subject.isPermitted(“xxx”);

  • subject. isPermittedAll(“xxxxx”,“yyyy”);

  • subject.checkRole(“xxx”); // 无返回值,可以认为内部使用断言的方式

Shiro 缓存模块讲解 #

  • 什么是shiro缓存
    • shiro中提供了对认证信息和授权信息的缓存。
      • 默认是关闭认证信息缓存的,对于授权信息的缓存shiro默认开启的(因为授权的数据量大)
  • AuthenticatingRealm 及 AuthorizingRealm 分别提供了对AuthenticationInfo 和 AuthorizationInfo 信息的缓存。

Shiro Session模块讲解 #

  • 什么是会话session
    • 用户和程序直接的链接,程序可以根据session识别到哪个用户,和javaweb中的session类似
  • 什么是会话管理器SessionManager
    • 会话管理器管理所有subject的所有操作,是shiro的核心组件
    • 核心方法:
    • //开启一个session Session start(SessionContext context);//指定Key获取session Session getSession(SessionKey key)
    • shiro中的会话管理器有多个实现
  • SessionDao 会话存储/持久化
    • SessionDAO AbstractSessionDAO CachingSessionDAO EnterpriseCacheSessionDAO MemorySessionDAO
    • 核心方法
    • //创建 Serializable create(Session session);
    • //获取 Session readSession(Serializable sessionId) throws UnknownSessionException;
    • //更新 void update(Session session)
    • //删除,会话过期时会调用 void delete(Session session);
    • //获取活跃的session Collection getActiveSessions(); 会话存储有多个实现

附属资料:
RememberMe
1、 Cookie 写到客户端并 保存
2、 通过调用subject.login()前,设置 token.setRememberMe(true);
3、 关闭浏览器再重新打开;会发现浏览器还是记住你的
4、 注意点:

  • subject.isAuthenticated() 表示用户进行了身份验证登录的,即Subject.login 进行了登录
  • subject.isRemembered() 表示用户是通过RememberMe登录的
  • subject.isAuthenticated()==true,则 subject.isRemembered()==false, 两个互斥
  • 总结:特殊页面或者API调用才需要authc进行验证拦截,该拦截器会判断用户是否是通过
    subject.login()登录,安全性更高,其他非核心接口或者页面则通过user拦截器处理即可

Shiro整合SpringBoot2.x案例实战介绍 #

技术选型:前后端分离的权限检验 + SpringBoot2.x + Mysql + Mybatis + Shiro + Redis + IDEA + JDK8

基于RBAC权限控制实战之Mysql数据库设计 #

# ************************************************************
# Sequel Pro SQL dump
# Version 4541
#
# http://www.sequelpro.com/
# https://github.com/sequelpro/sequelpro
#
# Host: 120.76.62.13 (MySQL 5.7.17)
# Database: xdclass_shiro
# Generation Time: 2019-05-12 13:44:51 +0000
# ************************************************************


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;


# Dump of table permission
# ------------------------------------------------------------

DROP TABLE IF EXISTS `permission`;

CREATE TABLE `permission` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL COMMENT '名称',
  `url` varchar(128) DEFAULT NULL COMMENT '接口路径',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `permission` WRITE;
/*!40000 ALTER TABLE `permission` DISABLE KEYS */;

INSERT INTO `permission` (`id`, `name`, `url`)
VALUES
	(1,'video_update','/api/video/update'),
	(2,'video_delete','/api/video/delete'),
	(3,'video_add','/api/video/add'),
	(4,'order_list','/api/order/list'),
	(5,'user_list','/api/user/list');

/*!40000 ALTER TABLE `permission` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table role
# ------------------------------------------------------------

DROP TABLE IF EXISTS `role`;

CREATE TABLE `role` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL COMMENT '名称',
  `description` varchar(64) DEFAULT NULL COMMENT '描述',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `role` WRITE;
/*!40000 ALTER TABLE `role` DISABLE KEYS */;

INSERT INTO `role` (`id`, `name`, `description`)
VALUES
	(1,'admin','普通管理员'),
	(2,'root','超级管理员'),
	(3,'editor','审核人员');

/*!40000 ALTER TABLE `role` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table role_permission
# ------------------------------------------------------------

DROP TABLE IF EXISTS `role_permission`;

CREATE TABLE `role_permission` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `role_id` int(11) DEFAULT NULL,
  `permission_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `role_permission` WRITE;
/*!40000 ALTER TABLE `role_permission` DISABLE KEYS */;

INSERT INTO `role_permission` (`id`, `role_id`, `permission_id`)
VALUES
	(1,3,1),
	(2,3,2),
	(3,3,3),
	(4,2,1),
	(5,2,2),
	(6,2,3),
	(7,2,4);

/*!40000 ALTER TABLE `role_permission` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table user
# ------------------------------------------------------------

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `username` varchar(128) DEFAULT NULL COMMENT '用户名',
  `password` varchar(256) DEFAULT NULL COMMENT '密码',
  `create_time` datetime DEFAULT NULL,
  `salt` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user` WRITE;
/*!40000 ALTER TABLE `user` DISABLE KEYS */;

INSERT INTO `user` (`id`, `username`, `password`, `create_time`, `salt`)
VALUES
	(1,'二当家小D','123456',NULL,NULL),
	(2,'大当家','123456789',NULL,NULL),
	(3,'jack','123',NULL,NULL);

/*!40000 ALTER TABLE `user` ENABLE KEYS */;
UNLOCK TABLES;


# Dump of table user_role
# ------------------------------------------------------------

DROP TABLE IF EXISTS `user_role`;

CREATE TABLE `user_role` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `role_id` int(11) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `remarks` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

LOCK TABLES `user_role` WRITE;
/*!40000 ALTER TABLE `user_role` DISABLE KEYS */;

INSERT INTO `user_role` (`id`, `role_id`, `user_id`, `remarks`)
VALUES
	(1,3,1,'二当家小D是editor'),
	(2,1,3,'jack是admin'),
	(3,2,3,'jack是root'),
	(4,3,3,'jack是editor'),
	(5,1,2,'大当家是admin');

/*!40000 ALTER TABLE `user_role` ENABLE KEYS */;
UNLOCK TABLES;



/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

  • 用户
  • 角色
  • 权限

img

项目依赖 #

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--阿里巴巴druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<!--spring整合shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>

数据库配置 #

#==============================数据库相关配置========================================
spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost/shiro?
useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username =root
spring.datasource.password =root
#使用阿里巴巴druid数据源,默认使用自带的
#spring.datasource.type =com.alibaba.druid.pool.DruidDataSource

#开启控制台打印sql
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# mybatis 下划线转驼峰配置,两者都可以
#mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.map-underscore-to-camel-case=true

用户角色权限多对多关联查询SQL #

第一步 查询用户对应的角色映射关系

select * from user u  left join user_role ur on u.id=ur.user_id where  u.id=3 

第二步 查询用户对应的角色信息

select * from user u  left join user_role ur on u.id=ur.user_id left join role r on ur.role_id = r.id where  u.id=3

第三步 查询角色和权限的关系

select * from user u  left join user_role ur on u.id=ur.user_id left join role r on ur.role_id = r.id left join role_permission rp on r.id=rp.role_id where  u.id=1 

第四步 查询角色对应的权限信息(某个用户具备的角色和权限集合)

select * from user u  left join user_role ur on u.id=ur.user_id left join role r on ur.role_id = r.id left join role_permission rp on r.id=rp.role_id left join permission p on rp.permission_id=p.id where  u.id=1 

查询角色权限

SELECT p.id AS id ,p.name AS NAME ,p.url AS url  FROM role_permission rp
LEFT JOIN permission p ON rp.`permission_id`= p.`id` 
WHERE rp.`role_id`=3

查询用户角色关系

SELECT ur.role_id AS id ,r.name AS NAME ,r.description AS description  FROM user_role ur
LEFT JOIN role r ON ur.`role_id`= r.`id` 
WHERE ur.`user_id` =3

img

自定义CustomRealm #

继承 AuthorizingRealm
重写 doGetAuthorizationInfo
重写 doGetAuthenticationInfo

package cn.siques.siquesshiro.config;

import cn.siques.siquesshiro.domain.Permission;
import cn.siques.siquesshiro.domain.User;
import cn.siques.siquesshiro.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.context.annotation.Configuration;
import cn.siques.siquesshiro.domain.Role;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@Configuration
public class MyCustomRealm extends AuthorizingRealm {
    @Resource
    private UserService userService;

    /**
     * 进行权限校验的时候调用
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("授权 doGetAuthenticationInfo");
        String username = (String) principals.getPrimaryPrincipal();
        User user = userService.findAllUserInfoByUserName(username);
        List<String> stringRoleList = new ArrayList<>();
        List<String> stringPermissionList = new ArrayList<>();

        List<Role> roleList = user.getRoleList();

        for (Role role:roleList
             ) {
            stringRoleList.add(role.getName());
            List<Permission> permissionList = role.getPermissionList();
            for (Permission permission: permissionList
                 ) {
               if(permission!=null){
                   stringPermissionList.add(permission.getName());
               }
            }
        }
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRoles(stringRoleList);
        simpleAuthorizationInfo.addStringPermissions(stringPermissionList);



        return simpleAuthorizationInfo;
    }


    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("认证 doGetAuthenticationInfo");
        String username = (String) token.getPrincipal();

        User user = userService.findAllUserInfoByUserName(username);
        // 取密码
        String pwd = user.getPassword();
        if (pwd==null ||"".equals(pwd)){
            return null;
        }


        return new SimpleAuthenticationInfo(username,user.getPassword(),this.getClass().getName());
    }
}

ShiroFilterFactoryBean配置 #

  • shiroFilterFactoryBean-》

    • SecurityManager-》

      • CustomSessionManager
      • CustomRealm-》hashedCredentialsMatcher
  • SessionManager

    • DefaultSessionManager: 默认实现,常用于javase
    • ServletContainerSessionManager: web环境
    • DefaultWebSessionManager:常用于自定义实现

使用Shiro Logout和加密处理 #

 @Test     
 public void testMD5(){   //加密算法        
 String hashName = "md5";   //密码明文        
 String pwd = "123";   //加密函数,使用shiro自带的         
 Object result = new SimpleHash(hashName, pwd, null, 2);         
 System.out.println(result);     
 }

进阶之自定义Shiro Filter过滤器 #

  • 背景知识:
    • /admin/order= roles[“admin, root”] ,表示 /admin/order 这个接口需要用户同时具备 admin 与 root 角色 才可访问, 相当于hasAllRoles() 这个判断方法
  • 我们的需求:
    • 订单信息,可以由角色 普通管理员 admin 或者 超级管理员 root 查看 只要用户具备其中一个角色即可

重写AuthorizationFilter,修改为只要有其中一个角色即可访问。

public class CustomRolesOrAuthorizationFilter  extends AuthorizationFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        Subject subject = getSubject(request, response);

        //获取当前访问路径所需要的角色集合
         String[] rolesArray = (String[]) mappedValue;
         //没有角色限制,有权限访问
         if (rolesArray == null || rolesArray.length == 0) {
            return true;
         }
        //当前subject是rolesArray中的任何一个,则有权限访问
        for (int i = 0; i < rolesArray.length; i++) {
            if (subject.hasRole(rolesArray[i])) {
                return true;
            }
        }
        return false;
    }
}
FactoryBean配置
Map<String, Filter> filtersMap = new LinkedHashMap<>(); filtersMap.put("roleOrFilter",new CustomRolesOrAuthorizationFilter()); filterChainDefinitionMap.put("/admin/**","roleOrFilter[admin,root]"); shiroFilterFactoryBean.setFilters(filtersMap); 

测试Shiro Filter过滤器 #

img

Redis整合CacheManager #

加依赖

<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
    /**
     * 配置redismanager
     */
    public RedisManager getRedisManager(){
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("localhost");
        redisManager.setPort(6379);
        return redisManager;
    }

    /**
     *  设置具体实现类
     */
    public RedisCacheManager redisCacheManager(){
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(getRedisManager());
        return redisCacheManager;
    }
  // 使用自定义cachemanager
        securityManager.setCacheManager(redisCacheManager());

调用接口出现的问题

class java.lang.String must has getter for field: authCacheKey or id\nWe need a
field to identify this Cache Object in Redis. So you need to defined an id field
which you can get unique id to identify this principal. For example, if you use
UserInfo as Principal class, the id field maybe userId, userName, email, etc. For
example, getUserId(), getUserName(), getEmail(), etc.\nDefault value is
authCacheKey or id, that means your principal object has a method called
\"getAuthCacheKey()\" or \"getId()\""

修改

 return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName());
 User newUser = (User) principals.getPrimaryPrincipal();
        User user = userService.findAllUserInfoByUserName(newUser.getUsername());

Redis整合SessionManager #

为什么session也要持久化?
重启应用,用户无感知,可以继续以原先的状态继续访问
怎么持久化?

//配置session持久化
customSessionManager.setSessionDAO(redisSessionDAO());
/**
* 自定义session持久化
* @return
*/
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(getRedisManager());
return redisSessionDAO;
}

注意点:
DO对象需要实现序列化接口 Serializable
logout接口和以前一样调用,请求logout后会删除redis里面的对应的key,即删除对应的token

ShiroConfig常用bean类配置 #

  • AuthorizationAttributeSourceAdvisor
    • 作用:加入注解的使用,不加入这个AOP注解不生效(shiro的注解 例如 @RequiresGuest)
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor()
{
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new
AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
  • DefaultAdvisorAutoProxyCreator

    • 作用: 用来扫描上下文寻找所有的Advistor(通知器), 将符合条件的Advisor应用到切入点的Bean中,需要在LifecycleBeanPostProcessor创建后才可以创建
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new
DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
阅读:75 . 字数:3612 发布于 5 个月前
Copyright 2018-2020 Siques