Shiro 基本使用总结

来源:互联网 发布:淘宝千人千面 编辑:程序博客网 时间:2024/06/02 06:09

1.简介

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

认证、授权:

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

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

角色、权限:

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

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

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

2.实现简单的登录功能

  1. 添加shiro依赖
 <dependency>    <groupId>org.apache.shiro</groupId>    <artifactId>shiro-core</artifactId>    <version>1.3.2</version></dependency><dependency>    <groupId>junit</groupId>    <artifactId>junit</artifactId>    <version>${junit.version}</version>    <scope>test</scope></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.shiro</groupId>    <artifactId>shiro-core</artifactId>    <version>1.3.2</version></dependency><dependency>   <groupId>c3p0</groupId>   <artifactId>c3p0</artifactId>   <version>0.9.1.2</version></dependency><dependency>    <groupId>mysql</groupId>    <artifactId>mysql-connector-java</artifactId>    <version>5.1.39</version></dependency><dependency>     <groupId>log4j</groupId>      <artifactId>log4j</artifactId>      <version>1.2.17</version></dependency><dependency>    <groupId>org.slf4j</groupId>    <artifactId>slf4j-api</artifactId>    <version>${slf4j.version}</version></dependency><dependency>    <groupId>org.slf4j</groupId>    <artifactId>slf4j-log4j12</artifactId>    <version>${slf4j.version}</version></dependency>
  1. 编写配置文件jdbcRealm.ini
[main]# 表示实例化右边字符串表示的类,赋给左边字符串表示的变量。##JdbcRealm默认使用users表jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealmdataSource = com.mchange.v2.c3p0.ComboPooledDataSourcedataSource.driverClass = com.mysql.jdbc.DriverdataSource.user = rootdataSource.password = ****# 表示对 dataSource 这个变量设置属性值 jdbcUrl ,这个属性值是一个字符串。dataSource.jdbcUrl = jdbc:mysql://localhost:3306/ssm?useUnicode=true&characterEncoding=UTF-8# 表示对 jdbcRealm 这个变量设置属性值 dataSource , 这个 dataSource 属性是上面实例化的一个对象,所以表示这个对象要使用前缀 `$`。jdbcRealm.dataSource = $dataSourcesecurityManager.realms = $jdbcRealm
  1. 数据库建表
DROP DATABASE db_shiro;# 创建数据库 db_shiroCREATE DATABASE db_shiro DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;# 使用数据库 db_shiroUSE 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<String, String> 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]# 声明了我们自己定义的一个 RealmmyMapRealm = 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.MyStaticRealmsecurityManager.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.AllSuccessfulStrategysecurityManager.authenticator.authenticationStrategy = $allSuccessfulStrategy

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

编写配置文件

[users]hqq = 123,role1,role2lucy = 111,role1[roles]role1 = user:selectrole2 = 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.MyPermissionResolverauthorizer.permissionResolver = permissionResolversecurityManager.authorizer = $authorizer

自定义角色匹配器

步骤1:实现 RolePermissionResolver 接口

public class MyRolePermissionResolver implements RolePermissionResolver {    @Override    public Collection<Permission> 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##配置RolePermissionResolverrolePermissionResolver = heqianqian.shiro.role.MyRolePermissionResolverauthorizer.rolePermissionResolver = rolePermissionResolver#配置自定义权限解析器permissionResolver = heqianqian.shiro.permission.MyPermissionResolverauthorizer.permissionResolver = permissionResolversecurityManager.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.HashedCredentialsMatcherhashMatcher.hashAlgorithmName=md5hashMatcher.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