shiro踩坑指南—基础概念与实战

说明

  代码及部分相关资料根据慕课网Mark老师的视频进行整理。

  其他资料:

基础概念

Authenticate/Authentication(认证)

  认证是指检查用户身份合法性,通过校验用户输入的密码是否正确,判断用户是否为本人。

  有几个概念需要理解:

  • Principals (主体标识)
    任何可以唯一地确定一个用户的属性都可以充当principal,例如:邮箱、手机号、用户ID等,这些都是与用户一一对应的,可以唯一地确定一个用户。

  • credentials (主体凭证)
    credentials是能确认用户身份的东西,可以是证书(Certificate),也可以是密码(password)。

  • token(令牌)
    这里的token和api里的token有一点儿差别,这里token是principal和credential的结合体或者说容器。这里先讲一部分,剩下的放到"Subject"讲解。

Authorize/Authorization(授权)

  shiro中的“授权”,更贴切说法是“鉴权”,即判定用户是否拥有某些权限,至于拥有该权限在业务上有何意义,则是由业务本身来决定。

  关于“授权”,shiro引入了两种概念:

  • Role (角色)
    角色用来区分用户的类别。角色与用户间是多对多的关系,一个用户可以拥有多个角色,如Bob可以同时是admin(管理员)和user(普通用户)。

  • Permission (权限)
    权限是对角色的具体的描述,用于说明角色在业务上的特殊性。如admin(管理员)可以拥有user:delete(删除用户)、user:modify(修改用户信息)等的权限。同样的,角色与权限是多对多的数量关系。
    shiro权限可以分级,使用":"分割,如delete、user:delete、user:info:delete。可以使用"*"作通配符,例如可以给admin赋予操作用户的所有权限,可以配置为"user:*",这样在授权时,isPermitted("user:123")、isPermitted("user:123:abc")都是返回true;如果配置为"*😗:delete",想要返回true,则需要类似这样的权限: isPermitted("123🔤delete")、isPermitted("hello:321:delete")。

Subject(主体)

  Subject对象用于应用程序与shiro的相关组件进行交互,可以把它看作应用程序中“用户”的代理,也可以将其视为shiro中的“用户”。譬如在一个应用中,User对象作为业务上以及程序中的“用户”,在实现shiro的认证和授权时,并不直接使用User对象与shiro组件进行交互,而是把User对象的信息(用户名和密码)交给Subject,Subject调用自己的方法,向shiro组件发起身份认证或授权。
  如下是Subject接口提供的方法,包括登录(login)、退出(logout)、认证(isAuthenticated)、授权(checkPermission)等:

  接着上面继续讲Token,在图片中可以看到,login方法需要传入一个AuthenticationToken类型参数,这是一个接口,点进去看是酱紫的:
这个接口有两个方法,分别用来返回principal和credential。由此可见Token就是principal和credential的容器,用于Subject提交“登录”请求时传递信息。   需要提醒,Subject的这些方法调用SecurityManager进行实际上的认证和授权过程。Subject只是业务程序与SecurityManager通信的门面。

SecurityManager

  顾名思义,SecurityManager是用来manage(管理)的,管理shiro认证授权的流程,管理shiro组件、管理shiro的一些数据、管理Session等等。
  如下是SecurityManager接口的继承关系:

  SecurityManager继承了Authorizer(授权)、Authenticator(认证)、和SessionManager(Session管理)。需要注意,此处Session是指Subject与SecurityManager通信的Session,不能狭隘地理解为WebSession。SecurityManager继承了了这几个接口后,又另外提供了login、logout和createSubject方法。

Realm(域)

  与Subject和SecurityManager一样,Realm是shiro中的三大核心组件之一。Realm相当于DAO,用于获取与用户安全相关的数据(用户密码、角色、权限等)。当Subject发起认证和授权时,实际上是调用其对应的SecurityManager的认证和授权的方法,而SecurityManager则又是调用Authenticator和Authorizer的方法,这两个类,最后是通过Realm来获取主体的认证和授权信息。
  shiro的认证和授权过程如下所示:

使用shiro的基本流程

shiro的使用其实是比较简单的,只要熟记这几个步骤,然后在代码中实现即可。
1. 创建Realm
  Realm是一个接口,其实现类有SimpleAccountRealm, IniRealm, JdbcRealm等,实际应用中一般需要自定义实现Realm,自定义的Realm通常继承自抽象类AuthorizingRealm,这是一个比较完善的Realm,提供了认证和授权的方法。
2. 创建SecurityManager并配置环境
  配置SecurityManager环境实际上是配置Realm、CacheManager、SessionManager等组件,最基本的要配置Realm,因为安全数据是通过Realm来获取。用SecurityManager的setRealm(xxRealm)方法即可给SecurityManager设置Realm。可以为SecurityManager设置多个Realm。
3. 创建Subject
  可以使用SecurityUtils创建Subject。SecurityUtils是一个抽象工具类,其提供了静态方法getSubject(),用来创建一个与线程绑定的Subject。创建出来的Subject用ThreadContext类来存储,该类也是一个抽象类,它包含一个Map<Object, Object>类型的ThreadLocal静态变量,该变量存储该线程对应的SecurityManager对象和Subject对象。在SecurityUtils调用getSubject方法时,实际上是调用SecurityManager的CreateSubject()方法,既然如此,为什么还要通过SecurityUtils创建Subject?因为SecurityUtils不仅仅创建了Subject还将其与当前线程绑定,而且,使用SecurityManager的CreateSubject()方法还要构建一个SubjectContext类型的参数。
4. Subject提交认证和授权
  Subject的login(Token token)方法可以提交“登录”(或者说认证),token就是待验证的用户信息(用户名和密码等)。登录(认证)成功后,使用Subject的ckeckRole()、checkPermission等方法判断主体是否拥有某些角色、权限,以达到授权的目的。再次提醒,Subject不实现实际上的认证和授权过程,而是交给SecurityManager处理。

shiro认证授权示例

  Realm用的是SimpleAccountRealm,SimpleAccountRealm直接把用户认证数据存到实例中, SecurityManager使用DefaultSecurityManager, 使用SecurityUtils创建Subject, Token用UsernamePasswordToken。 用Junit进行测试。 maven依赖如下:

<!--单元测试-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13-beta-3</version>
</dependency>
<!--shiro核心包-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>

shiro认证示例

AuthenticationTest.java:

package com.lifeofcoding.shiro;

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.Test;

public class AuthenticationTest {
    @Test
    public void testAuthentication(){
        //1.创建Realm并添加数据
        SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
        simpleAccountRealm.addAccount("java","123");

        //2.创建SecurityManager并配置环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);

        //3.创建subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        //4.Subject通过Token提交认证
        UsernamePasswordToken token = new UsernamePasswordToken("java","123");
        subject.login(token);

        //验证认证情况
        System.out.println("isAuthenticated: "+ subject.isAuthenticated());
        //退出登录subject.logout();
    }
}

shiro授权示例

  SimpleAccountRealm添加用户角色和权限的方法比较简单,可以自己琢磨。此处的Realm改用IniRealm,iniRealm需要编写ini文件存储用户的信息,ini文件放在resource文件夹下。代码如下:
AuthorizationTest.java

package com.lifeofcoding.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
import java.util.ArrayList;

public class AuthorizationTest {
    @Test
    public void testAuthorization() {
        //1.创建Realm并添加数据
        IniRealm iniRealm = new IniRealm("classpath:UserData.ini");

        //2.创建SecurityManager并配置环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(iniRealm);

        //3.创建Subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        //4.主体提交认证
        UsernamePasswordToken token = new UsernamePasswordToken("java", "123");
        subject.login(token);

        /*以下为授权的几种方法*/
        //①.直接判断是否有某权限,isPermitted方法返回boolean,不抛异常
        System.out.println("user:login isPermitted: " + subject.isPermitted("user:login"));

        //②.通过角色进行授权,方法有返回值,不抛异常
        subject.hasRole("user");//判断主体是否有某角色
        subject.hasRoles(new ArrayList<String>() {//返回boolean数组,数组顺序与参数Roles顺序一致,接受List<String>参数
                             {
                                 add("admin");
                                 add("user");
                             }
                         });
        subject.hasAllRoles(new ArrayList<String>() {//返回一个boolean,Subject包含所有Roles时才返回true,接受Collection<String>参数
                               {
                                   add("admin");
                                   add("user");
                               }
                            });

        //③.通过角色授权,与上面大体相同,不过这里的方法无返回值,授权失败会抛出异常,需做好异常处理
        subject.checkRole("user");
        subject.checkRoles("user", "admin");//变参

        //④.通过权限授权,无返回值,授权失败抛出异常
        subject.checkPermission("user:login");
        //ini文件配置了test角色拥有"prefix:*"权限,也就是所有以"prefix"开头的权限
        subject.checkPermission("prefix:123:456:......");
        //ini文件配置了test角色拥有"*:*:suffix"权限,意味着其拥有所有以"suffix"结尾的,一共有三级的权限
        subject.checkPermission("1:2:suffix");
        subject.checkPermission("abc:123:suffix");
        subject.checkPermissions("user:login", "admin:login");//变参
        //subject.checkPermission(Permission permission); 需要Permission接口的实现类对象作参数
        //subject.checkPermissions(Collection<Permission> permissions);
    }
}

user.ini:

[users]
java = 123,user,admin,test
[roles]
user = user:login,user:modify
admin = user:delete,user:modify,admin:login
test = prefix:*,*:*:suffix
ini文件的Demo

[main]
# Objects and their properties are defined here,
# Such as the securityManager, Realms and anything
# else needed to build the SecurityManager
# 此处可以用来配置shiro组件,不用编写代码,如:

--CredentialsMatcher是用来设置加密的--##

hashedCredentialsMatcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher

--设置加密的算法--##

hashedCredentialsMatcher.hashAlgorithmName = MD5

--设置加密次数--##

hashedCredentialsMatcher.hashIterations = 1

--给Realm配置加密器的Matcher,"$"表引用--##

iniRealm.credentialsMatcher = $hashedCredentialsMatcher

--配置SecurityManager--##

securityManager = com.xxx.xxxManager
securityManager.realm = $iniRealm

[users]

The 'users' section is for simple deployments

when you only need a small number of statically-defined

set of User accounts.

此处是用户信息,以及用户与角色对应关系,格式为 username=password,roleName1,roleName2,roleName3,……

Java=123,user,admin
Go=123
Python=123,user

[roles]

The 'roles' section is for simple deployments

when you only need a small number of statically-defined

roles.

角色与权限对应关系,格式:rolename = permissionDefinition1, permissionDefinition2,……

user=user:delete,user:modify,user:login
admin=user:delete

[urls]

The 'urls' section is used for url-based security

in web applications. We'll discuss this section in the

Web documentation

用于配置网页过滤规则

/some/path = ssl, authc
/another/path = ssl, roles[admin]


下面介绍其他的Realm


JdbcRealm

  JdbcRealm包含默认的数据库查询语句,直接使用即可,但要注意创建的表结构要跟查询语句相对应。当然也可以自己去自定义查询语句和数据库。
  mavern依赖:

<!--数据库相关-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.6</version>
</dependency>

默认查询语句

默认的查询语句

默认的'users'表结构 ![](https://tva1.sinaimg.cn/large/006y8mN6gy1g6lhnbcl5ij308m01d0sn.jpg) 默认的'user_roles'表结构 ![](https://tva1.sinaimg.cn/large/006y8mN6gy1g6lh4vs6jej306001aa9y.jpg) 默认的'roles_permissions'表结构 ![](https://tva1.sinaimg.cn/large/006y8mN6gy1g6lhezdqcnj306401cgli.jpg)
数据库sql语句

create database shiro;
use shiro;
SET FOREIGN_KEY_CHECKS=0;

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;

INSERT INTO roles_permissions VALUES (null,'admin','user:delete');

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 AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO users VALUES ('1', 'java', '123', null);

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;

INSERT INTO user_roles VALUES (null,'java','admin');

JdbcRealmTest.java:

package com.lifeofcoding.shiro;

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class JdbcRealmTest {

    DruidDataSource druidDataSource = new DruidDataSource();
    {
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("0113");
    }

    @Test
    public void testJdbcRealm(){

        //1.创建Realm并添加数据
        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setDataSource(druidDataSource);//配置数据源
        jdbcRealm.setPermissionsLookupEnabled(true);//设置允许查询权限,否则checkPermission抛异常,默认值为false

        //2.创建SecurityManager并配置环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(jdbcRealm);

        //3.创建subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        //4.Subject通过Token提交认证
        UsernamePasswordToken token = new UsernamePasswordToken("java","123");
        subject.login(token);//退出登录subject.logout();

        //验证认证与授权情况
        System.out.println("isAuthenticated: "+ subject.isAuthenticated());
        subject.hasRole("admin");
        subject.checkPermission("user:delete");
    }
}

自定义查询语句

自定义的查询语句

自定义的'test_users'表结构


自定义的'test_user_roles'表结构


自定义的'test_roles_permissions'表结构

数据库sql语句

DROP TABLE IF EXISTS test_users;
CREATE TABLE test_users (
  user_name varchar(20) DEFAULT NULL,
  password varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test_users VALUES('java','123');

DROP TABLE IF EXISTS test_user_roles;
CREATE TABLE test_user_roles (
user_name varchar(20) DEFAULT NULL,
role varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test_user_roles VALUES('java','admin');

DROP TABLE IF EXISTS test_roles_permissions;
CREATE TABLE test_roles_permissions (
role varchar(20) DEFAULT NULL,
permission varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO test_roles_permissions VALUES('admin','user:delete');

MyJdbcRealmTest.java:

import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class MyJdbcRealmTest {

    //从数据库获取对应用户密码实现认证
    protected static final String AUTHENTICATION_QUERY = "select password from test_users where user_name = ?";
    //从数据库中获取对应用户的所有角色
    protected static final String USER_ROLES_QUERY = "select role from test_user_roles where user_name = ?";
    //从数据库中获取角色对应的所有权限
    protected static final String PERMISSIONS_QUERY = "select permission from test_roles_permissions where role = ?";

    DruidDataSource druidDataSource = new DruidDataSource();
    {
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("0113");
    }

    @Test
    public void testMyJdbcRealm(){

        //1.创建Realm并设置数据库查询语句
        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setDataSource(druidDataSource);//配置数据源
        jdbcRealm.setPermissionsLookupEnabled(true);//设置允许查询权限,否则checkPermission抛异常,默认值为false
        jdbcRealm.setAuthenticationQuery(AUTHENTICATION_QUERY);//设置用户认证信息查询语句
        jdbcRealm.setUserRolesQuery(USER_ROLES_QUERY);//设置用户角色信息查询语句
        jdbcRealm.setPermissionsQuery(PERMISSIONS_QUERY);//设置角色权限信息查询语句

        //2.创建SecurityManager并配置环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(jdbcRealm);

        //3.创建subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        //4.Subject通过Token提交认证
        UsernamePasswordToken token = new UsernamePasswordToken("java","123");
        subject.login(token);//退出登录subject.logout();

        //验证认证与授权情况
        System.out.println("isAuthenticated: "+ subject.isAuthenticated());
        subject.hasRole("admin");
        subject.checkPermission("user:delete");
    }
}

自定义Realm

  自定义Realm,可以继承抽象类AuthorizingRealm,实现其两个方法——doGetAuthenticationInfo和doGetAuthorizationInfo,分别用来返回AuthenticationInfo(认证信息)和AuthorizationInfo(授权信息)。

如上所示,AuthenticationInfo包含principal和crendential,和token一样,不同的是,前者是切切实实的在数据库或其他数据源中的数据,而后者是用户输入的,待校验的数据。
AuthorizationInfo也是类似的,包含用户的角色和权限信息。 可以用SimpleAuthenticationInfo和SimpleAuthorizationInfo来实现这两个方法:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    //1.获取主体中的用户名
    String userName = (String) authenticationToken.getPrincipal();
    //2.通过用户名获取密码,getPasswordByName自定义实现
    String password = getPasswordByUserName(userName);
    if(null == password){
        return null;
    }
    //构建AuthenticationInfo返回
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
    return authenticationInfo;
}

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    //1.获取用户名。principal为Object类型,是用户唯一标识,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
    String userName = (String) principalCollection.getPrimaryPrincipal();
    //2.获取角色信息,getRoleByUserName自定义
    Set<String> roles = getRolesByUserName(userName);
    //3.获取权限信息,getPermissionsByRole方法同样自定义,也可以通过用户名查找权限信息
    Set<String> permissions = getPermissionsByUserName(userName);
    //4.构建认证信息并返回。
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    simpleAuthorizationInfo.setStringPermissions(permissions);
    simpleAuthorizationInfo.setRoles(roles);
    return simpleAuthorizationInfo;
}

完整的,包含添加用户功能的自定义Realm
MyRealm.java:

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.apache.shiro.util.CollectionUtils;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class MyRealm extends AuthorizingRealm {

    /**存储用户名和密码*/
    protected final Map<String,String> userMap;
    /**存储用户及其对应的角色*/
    protected final Map<String, Set<String>> roleMap;
    /**存储所有角色以及角色对应的权限*/
    protected final Map<String,Set<String>> permissionMap;

    {
        //设置Realm名
        super.setName("MyRealm")  ;
    }

    public MyRealm(){
        userMap = new ConcurrentHashMap<>(16);
        roleMap = new ConcurrentHashMap<>(16);
        permissionMap = new ConcurrentHashMap<>(16);
    }

    /**
     * 身份认证必须实现的方法
     * @param authenticationToken token
     * @return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //1.获取主体中的用户名
        String userName = (String) authenticationToken.getPrincipal();
        //2.通过用户名获取密码,getPasswordByName自定义实现
        String password = getPasswordByUserName(userName);
        if(null == password){
            return null;
        }
        //构建AuthenticationInfo返回
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
        return authenticationInfo;
    }

    /**
     * 用于授权
     * @return org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //1.获取用户名。principal为Object类型,是用户唯一标识,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
        String userName = (String) principalCollection.getPrimaryPrincipal();
        //2.获取角色信息,getRoleByUserName自定义
        Set<String> roles = getRolesByUserName(userName);
        //3.获取权限信息,getPermissionsByRole方法同样自定义,也可以通过用户名查找权限信息
        Set<String> permissions = getPermissionsByUserName(userName);
        //4.构建认证信息并返回。
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 自定义部分,通过用户名获取权限信息
     * @return java.util.Set<java.lang.String>
     */
    private Set<String> getPermissionsByUserName(String userName) {
        //1.先通过用户名获取角色信息
        Set<String> roles = getRolesByUserName(userName);
        //2.通过角色信息获取对应的权限
        Set<String> permissions = new HashSet<String>();
        //3.添加每个角色对应的所有权限
        roles.forEach(role -> {
            if(null != permissionMap.get(role)) {
                permissions.addAll(permissionMap.get(role));
            }

        });
        return permissions;
    }

    /**
     * 自定义部分,通过用户名获取密码,可改为数据库操作
     * @param userName 用户名
     * @return java.lang.String
     */
    private String getPasswordByUserName(String userName){
        return userMap.get(userName);
    }

    /**
     * 自定义部分,通过用户名获取角色信息,可改为数据库操作
     * @param userName 用户名
     * @return java.util.Set<java.lang.String>
     */
    private Set<String> getRolesByUserName(String userName){
        return roleMap.get(userName);
    }

    /**
     * 往realm添加账号信息,变参不传值会接收到长度为0的数组。
     */
    public void addAccount(String userName,String password) throws UserExistException{
        addAccount(userName,password,(String[]) null);
    }

    /**
     * 往realm添加账号信息
     * @param userName 用户名
     * @param password 密码
     * @param roles 角色(变参)
     */
    public void addAccount(String userName,String password,String... roles) throws UserExistException{
        if( null != userMap.get(userName) ){
            throw new UserExistException("user \""+ userName +" \" exists");
        }
        userMap.put(userName, password);
        roleMap.put(userName, CollectionUtils.asSet(roles));
    }

    /**
     * 从realm删除账号信息
     * @param userName 用户名
     */
    public void delAccount(String userName){
        userMap.remove(userName);
        roleMap.remove(userName);
    }

    /**
     * 添加角色权限,变参不传值会接收到长度为0的数组。
     * @param roleName 角色名
     */
    public void addPermission(String roleName,String...permissions){
        permissionMap.put(roleName,CollectionUtils.asSet(permissions));
    }

    /**用户已存在异常*/
    public class UserExistException extends Exception{
        public UserExistException(String message){super(message);}
    }
}

测试代码
MyRealmTest.java:

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class MyRealmTest {

    @Test
    public void testMyRealm(){

        //1.创建Realm并添加数据
        MyRealm myRealm = new MyRealm();
        try {
            myRealm.addAccount("java", "123", "admin");
        }catch (Exception e){
            e.printStackTrace();
        }
        myRealm.addPermission("admin","user:delete");

        //2.创建SecurityManager并配置环境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(myRealm);

        //3.创建subject
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();

        //4.Subject通过Token提交认证
        UsernamePasswordToken token = new UsernamePasswordToken("java","123");
        subject.login(token);//退出登录subject.logout();

        //验证认证与授权情况
        System.out.println("isAuthenticated: "+ subject.isAuthenticated());
        subject.hasRole("admin");
        subject.checkPermission("user:delete");
    }
}

加密

  使用加密,只需要在Realm返回的AuthenticationInfo添加用户密码对应的盐值:

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    //1.获取主体中的用户名
    String userName = (String) authenticationToken.getPrincipal();
    //2.通过用户名获取密码,getPasswordByName自定义实现
    String password = getPasswordByUserName(userName);
    if(null == password){
        return null;
    }
    //3.构建authenticationInfo认证信息
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
    //设置盐值
    String salt = getSaltByUserName(userName);
    authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
    return authenticationInfo;
}

  并且在测试代码中给Realm设置加密:

//设置加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");//设置加密算法
matcher.setHashIterations(3);//设置加密次数
myEncryptedRealm.setCredentialsMatcher(matcher);
  CredentialMatcher用来匹配用户密码。shiro通过Realm获取AuthenticationInfo,AuthenticationInfo里面包含该用户的principal、credential、salt。principal就是用户名或手机号或其他,credential就是密码(加盐加密后,存到数据源中的密码),salt就是密码对应的“盐”。shiro获取到这些信息之后,利用CredentialMatcher中配置的信息(加密算法,加密次数等),对token中用户输入的、待校验的密码,进行加盐加密,然后比对结果是否和AuthenticationInfo中的credential一致,若一致,则用户通过认证。   “加密”算法一般用的是hash算法,hash算法并不是用来加密的,而是用来生成信息摘要,该过程是不可逆的,不能从结果逆推得出用户的密码的原文。下文这一段话也是相对于hash算法而言,其他算法不在考虑范围内。shiro的CredentialMatcher并没有“解密”这个概念,因为不能解密,不能把数据库中“加密”后的密码还原,只能对用户输入的密码,进行一次相同的“加密”,然后比对数据库的“加密”后的密码,从而判断用户输入的密码是否正确。

  必要地,在存密码时,要存储加盐加密后的密码:

public void addAccount(String userName,String password,String... roles) throws UserExistException {
    if(null != userMap.get(userName)){
        throw new UserExistException("user \""+ userName +"\" exist");
    }
    //如果配置的加密次数大于0,则进行加密
    if(iterations > 0){
        //使用随机数作为密码,可改为UUID或其它
        String salt = String.valueOf(Math.random()*10);
        saltMap.put(userName,salt);
        password = doHash(password, salt);
    }
    userMap.put(userName, password);
    roleMap.put(userName, CollectionUtils.asSet(roles));
}
MyEncryptedRealm.java

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.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class MyEncryptedRealm extends AuthorizingRealm {

/** 加密次数 */
private int iterations;
/** 算法名 */
private String algorithmName;

/** 存储用户名和密码 */
private final Map<String,String> userMap;
/** 存储用户及其对应的角色 */
private final Map<String, Set<String>> roleMap;
/** 存储所有角色以及角色对应的权限 */
private final Map<String,Set<String>> permissionMap;
/** 存储用户盐值 */
private Map<String,String> saltMap;

{
    //设置Realm名
    super.setName("MyRealm");
}

public MyEncryptedRealm(){
    this.iterations = 0;
    this.algorithmName = "MD5";
    this.userMap = new ConcurrentHashMap<>(16);
    this.roleMap = new ConcurrentHashMap<>(16);
    this.permissionMap  = new ConcurrentHashMap<>(16);
    this.saltMap = new ConcurrentHashMap<>(16);
}

/**
 * 身份认证必须实现的方法
 * @param authenticationToken token
 * @return org.apache.shiro.authc.AuthenticationInfo
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    //1.获取主体中的用户名
    String userName = (String) authenticationToken.getPrincipal();
    //2.通过用户名获取密码,getPasswordByName自定义实现
    String password = getPasswordByUserName(userName);
    if(null == password){
        return null;
    }
    //3.构建authenticationInfo认证信息
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
    //设置盐值
    String salt = getSaltByUserName(userName);
    authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
    return authenticationInfo;
}


/**
 * 用于授权
 * @return org.apache.shiro.authz.AuthorizationInfo
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    //1.获取用户名。principal为Object类型,是用户唯一凭证,可以是用户名,用户邮箱,数据库主键等,能唯一确定一个用户的信息。
    String userName = (String) principalCollection.getPrimaryPrincipal();
    //2.获取角色信息,getRoleByUserName自定义
    Set<String> roles = getRolesByUserName(userName);
    //3.获取权限信息,getPermissionsByRole方法同样自定义,也可以通过用户名查找权限信息
    Set<String> permissions = getPermissionsByUserName(userName);
    //4.构建认证信息并返回。
    SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    simpleAuthorizationInfo.setStringPermissions(permissions);
    simpleAuthorizationInfo.setRoles(roles);
    return simpleAuthorizationInfo;
}

/**
 * 自定义部分,通过用户名获取权限信息
 * @return java.util.Set<java.lang.String>
 */
private Set<String> getPermissionsByUserName(String userName) {
    //1.先通过用户名获取角色信息
    Set<String> roles = getRolesByUserName(userName);
    //2.通过角色信息获取对应的权限
    Set<String> permissions = new HashSet<>();
    //3.添加每个角色对应的所有权限
    roles.forEach(role -> {
        if (null != permissionMap.get(role)) {
            permissions.addAll(permissionMap.get(role));
        }
    });
    return permissions;
}

/**
 * 自定义部分,通过用户名获取密码,可改为数据库操作
 * @return java.lang.String
 */
private String getPasswordByUserName(String userName){
    return userMap.get(userName);
}

/**
 * 自定义部分,通过用户名获取角色信息,可改为数据库操作
 * @return java.util.Set<java.lang.String>
 */
private Set<String> getRolesByUserName(String userName){
    return roleMap.get(userName);
}

/**
 * 自定义部分,通过用户名获取盐值,可改为数据库操作
 * @return java.util.Set<java.lang.String>
 */
private String getSaltByUserName(String userName) {
    return saltMap.get(userName);
}

/**
 * 往realm添加账号信息,变参不传值会接收到长度为0的数组。
 */
public void addAccount(String userName,String password) throws UserExistException {
    addAccount(userName,password,(String[]) null);
}

/**
 * 往realm添加账号信息
 */
public void addAccount(String userName,String password,String... roles) throws UserExistException {
    if(null != userMap.get(userName)){
        throw new UserExistException("user \""+ userName +"\" exist");
    }
    //如果配置的加密次数大于0,则进行加密
    if(iterations > 0){
        String salt = String.valueOf(Math.random()*10);
        saltMap.put(userName,salt);
        password = doHash(password, salt);
    }
    userMap.put(userName, password);
    roleMap.put(userName, CollectionUtils.asSet(roles));
}

/**
 * 从realm删除账号信息
 * @param userName 用户名
 */
public void deleteAccount(String userName){
    userMap.remove(userName);
    roleMap.remove(userName);
}


/**
 * 添加角色权限,变参不传值会接收到长度为0的数组。
 * @param roleName 角色名
 */
public void addPermission(String roleName,String...permissions){
    permissionMap.put(roleName, CollectionUtils.asSet(permissions));
}

/**
 * 设置加密次数
 * @param iterations 加密次数
 */
public void setHashIterations(int iterations){
    this.iterations = iterations;
}

/**
 * 设置算法名
 * @param algorithmName 算法名
 */
public void setAlgorithmName(String algorithmName){
    this.algorithmName = algorithmName;
}

/**
 * 计算哈希值
 * @param str 要进行"加密"的字符串
 * @param salt 盐
 * @return String
 */
private String doHash(String str,String salt){
    salt = null==salt ? "" : salt;
    return new SimpleHash(this.algorithmName,str,salt,this.iterations).toString();
}


/**
 * 注册时,用户已存在的异常
 */
public class UserExistException extends Exception{
    public UserExistException(String message) {super(message);}
}

}

MyEncryptedRealmTest.java
 
package com.lifeofcoding.shiro;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

/**

  • @Classname MyEncryptionRealmTest

  • @Description TODO

  • @Date 2019-11-2019-11-20-17:41

  • @Created by yo
    */
    public class MyEncryptedRealmTest {

    private MyEncryptedRealm myEncryptedRealm;

    @Test
    public void testShiroEncryption() {
    //1.创建Realm并添加数据
    MyEncryptedRealm myEncryptedRealm = new MyEncryptedRealm();
    myEncryptedRealm.setHashIterations(3);
    myEncryptedRealm.setAlgorithmName("MD5");
    try {
    myEncryptedRealm.addAccount("java", "123456", "admin");
    }catch (Exception e){
    e.printStackTrace();
    }
    myEncryptedRealm.addPermission("admin","user:create","user:delete");

     //2.创建SecurityManager对象
     DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(myEncryptedRealm);
    
     //3.设置加密
     HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
     matcher.setHashAlgorithmName("MD5");//设置加密算法
     matcher.setHashIterations(3);//设置加密次数
     myEncryptedRealm.setCredentialsMatcher(matcher);
    
     //4.创建主体并提交认证
     SecurityUtils.setSecurityManager(defaultSecurityManager);
     Subject subject = SecurityUtils.getSubject();
    
     UsernamePasswordToken token = new UsernamePasswordToken("java","123456");
     subject.login(token);
     System.out.println(subject.getPrincipal()+" isAuthenticated: "+subject.isAuthenticated());
     subject.checkRole("admin");
     subject.checkPermission("user:delete");
    

    }
    }

文件传送门

github地址

posted @ 2020-01-09 21:29  LifeOfCoding  阅读(958)  评论(1编辑  收藏  举报