Shiro 基本使用总结

1.简介

Apache Shiro是一个强大且易用的 Java 安全框架,执行身份验证、授权、密码学和会话管理。

认证、授权:

认证简单的说,就是登录的时候判断你的用户名和密码是否完全匹配,就是证明你是你。

授权,是在认证的基础之上,进行角色和权限的授予。权限决定了一个用户可以进行怎样的操作

角色、权限:

权限定义了一个用户是否可以执行某个操作。

角色就是一组权限的集合。

我们通常是把一组权限绑定到一种角色上,再把一个或者多个角色赋给一个用户,这样就实现了权限的控制。即权限通过角色定义到用户上。角色作为权限的集合,方便了我们对权限的管理。

2.实现简单的登录功能

  1. 添加shiro依赖
 <dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-coreartifactId>
    <version>1.3.2version>
dependency>
<dependency>
    <groupId>junitgroupId>
    <artifactId>junitartifactId>
    <version>${junit.version}version>
    <scope>testscope>
dependency>
  1. 配置文件shiro.ini
[users]
heqianqian = 12345666

3.编写测试代码

public class SimpleLoginTest {
    @Test
    public void test() throws Exception {
        ShiroUtils.login("classpath:shiro/shiro.ini", "heqianqian", "12345666");
    }
}
public class ShiroUtils {

    public static Subject getSubject(String path) {
        IniSecurityManagerFactory factory = new IniSecurityManagerFactory(path);
        SecurityManager manager = factory.getInstance();
        SecurityUtils.setSecurityManager(manager);
        return SecurityUtils.getSubject();
    }

    public static Subject login(String path, String userName, String passWord) {
        Subject subject = getSubject(path);
        UsernamePasswordToken token = new UsernamePasswordToken(userName, passWord);
        try {
            subject.login(token);
            System.out.println("登录成功");
        } catch (UnknownAccountException e) {
            System.out.println("无效的用户名");
            e.printStackTrace();
        } catch (IncorrectCredentialsException e) {
            System.out.println("密码错误");
            e.printStackTrace();
        } catch (AuthenticationException e) {
            e.printStackTrace();
        }

        //subject.logout();
        //System.out.println("退出成功");
        return subject;
    }
}
  • Subject 就是我们登录的主体,这里相当于一个桥梁,我们所有的操作其实都要通过 Subject 来帮助我们完成。
  • SecurityManager 类似于 spring MVC 中的 DispatcherServlet ,起到类似前端控制器的作用
  • Realm 是安全数据源,我们要把类似于用户名和密码的信息存放到 Realm 中,Shiro 就可以帮助我们完成认证和授权等一系列操作
    在这个例子中的 Realm 叫 IniRealm

3.使用JDBCRealm

前面例子的数据写的配置文件里,我们也可以从数据库读取数据

  1. 添加依赖
 <dependency>
    <groupId>org.apache.shirogroupId>
    <artifactId>shiro-coreartifactId>
    <version>1.3.2version>
dependency>
<dependency>
   <groupId>c3p0groupId>
   <artifactId>c3p0artifactId>
   <version>0.9.1.2version>
dependency>
<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>5.1.39version>
dependency>
<dependency>
     <groupId>log4jgroupId>
      <artifactId>log4jartifactId>
      <version>1.2.17version>
dependency>
<dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-apiartifactId>
    <version>${slf4j.version}version>
dependency>
<dependency>
    <groupId>org.slf4jgroupId>
    <artifactId>slf4j-log4j12artifactId>
    <version>${slf4j.version}version>
dependency>
  1. 编写配置文件jdbcRealm.ini
[main]
# 表示实例化右边字符串表示的类,赋给左边字符串表示的变量。
##JdbcRealm默认使用users表
jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
dataSource = com.mchange.v2.c3p0.ComboPooledDataSource
dataSource.driverClass = com.mysql.jdbc.Driver
dataSource.user = root
dataSource.password = ****
# 表示对 dataSource 这个变量设置属性值 jdbcUrl ,这个属性值是一个字符串。
dataSource.jdbcUrl = jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=UTF-8
# 表示对 jdbcRealm 这个变量设置属性值 dataSource , 这个 dataSource 属性是上面实例化的一个对象,所以表示这个对象要使用前缀 `$`。
jdbcRealm.dataSource = $dataSource
securityManager.realms = $jdbcRealm
  1. 数据库建表
DROP DATABASE db_shiro;
# 创建数据库 db_shiro
CREATE DATABASE db_shiro DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
# 使用数据库 db_shiro
USE db_shiro;

drop table if exists users;
create table users(
  id int(11) not null auto_increment comment '主键',
  username varchar(20) default null comment '用户名',
  password varchar(20) default null comment '密码',
  PRIMARY KEY (id)
)engine=innodb auto_increment=1 default charset=utf8 comment '用户表';

insert into users(username,password) values('hqq','123456');

注意:
这里建表的表名是users 因为JdbcRealm默认查找users表里的数据

JdbcRealm 源码

public class JdbcRealm extends AuthorizingRealm {
    protected static final String DEFAULT_AUTHENTICATION_QUERY = "select password from users where username = ?";
    protected static final String DEFAULT_SALTED_AUTHENTICATION_QUERY = "select password, password_salt from users where username = ?";
    protected static final String DEFAULT_USER_ROLES_QUERY = "select role_name from user_roles where username = ?";
    protected static final String DEFAULT_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ?";
...
}

4.编写测试方法

public class JdbcRealmTest {

    private String userName = "heqianqian";

    private String passWord = "123";

    private Logger logger = Logger.getLogger(JdbcRealmTest.class);

    @Test
    public void test() throws Exception {
        ShiroUtils.login("classpath:shiro/jdbcRealm.ini", userName, passWord);
    }
}

4.自定义Realm

方式一:implements Realm

这种方式实现的 Realm 仅只能实现认证操作,并不能实现授权操作。

public class MapRealm implements Realm {

    private static Map users = new HashMap<>();

    static {
        users.put("heqianqian", "123");
        users.put("lucy", "456");
    }

    /**
     * 返回唯一的Realm名字
     */
    @Override
    public String getName() {
        System.out.println("设置Realm的名字");
        return "My MapRealm";
    }

    /**
     * 判断是否支持Token
     */
    @Override
    public boolean supports(AuthenticationToken authenticationToken) {
        System.out.println("Map Realm 中给出支持的 Token 的方法");
        // 表示仅支持 UsernamePasswordToken 类型的 Token
        return authenticationToken instanceof UsernamePasswordToken;
    }

    /**
     * 获取认证信息
     */
    @Override
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("Map Realm 中返回认证信息的方法");
        String userName = (String) authenticationToken.getPrincipal();
        String passWord = new String((char[])authenticationToken.getCredentials());
        if (!users.containsKey(userName)) {
            System.out.println("用户名错误!");
        } else if (!users.get(userName).equals(passWord)) {
            System.out.println("密码错误!");
        }
        return new SimpleAuthenticationInfo(userName, passWord, getName());
    }
}

编写配置文件mapRealm.ini

[main]
# 声明了我们自己定义的一个 Realm
myMapRealm = heqianqian.shiro.realm.MapRealm
# 将我们自己定义的 Realm 注入到 securityManager 的 realms 属性中去
securityManager.realms = $myMapRealm

测试

public class MapRealmTest {
    @Test
    public void test() throws Exception {
        ShiroUtils.login("classpath:shiro/mapRealm.ini","heqianqian","123");
    }
}

方式二:extends AuthorizingRealm(比较常用的一种方式,因为这样做既可以实现认证操作,也可以实现授权操作)

public class MyStaticRealm extends AuthorizingRealm {

    /**
     * 用于授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 暂时忽略,以后介绍
        return null;
    }

    /**
     * 用于认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("Static Realm 中认证的方法");
        String userName = authenticationToken.getPrincipal().toString();
        String password = new String((char[]) authenticationToken.getCredentials());
        if (!"heqianqian".equals(userName)) {
            throw new UnknownAccountException("无效的用户名");
        } else if (!"123".equals(password)) {
            throw new IncorrectCredentialsException("密码无效");
        }
        return new SimpleAuthenticationInfo("heqianqian", "123", getName());
    }
}

编写配置文件mystaticRealm.ini

[main]
myStaticRealm = heqianqian.shiro.realm.MyStaticRealm
securityManager.realms = $myStaticRealm

测试

 @Test
    public void testPermission() throws Exception {
        Subject subject = ShiroUtils.login("classpath:shiro/mystaticRealm.ini", "heqianqian", "123");
    }

知识点:配置认证策略

这时候,我们会有一个疑问,securityManager 的属性既然是 realms,说明可以设置若干个 Realm,它们认证的顺序是如何的呢。

那么对于若干个 Realm,Shiro 提供了一种配置方式,让我们来决定在多个 Reaml 同时声明的情况下,采用哪些 Realm 返回的认证信息的方式,这就是我们的认证策略。

认证策略主要有以下三种:

1、FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;

2、AtLeastOneSuccessfulStrategy: (这是默认使用的认证策略,即在不配置情况下 Shiro 所采用的认证策略)只要有一个 Realm 验证成功即可, 和 FirstSuccessfulStrategy 不同,返回所有 Realm 身份验证成功的认证信息;

3、AllSuccessfulStrategy:所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。

配置示例

# 配置认证策略
allSuccessfulStrategy = org.apache.shiro.authc.pam.AllSuccessfulStrategy
securityManager.authenticator.authenticationStrategy = $allSuccessfulStrategy

5. 基于字符串的角色和权限

编写配置文件

[users]
hqq = 123,role1,role2
lucy = 111,role1
[roles]
role1 = user:select
role2 = user:add,user:update,user:delete

测试角色和权限

public class RoleTest {

    @Test
    public void testHasRole() throws Exception {
        Subject subject = ShiroUtils.login("classpath:shiro/shiro-role.ini", "hqq", "123");
        assertEquals(true, subject.hasRole("role1"));
        assertEquals(true, subject.hasRole("role2"));
        assertEquals(true, subject.hasAllRoles(Arrays.asList("role1", "role2")));
        subject.logout();
    }

    @Test
    public void testCheckRole() throws Exception {
        Subject subject = ShiroUtils.login("classpath:shiro/shiro-role.ini", "hqq", "123");
        subject.checkRole("role1");
        subject.checkRole("role2");
        subject.checkRoles(Arrays.asList("role1", "role2"));
        assertEquals(true, subject.hasRole("role2"));

    }
}
public class PermissionTest {

    @Test
    public void testHasPermission() throws Exception {
        Subject subject = ShiroUtils.login("classpath:shiro/shiro-permission.ini", "hqq", "123");
        assertEquals(true, subject.isPermitted("user:select"));
        subject.isPermitted("user:select", "user:add", "user:delete", "user:update");
        assertEquals(true, subject.isPermittedAll("user:select", "user:select", "user:add", "user:delete", "user:update"));
        subject.logout();
    }

    @Test
    public void testCheckPermission() throws Exception {
        Subject subject = ShiroUtils.login("classpath:shiro/shiro-permission.ini", "hqq", "123");
        subject.checkPermission("user:select");
        subject.checkPermissions("user:select", "user:add", "user:delete", "user:update");
        subject.logout();
    }
}

6.自定义权限解析器和角色解析器

自定义权限解析规的步骤

步骤1:实现 Permission 接口

public class MyPermission implements Permission {

    private String resourceId;
    private String operator;
    private String instanceId;

    public MyPermission() {

    }

    public MyPermission(String permissionStr) {
        String[] strs = permissionStr.split("\\+");
        if (strs.length > 1) {
            this.resourceId = strs[1];
        }
        if (this.resourceId == null || "".equals(this.resourceId)) {
            this.resourceId = "*";
        }
        if (strs.length > 2) {
            this.operator = strs[2];
        }
        if (strs.length > 3) {
            this.instanceId = strs[3];
        }
        if (this.instanceId == null || "".equals(this.instanceId)) {
            this.instanceId = "*";
        }

        System.out.println("实例化 MyPermission 时 => " + this.toString());
    }

    /**
     * 【这是一个非常重要的方法】
     * 由程序员自己编写授权是否匹配的逻辑,
     * 我们这里的实现,是将 Realm 中给出的 Permission 和 ini 配置中指定的 PermissionResoler 中指定的 Permission 进行比对
     * 比对的规则完全由我们自己定义
     */
    @Override
    public boolean implies(Permission permission) {
        if (!(permission instanceof MyPermission)) {
            return false;
        }
        MyPermission mp = (MyPermission) permission;
        if (!"*".equals(mp.resourceId) && !this.resourceId.equals(mp.resourceId)) {
            return false;
        }
        if (!"*".equals(mp.operator) && !this.operator.equals(mp.operator)) {
            return false;
        }
        if (!"*".equals(mp.instanceId) && !this.instanceId.equals(mp.instanceId)) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "MyPermission{" +
                "resourceId='" + resourceId + '\'' +
                ", operator='" + operator + '\'' +
                ", instanceId='" + instanceId + '\'' +
                '}';
    }
}

步骤2:实现 PermissionResolver 接口

实现 PermissionResolver 接口的意义在于告诉 Shiro 根据字符串的表现形式(表现特征),采用什么样的 Permission 进行匹配。

public class MyPermissionResolver implements PermissionResolver {
    @Override
    public Permission resolvePermission(String s) {
    /*如果我们的权限字符串是以 “+” 号开头的话,就使用我们自定义的 MyPermission ,否则就使用默认的 WildcardPermission 解析这个字符串。*/
        if (s.startsWith("+")) {
            return new MyPermission(s);
        }
        return new WildcardPermission(s);
    }
}

说明:这段代码实现的一个效果是:如果我们的权限字符串是以 “+” 号开头的话,就使用我们自定义的 MyPermission ,否则就使用默认的 WildcardPermission 解析这个字符串。

步骤3:实现授权的逻辑

public class MyStaticRealm extends AuthorizingRealm {

    /**
     * 用于授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("Static Realm 中授权的方法");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRole("r1");
        info.addRole("r2");
        info.addRole("role1");
        info.addStringPermission("+user+");
        info.addObjectPermission(new MyPermission("+user+add+1"));
        return info;
    }

    /**
     * 用于认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("Static Realm 中认证的方法");
        String userName = authenticationToken.getPrincipal().toString();
        String password = new String((char[]) authenticationToken.getCredentials());
        if (!"heqianqian".equals(userName)) {
            throw new UnknownAccountException("无效的用户名");
        } else if (!"123".equals(password)) {
            throw new IncorrectCredentialsException("密码无效");
        }
        return new SimpleAuthenticationInfo("heqianqian", "123", getName());
    }
}

编写配置文件

#配置自定义权限解析器
permissionResolver = heqianqian.shiro.permission.MyPermissionResolver
authorizer.permissionResolver = permissionResolver
securityManager.authorizer = $authorizer

自定义角色匹配器

步骤1:实现 RolePermissionResolver 接口

public class MyRolePermissionResolver implements RolePermissionResolver {
    @Override
    public Collection resolvePermissionsInRole(String s) {
        System.out.println("MyRolePermissionResolver");
        if (s.contains("role1")) {
            return Arrays.asList((Permission) new MyPermission("+user+add+2"));
        }
        return null;
    }
}

步骤2:在 shiro.ini 中配置我们的 RolePermissionResolver

authorizer = org.apache.shiro.authz.ModularRealmAuthorizer

##配置RolePermissionResolver
rolePermissionResolver = heqianqian.shiro.role.MyRolePermissionResolver
authorizer.rolePermissionResolver = rolePermissionResolver

#配置自定义权限解析器
permissionResolver = heqianqian.shiro.permission.MyPermissionResolver
authorizer.permissionResolver = permissionResolver
securityManager.authorizer = $authorizer

步骤3:在自定义的 Realm 的授权方法中添加角色

public class MyStaticRealm extends AuthorizingRealm {

    /**
     * 用于授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("static Realm 中授权的方法");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addRole("role1");
        return info;
    }

    /**
     * 用于认证
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 省略
    }
}

步骤 4 :判断用户是否具有授权方法中指定角色的权限

currentSubject.checkPermission("+user+add+1");

7. 加密

PasswordService service = new DefaultPasswordService();
String str1 = service.encryptPassword("123456");
String str2 = service.encryptPassword("123456");
System.out.println(str1);
System.out.println(str2);
// 盐值是存放在加密以后的密码中的
boolean boolean1 = service.passwordsMatch("123456",str1);
System.out.println(boolean1);
boolean boolean2 = service.passwordsMatch("123456",str2);
System.out.println(boolean2)

自定义 Realm 的认证实现部分:

public class MyPasswordRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    /**
     * 验证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // default
        String userName = (String) authenticationToken.getPrincipal();
        //String passWordFromDB = "$shiro1$SHA-256$500000$onR5YN4/BqH6toWLn9atsg==$g31FX9UERzy36yBlruzIZqGwCLf8V9P5p3hBrZQOAY0=";
        //md5+salt
        String passWordFromDB = "c39d11270c99cb00a13a6ac1e54effa2";
        String salt = "hqq";
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userName, passWordFromDB, getName());
        info.setCredentialsSalt(ByteSource.Util.bytes(salt.getBytes()));
        return info;
    }
}

配置文件

# 声明一个 Shiro 已经有的密码匹配的类
passwordMatcher=org.apache.shiro.authc.credential.PasswordMatcher
# 声明自定义的 Realm 类
;myPasswordRealm= heqianqian.shiro.realm.MyPasswordRealm
# 将 passwordMatcher 注入到自定义的 Realm 类中
myPasswordRealm.credentialsMatcher=$passwordMatcher
# 将自定义的 Realm 注入到 securityManager 中
securityManager.realms=$myPasswordRealm

加盐

String originPassword = "123456";
String salt = "hello";
String md5 = new Md5Hash(originPassword,salt).toString();
System.out.println(md5);
public class MyPasswordRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username  = authenticationToken.getPrincipal().toString();
        // String password = new String((char[]) authenticationToken.getCredentials());
        // 此时我们应该从数据库中根据 username 查询出对应的密码
        String passwordFromDB = "eeb9bad681184779aa6570e402d6ef6c";
        String salt = "hello";
        SimpleAuthenticationInfo info= new SimpleAuthenticationInfo(username,passwordFromDB,getName());
        // 设置加密算法的盐值,使用 ByteSource 这个工具类
        info.setCredentialsSalt(ByteSource.Util.bytes(salt.getBytes()));
        return info;
    }
}

配置文件

# 声明一个 Shiro 已经有的密码匹配的类
hashMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
hashMatcher.hashAlgorithmName=md5
hashMatcher.hashSalted=hello
# 声明自定义的 Realm 类
myPasswordRealm=com.liwei.realm.MyPasswordRealm
# 将 passwordMatcher 注入到自定义的 Realm 类中
myPasswordRealm.credentialsMatcher=$hashMatcher
# 将自定义的 Realm 注入到 securityManager 中
securityManager.realms=$myPasswordRealm

参考文章

http://blog.csdn.net/lw_power?viewmode=contents

你可能感兴趣的:(Shiro)