Shiro笔记(一)

Shiro笔记(一)

前言:Shiro是一个权限管理工具!

权限管理

  现在企业应用中,基本上涉及到用户参与的系统都要进行权限管理,权限管理用来控制不同的用户角色对系统的访问权限,按照安全规则或者策略控制用户能够访问的资源。权限管理属于系统安全的范畴。

权限管理包括两部分:

  • 身份认证 Authentication

    判断一个用户是否为合法用户的处理过程,最简单的方式就是我们常常使用的账号密码登录的方式。其他的身份认证方式还有例如指纹解锁,硬件key刷卡等。

  • 授权 Authorization

    当用户身份认证通过后,系统要根据用户的角色进行系统访问资源的授权。

例如我们在web后台管理中经常用到权限配置功能来配置不同角色的权限,就可以通过shiro来实现,例如

角色的权限配置
 

Shiro概述

Shiro 是一个功能强大且易于使用的java安全框架,它能够进行身份验证,授权,加密和会话管理。通过使用Shiro易于理解的API,我们可以轻松对任何应用程序进行系统的保护。

和Shiro一样用于权限管理的还有Spring全家桶的Spring Security

所属组织:Apache。

架构

  Apache Shiro的设计目标是通过直观且易于使用来简化应用程序安全性。Shiro的核心设计模拟了大多数人如何在应用程序与某人(或某物)交互的情况下如何考虑应用程序安全性。例如,如果没有登录会在界面中显示注册或登录按钮,如果已经登录了可以点击查看自己的账户信息等。

架构图

Shiro架构图

Subject

  即主体,简单理解就是一个进行登录的用户,但是也可以是程序或其他与该应用程序进行交互的对象。官网上称为“视图”。

同时这是一个接口,接口中定义了很多和认证授权相关的方法,我们可以通过该对象获取当前登录的对象信息等等,在进行认证授权的时候主要就是操作该对象。

SecurityManager

  安全管理器,是Shiro框架的核心,用来协调管理其内部的安全组件,负责对所有的Subject对象进行安全管理。但是一般我们配置好之后就不会去操作该对象了,而是将关注的核心放在Subject上。

SecurityManager继承Authenticator接口,Authorizer接口,SessionManager接口。

Realm

  领域,相当于数据源,SecurityManager进行身份等安全认证的时候需要获取用户的权限数据,比如用户的账号密码,或者用户的操作权限字符串等,这个时候就要和数据库进行交互了。

  • 注意:Realm不仅仅能和数据库进行交互,还有认证授权相关的代码。后面的身份认证和授权的方法就在该类中书写!

Authenticator

  认证器,主要用于对用户的身份进行验证,例如账号密码的验证。

Authorizer

  授权器,当用户通过认证器的认证之后,在访问系统资源(界面展示的内容,API是否能够调用)的时候就可以通过授权器判断用户是否有该权限进行对应的操作。

SessionManager

  会话管理器,是Shiro中自定义的一套session管理机制,不依赖web的session管理,可以用在非web引用上进行session的管理。

SessionDao

  会话管理的dao,是对Shiro中的session进行操作的一套接口,例如将session存储到数据库中等。

CacheManager

  缓存管理,可以将用户资源权限数据存储到缓存中,提供性能,不用每次都去数据库中查询是否有对应的权限。Shiro中提供了缓存机制,当然也可以整合redis进行权限数据的缓存。

Cryptography

  密码管理,shiro提供了一套加密和解密的对象,例如进行散列和MD5算法加密等。

shiro工作流程

Shiro工作流程

 

登录流程简单使用

  1. 引入依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.5.3</version>
</dependency>
  1. 创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();

该对象只是SecurityManager接口的其中一个实现类,如果是web应用场景下还有其他的专用实现类。

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

ini是Shiro规定的配置文件的格式,充当数据源的作用,在单机应用可以用IniRealm来获取身份信息或授权信息。但是一般都用来测试使用。

下面是shiroConfig.ini文件中的内容:

[users]
xiaocheng=123
zhangshan=123456
lisi=789

账号=密码的方式来记录。

  1. 在安全管理器工具类中设置安全管理器
SecurityUtils.setSecurityManager(securityManager);

之后的操作都会通过安全管理工具类来获取Subject对象等,而不会去直接操作SecurityManager对象。

  1. 通过安全管理工具类获取Subject对象
// 获取主体对象
Subject subject = SecurityUtils.getSubject();
// 获取登录的实体对象
User principal = (User) subject.getPrincipal();

如果我们还要进一步获取登录的bean对象(假设我们自定义为User类)信息,可以通过

User principal = (User) subject.getPrincipal();来获取,相当于我们之前在session中通过getAttribuite(“user”)来获取当前登录对象的信息一样。

  1. 创建token

    通过账号和密码来创建token对象进行身份的认证

UsernamePasswordToken token = new UsernamePasswordToken("zhangshan", "123456");

在这个过程中可以进行MD5加散列的方式进行密码的加密。

  1. 登录验证
subject.login(token);

如果账号名不存在会抛出UnknownAccountException异常,如果密码错误会抛出IncorrectCredentialsException异常。

上面的方式基本不用使用了,下面是shiro与spring boot的整合的使用

与spring boot整合

Shiro在与Spring Boot进行整合的时,主要使用ShiroFilter来进行路径访问的拦截与配置,与Spring Boot整合的具体流程如下

Spring Boot整合Shiro

请求只有通过ShiroFilter中的认证和授权才能够访问Spring Boot应用中的资源。

  1. 引入依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.7.1</version>
</dependency>
  1. 创建Shiro配置类

Shiro配置类一般要配置ShiroFilter,SecurityManager,Realm三个对象。

package cn.net.smrobot.springboot_jsp_shiro.config;

@Configuration
public class ShiroConfig {

    /**
     * 1. 注入ShiroFilter
     */
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
        // 给ShiroFilter设置安全管理器,在web环境下要使用DefaultWebSecurityManager实现类
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        // 配置受限资源
        Map<String, String> map = new HashMap<>(4);
        // authc 表示要资源要通过身份认证之后才能访问,如果没有认证会跳到setLoginUrl中设置的页面
        map.put("/index.jsp", "authc");
        // annon 可以用来设置公共资源的访问,不用身份认证就能访问
        map.put("/login.jsp", "anon");
        // 设置login页面,默认就是设置login.jsp,如果没有该页面,在自动跳转的时候会报错。
        shiroFilterFactoryBean.setLoginUrl("/login.jsp");
        //登录成功后跳转的路径 - index主页
        shiroFilterFactoryBean.setSuccessUrl("/index");
        // 没有权限时跳转的路径 - 403页面
		shiroFilterFactoryBean.setUnauthorizedUrl("/403");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * 2. 注入web的安全管理器
     * 会自动将该对象注入到SecurityUntils对象中
     */
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        // 给安全管理设置领域
        defaultWebSecurityManager.setRealm(realm);
        return defaultWebSecurityManager;
    }
    
    /**
     * 3. 注入Realm
     * 一般都会自定义Realm
     */
    @Bean
    public Realm getRealm() {
        MyRealm myRealm = new MyRealm();
        // 创建hash凭证匹配器 - 可以用来使用md5算法对密码进行加密
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 设置算法名称
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 设置散列次数
        hashedCredentialsMatcher.setHashIterations(1024);
        // 设置凭证匹配器
        MyRealm.setCredentialsMatcher(hashedCredentialsMatcher);
        return MyRealm;
    }
}


在ShiroFilter中配置受限资源时有如下配置内容,可以用通过这些过滤器来配置控制url的权限:

配置缩写对应的过滤器功能
anonAnonymousFilter指定url相当于公共资源被访问
authcFormAuthenticationFilter指定url需要form表单登录,默认会从请求中获取usernamepassword,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。可以用这个过滤器做登录逻辑,但是一般都会自己在控制器中写登录逻辑。
authcBasicBasicHttpAuthenticationFilter指定url需要basic登录
logoutLogoutFilter登出过滤器,配置指定url就可以实现退出功能,非常方便
noSessionCreationNoSessionCreationFilter禁止创建会话
permsPermissionsAuthorizationFilter需要指定权限才能访问
portPortFilter需要指定端口才能访问
rolesRolesAuthorizationFilter需要指定角色才能访问
sslSslFilter需要https请求才能访问
userUserFilter需要已登录或“记住我”的用户才能访问

比较常用的是authc和anon

  1. 创建自定义的Realm对象,继承AuthorizingRealm,并重写里面的两个用于身份认证和授权的方法。

MyRealm.java

/**
 * 授权相关服务-shiro
 */
public class MyRealm extends AuthorizingRealm {

	@Autowired
	private UserDao userDao;
	
    // 权限授权处理方法
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        
        // 根据需要配置权限
        // Role是基于角色进行管理的权限配置
        simpleAuthorizationInfo.addRole("admin");
        // Permission是基于权限字符串进行管理的权限配置 - 一般都会使用这种方式加之和数据库进行配合使用
        simpleAuthorizationInfo.addStringPermission("order:*");
       
        return simpleAuthorizationInfo;
	}

    // 身份认证处理方法
	@Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		// 获取用户名
        String principal = (String) authenticationToken.getPrincipal();
		// 根据用户名去数据库查找对应的对象
        User user = userDao.findUserByName(principal);
        if (!Objects.isNull(user)) {
            // 进行身份认证 - 这里就会执行ShiroConfig配置Realm的hash算法
            return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),
                                                ByteSource.Util.bytes(user.getSalt()), this.getName());
        }
        return null;
    }
}

补充

Shiro等权限管理框架的主流授权方式有如下两种:

  • 基于角色的访问控制

    RBAC Role-Based Access Control

    以角色为中心进行访问控制

    例如上面的使用simpleAuthorizationInfo.addRole("admin");有admin角色的才能够访问对应的资源。

  • 基于资源的访问控制

    RBAC Resource-Based Access Control

    以资源为中心进行访问控制,通过权限字符串的形式来进行资源的配置

    权限字符串:

    # 资源标识符:操作:资源实例标识符  - 对哪个资源的哪个实例具有什么操作
    例如:
    - admin:ad:delete
    - admin:ad:update
    

    可以通过simpleAuthorizationInfo.addStringPermission("order:*");添加对应的权限
     

注册登录案例

主要是对密码进行md5加随机盐的方式进行加密。

使用的框架是:Spring Boot + Mybatis + Shiro。

该案例并没有采用前后端分离的方式!!!

  • 注册用户

controller层

@PostMapping("register")
public String register(User user) {
    boolean register = userService.register(user);
    if (register) {
        return "redirect:/login.jsp";
    } else {
        return "redirect:/register.jsp";
    }
}

service层

public boolean register(User user) {
    // 1. 获取8位的随机盐
    String salt = SaltUtils.getSalt(8);
    // 2. 对密码和随机盐一起进行加密
    // 1024为进行hash的次数
    Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
    // 3. 设置加密后的密码和保存随机盐
    user.setPassword(md5Hash.toHex());
    user.setSalt(salt);
    try {
        userDao.register(user);
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

// SaltUtils.java - 和随机相关的工具类
/**
* 获取随机盐
* @param length
* @return
*/
public static String getSalt(int length) {

    char[] chars = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcbvnm-=^$&*?".toCharArray();
    StringBuilder sb = new StringBuilder();
    Random random = new Random();
    for (int i = 0; i < length; i++) {
        sb.append(chars[random.nextInt(chars.length)]);
    }
    return sb.toString();
}

Mybatis对应接口的配置文件

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.net.smrobot.springboot_jsp_shiro.dao.UserDao">
    <insert id="register" parameterType="user" useGeneratedKeys="true" keyProperty="id">
        insert into shiro_user values (#{id}, #{username}, #{password}, #{salt})
    </insert>
</mapper>

注册完成后会往数据库中插入一条新的数据
在这里插入图片描述

username=zhangsan
password=1234567890
  • 用户登录

controller控制层

@PostMapping("login")
public String login(String username, String password) {
    Subject subject = SecurityUtils.getSubject();
    try {
        // 调用shiro的登录认证方法
        subject.login(new UsernamePasswordToken(username, password));
    } catch (UnknownAccountException e) {
        System.out.println("用户名不存在");
        return "redirect:/login.jsp";
    } catch (IncorrectCredentialsException e) {
        System.out.println("密码错误");
        return "redirect:/login.jsp";

    }
    return "redirect:/index.jsp";

}

  由于是使用shiro进行认证,因此不用在自己的service层中写login方法,而是将登录验证交给Shiro处理。Shiro中具体处理身份验证的方法在Realm类中实现。

Realm

public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserDao userDao;

    // 授权用的,身份认证不用使用到
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    //身份认证方法 - 上面调用subject.login()时对自动调用该方法进行认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
		// 1 获取用户名
        String principal = (String) authenticationToken.getPrincipal();
		// 2 从数据库中根据用户名查询是否有对应用户
        User user = userDao.findUserByName(principal);
        if (!Objects.isNull(user)) {
            // 进行密码的检验
            return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),
                    ByteSource.Util.bytes(user.getSalt()), this.getName());
        }
        return null;
    }
}

  由于在注册的时候使用的是md5加随机盐散列的方式,因此在登录的时候也要采用相同的方式对密码进行加密验证,因此需要在Realm中设置相关的凭证校验器。

/**
* 3. 创建Realm
*/
@Bean
public Realm getRealm() {
    MyRealm myRealm = new MyRealm();
    //1. 设置hash凭证匹配器
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    //2. 设置算法名称
    hashedCredentialsMatcher.setHashAlgorithmName("md5");
    //3. 设置散列次数 - 和注册时设置的散列次数一致。
    hashedCredentialsMatcher.setHashIterations(1024);
    customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
    return MyRealm;
}

注册登录的上面的流程大致如下:

 

权限分配案例

在现在的开发中,经常会用到RBAC的模式来维护用户角色权限表。

用户 -- 角色 -- 权限 -- 资源

表的关系大致如下:

在这里插入图片描述

一般都需要5张表来维护一整个用户角色权限系统。

tips: 中间表的连接不要使用外键的方式进行约束!将验证逻辑放在应用程序中而不是交给DBMS系统处理。

数据库中的数据如下:

shiro_user表

在这里插入图片描述

shiro_role表

在这里插入图片描述

shiro_user_role表

在这里插入图片描述

shiro_permission表

在这里插入图片描述

shiro_role_permissions表
在这里插入图片描述

  • dao层
    /**
     * 根据用户名获取其所有的角色信息
     * @param username
     * @return
     */
    List<Role> findRolesByUsername(String username);

    /**
     * 根据角色id查询其所有的权限字符串
     * @param id
     * @return
     */
    List<Permission> findRoleIdPermissions(int id);

mybatis对应的sql语句

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.net.smrobot.springboot_jsp_shiro.dao.UserDao">
    <select id="findRolesByUsername" parameterType="String" resultType="Role">
        select r.id, r.name
        from shiro_user as u
        left join shiro_user_role as sur
        on u.id = sur.uid
        left join shiro_role as r
        on sur.rid = r.id
        where u.username=#{username}
    </select>

    <select id="findRoleIdPermissions" parameterType="int" resultType="Permission">
        select p.id, p.permission, p.url
        from shiro_role as r
        left join shiro_role_permissions as srp
        on r.id = srp.rid
        left join shiro_permissions as p
        on p.id = srp.pid
        where r.id = #{id}
    </select>
</mapper>
  • 修改自定义的Realm中授权方法
public class MyRealm extends AuthorizingRealm {

    @Autowired
    private UserDao userDao;
	
    // 权限获取相关代码
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("登录的用户=" + primaryPrincipal);
        // 获取该用户的角色信息
        List<Role> rolesList = userDao.findRolesByUsername(primaryPrincipal);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        for (Role role : rolesList) {
            List<Permission> permissionList = userDao.findRoleIdPermissions(role.getId());
            for (Permission permission : permissionList) {
                System.out.println("permission=" + permission.getPermission());
                // 将该获取到权限字符串进行添加
                simpleAuthorizationInfo.addStringPermission(permission.getPermission());
            }
        }
        return simpleAuthorizationInfo;

    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        String principal = (String) authenticationToken.getPrincipal();

        User user = userDao.findUserByName(principal);
        if (!Objects.isNull(user)) {
            return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),
                    ByteSource.Util.bytes(user.getSalt()), this.getName());
        }
        return null;
    }
}

这样,但每次访问被Shiro所管理的资源时候就会的时候就会的调用一次权限获取的方法。


shiro管理资源的方法

  1. 在jsp页面中使用Shiro标签的方式进行管理,例如:
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

<shiro:hasAnyRoles name="user,admin">
    <li><a href="">用户管理</a>
        <ul>
            <shiro:hasPermission name="user:add:*">
                <li><a href="">添加</a></li>
            </shiro:hasPermission>
            <shiro:hasPermission name="user:delete:*">
                <li><a href="">删除</a></li>
            </shiro:hasPermission>
            <shiro:hasPermission name="user:update:*">
                <li><a href="">修改</a></li>
            </shiro:hasPermission>
            <shiro:hasPermission name="user:find:*">
                <li><a href="">查询</a></li>
            </shiro:hasPermission>
        </ul>
    </li>
</shiro:hasAnyRoles>
<shiro:hasRole name="admin">
    <li><a href="">商品管理</a></li>
    <li><a href="">订单管理</a></li>
    <li><a href="">物流管理</a></li>
</shiro:hasRole>
  1. 或者是在controller控制器中通过注解的方式控制路径的访问
@RequiresPermissions("admin:ad:list")	// 有该权限字段才能够访问
@RequiresPermissionsDesc(menu = { "推广管理", "广告管理" }, button = "查询")
@GetMapping("/list")
public Object list() {
    // 处理业务逻辑
}

 

使用CacheManager缓存

  在上面的Shiro管理资源的方式中,每个标签或者每个控制器中使用@RequiresPermissions或者@RequiresRoles注解的方法都会调用一次Realm中授权方法,重复获取。而一般权限数据是不会有太多的改变的,所以可以使用缓存的方式来保存权限数据。Shiro提供了缓存管理器供我们进行缓存操作。

使用方式如下:

  1. 引入依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.7.1</version>
</dependency>
  1. 修改自定义的Realm,开启缓存管理器
@Bean
public Realm getRealm() {
    MyRealm customerRealm = new MyRealm();
    // 设置hash凭证匹配器
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    // 设置算法名称
    hashedCredentialsMatcher.setHashAlgorithmName("md5");
    // 设置散列次数
    hashedCredentialsMatcher.setHashIterations(1024);
    MyRealm.setCredentialsMatcher(hashedCredentialsMatcher);

    // 开启缓存管理器
    MyRealm.setCachingEnabled(true);
    MyRealm.setAuthorizationCachingEnabled(true);
    MyRealm.setAuthorizationCacheName("authorization");
    MyRealm.setAuthenticationCachingEnabled(true);
    MyRealm.setAuthenticationCacheName("authentication");
    MyRealm.setCacheManager(new EhCacheManager());
    return MyRealm;
}

这样就只会在最开始调用一次授权的方法,数据将会被缓存起来。

当然也可以使用Redis的方式进行缓存

备注:上述的权限获取的案例并不是在前后端分离的背景下进行的,前后端分离的页面的权限控制是根据后台返回的权限字段,
通过JS的方式来实现的,而Shiro中权限控制主要在controller类方法中。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值