shiro

/ 工具和中间件 / 2 条评论 / 1159浏览

shiro教程

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。
官网:shiro.apache.org
shiro作用:验证用户、对用户执行访问控制、可以使用多个数据库、单点登录功能(SSO)

shiro认证流程

Application Code:应用程序代码,由开发人员负责开发的(action)

Subject:框架提供的接口,代表当前用户对象(当前的登陆对象)

SecurityManager:框架提供的接口,代表安全管理器对象(核心对象)

Realm:可以开发人员编写,框架也提供一些,类似于DAO,用于访问权限数据(自己书写的校验对象)

shiro案例(单机)

user实体类

/**
 * 用户类:存放账号、密码
 */
public class User {
	private String name;
	private String password;
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
}

shiro.ini配置用户、角色、权限

#定义用户
[users]
# 用户名zhangsan   密码12312   角色admin
zhangsan = 12312, admin
# 用户名lisi       密码12345   角色产品经理
lisi = 12312, productManager

#定义角色
[roles]
# 管理员什么都能做
admin = *
# 产品经理只能做与产品相关
productManager = addProduct,deleteProduct,editProduct,updateProduct,listProduct
# 订单经理只能做订单相关
orderManager = addOrder,deleteOrder,editOrder,updateOrder,listOrder

测试登录、校验角色、权限

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;

import java.util.ArrayList;
import java.util.List;

public class TestShiro {

	public static void main(String[] args) {
		//用户集合
		User zs = new User();
		zs.setName("zhangsan");
		zs.setPassword("12312");
		User ls = new User();
		ls.setName("lisi");
		ls.setPassword("12312");
		User ww = new User();
		ww.setName("wangwu");
		ww.setPassword("12312");
		List<User> users = new ArrayList<>();
		users.add(zs);
		users.add(ls);
		users.add(ww);

		//角色集合
		String roleAdmin = "admin";
		String roleProductManager = "productManager";
		List<String> roles = new ArrayList<>();
		roles.add(roleAdmin);
		roles.add(roleProductManager);

		//权限集合
		String permitAddProduct = "addProduct";
		String permitAddOrder = "addOrder";
		List<String> permits = new ArrayList<>();
		permits.add(permitAddProduct);
		permits.add(permitAddOrder);

		//登录每个用户
		for(User user : users){
			if(login(user))
				System.out.printf("%s \t登陆成功,用的密码是 %s\t %n",user.getName(),user.getPassword());
			else
				System.out.printf("%s \t登陆失败,用的密码是 %s\t %n",user.getName(),user.getPassword());

			System.out.println("------------------------ 分割线 ------------------------");
		}

		System.out.println("=====================================================================");

		//判断能够登录的用户是否拥有某个角色
		for (User user : users) {
			for (String role : roles) {
				if(login(user)) {
					if(hasRole(user, role))
						System.out.printf("%s\t 拥有角色: %s\t%n",user.getName(),role);
					else
						System.out.printf("%s\t 不拥有角色: %s\t%n",user.getName(),role);
				}
			}
		}

		System.out.println("=====================================================================");

		//判断能够登录的用户是否拥有某个权限
		for (User user : users) {
			for (String permit : permits) {
				if(login(user)) {
					if(isPermitted(user, permit))
						System.out.printf("%s\t 拥有权限: %s\t%n",user.getName(),permit);
					else
						System.out.printf("%s\t 不拥有权限: %s\t%n",user.getName(),permit);
				}
			}
		}

	}

	/**
	 * 得到Subject对象
	 * Subject在Shiro框架下就是当前用户
	 * @param user
	 * @return
	 */
	private static Subject getSubject(User user){
		//加载配置文件,并获取工厂
		Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
		//获取安全管理者实例
		SecurityManager sm = factory.getInstance();
		//将安全管理者放入全局对象
		SecurityUtils.setSecurityManager(sm);
		//全局对象通过安全管理者生成Subject对象
		Subject subject = SecurityUtils.getSubject();

		return subject;
	}

	/**
	 * 校验用户是否有指定角色
	 * @param user
	 * @param role
	 * @return
	 */
	private static boolean hasRole(User user, String role){
		Subject subject = getSubject(user);
		return subject.hasRole(role);
	}

	/**
	 * 校验用户是否有指定权限
	 * @param user
	 * @param permitted
	 * @return
	 */
	private static boolean isPermitted(User user, String permitted){
		Subject subject = getSubject(user);
		return subject.isPermitted(permitted);
	}

	/**
	 * 登录操作
	 * @param user
	 * @return
	 */
	private static boolean login(User user){
		Subject subject = getSubject(user);
		//如果已经登录过了,退出
		if(subject.isAuthenticated())
			subject.logout();

		//封装用户的数据
		UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword());
		try {
			//将用户的数据token 最终传递到Ralm中进行对比
			subject.login(token);
		}catch ( UnknownAccountException uae ) {
			System.out.println("用户名不存在");
		} catch ( IncorrectCredentialsException ice ) {
			System.out.println("密码错误");
		} catch ( LockedAccountException lae ) {
			System.out.println("用户被锁定,不能登录");
		} catch ( AuthenticationException ae ) {
			System.out.println("严重的错误");
		}
		return subject.isAuthenticated();
	}
}

shiro2案例(数据库)

RBAC

RBAC 是当下权限系统的设计基础,同时有两种解释:
一: Role-Based Access Control,基于角色的访问控制
即,你要能够删除产品,那么当前用户就必须拥有产品经理这个角色
二:Resource-Based Access Control,基于资源的访问控制
即,你要能够删除产品,那么当前用户就必须拥有删除产品这样的权限

库和表(之前shiro.ini中的数据存入到数据库中)

3张基础表:用户user,角色role,权限permission;  
以及2张中间表来建立用户与角色的多对多关系user_role,角色与权限的多对多关系role_permission。

UserDao操作数据库

import java.sql.*;
import java.util.HashSet;
import java.util.Set;

/**
 * @author langao_q
 * @create 2019-12-21 16:20
 * 数据库操作dao类
 */
public class UserDao {

    //加载驱动
    public UserDao(){
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    //获取数据库连接
    public Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/shiro?characterEncoding=UTF-8", "root",
                "123456");
    }

    /**
     * 查询用户的密码
     * @param userName
     * @return
     */
    public String getPassword(String userName) {
        String sql = "select password from user where name = ?";
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
            ps.setString(1, userName);
            ResultSet rs = ps.executeQuery();
            if (rs.next())
                return rs.getString("password");
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 查询用户对应的所有角色
     * @param userName
     * @return
     */
    public Set<String> listRoles(String userName) {
        Set<String> roles = new HashSet<>();
        String sql = "select r.name from user u "
                + "left join user_role ur on u.id = ur.uid "
                + "left join Role r on r.id = ur.rid "
                + "where u.name = ?";
        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
            ps.setString(1, userName);
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                roles.add(rs.getString(1));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return roles;
    }

    /**
     * 查询用户对应的所有权限
     * @param userName
     * @return
     */
    public Set<String> listPermissions(String userName) {
        Set<String> permissions = new HashSet<>();
        String sql =
                "select p.name from user u "+
                        "left join user_role ru on u.id = ru.uid "+
                        "left join role r on r.id = ru.rid "+
                        "left join role_permission rp on r.id = rp.rid "+
                        "left join permission p on p.id = rp.pid "+
                        "where u.name =?";

        try (Connection c = getConnection(); PreparedStatement ps = c.prepareStatement(sql);) {
            ps.setString(1, userName);
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                permissions.add(rs.getString(1));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return permissions;
    }
    
    //测试
    public static void main(String[] args) {
        System.out.println(new UserDao().listRoles("zhangsan"));
        System.out.println(new UserDao().listRoles("lisi"));
        System.out.println(new UserDao().listPermissions("zhangsan"));
        System.out.println(new UserDao().listPermissions("lisi"));
    }
}

Realm用户认证和授权(重点)

当应用程序向 Shiro提供了账号和密码之后,Shiro 就会问 Realm这个账号密码是否对,如果对的话,其所对应的用户拥有哪些角色,哪些权限。 Realm.doGetAuthorizationInfo执行时机有三个:

1、subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候;
2、@RequiresRoles("admin") :在方法上加注解的时候;
3、[@shiro.hasPermission name = "admin"][/@shiro.hasPermission]:在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。

doGetAuthenticationInfo执行时机如下

Subject subject= SecurityUtils.getSubject();
subject.login(token);
import com.how2java.dao.UserDao;
import org.apache.shiro.authc.*;
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 java.util.Set;

/**
 * @author langao_q
 * @create 2019-12-21 16:30
 * shiro获取访问权限数据
 */
public class DatabaseRealm extends AuthorizingRealm {

    /**
     * 授权:已经认证过 进行授权操作
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //能进入到这里,表示账号登录成功了
        String username = (String) principalCollection.getPrimaryPrincipal();
        //通过dao获取角色和权限
        Set<String> roles = new UserDao().listRoles(username);
        Set<String> permissions = new UserDao().listPermissions(username);

        //授权对象
        SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
        //把dao获取到的角色和权限放到用户中去
        s.setRoles(roles);
        s.setStringPermissions(permissions);
        return s;
    }

    /**
     * 认证:对用户进行认证校验操作
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取用户名和密码
        UsernamePasswordToken t = (UsernamePasswordToken) token;
        String username = token.getPrincipal().toString();
        String password = new String(t.getPassword());

        //获取数据库中的密码
        String passwordDb = new UserDao().getPassword(username);
        //如果数据库中密码为空就是用户不存在,如果不同就是密码错误,此处统一抛出AuthenticationException,而不是抛出具体错误原因,免得给破解者提供帮助信息
        if(passwordDb == null || !passwordDb.equals(password))
            throw new AuthenticationException();
        //认证信息里存放账号密码,getName是当前Realm的继承方法,通常返回当前类名:databaseRealm
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName());
        return info;
    }
}

修改shiro.ini

Shiro找到这个我们前面自己写的Realm

[main]
# 自己配置的Realm类路径
databaseRealm=com.how2java.realm.DatabaseRealm
securityManager.realms=$databaseRealm

TestShiro测试

用之前的TestShiro就可以进行测试,结果一致

手动加盐

md5加密可通过比对结果来破解,通过加盐和多加密次数得到更高的难度的密码

    /**
     * 使用shiro的自带工具类加盐加密
     */
    @Test
    public void test2(){
        String password = "123";
        String salt = new SecureRandomNumberGenerator().nextBytes().toString();
        int times = 2;
        String algorithmName = "md5";
        String encodedPassword = new SimpleHash(algorithmName,password,salt,times).toString();
        System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ",password,salt,times,encodedPassword);
    }

数据库增加一个存储salt(盐)的字段,UserDao增加createUser和getUser的方法

    /**
     * 创建用户
     * 传入用户名和密码 在此进行加密加盐
     * @param name
     * @param password
     * @return
     */
    public String createUser(String name, String password){
        String sql = "insert into user values(null,?,?,?)";

        //生成一个盐
        String salt = new SecureRandomNumberGenerator().nextBytes().toString();
        //加密 加盐。md5加密 次数为两次
        String encodedPassword = new SimpleHash("md5",password,salt,2).toString();
        try{
            Connection c = getConnection();
            PreparedStatement pr = c.prepareStatement(sql);
            pr.setString(1, name);
            pr.setString(2, encodedPassword);
            pr.setString(3, salt);
            pr.execute();
        }catch (SQLException e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 根据用户名获取User对象
     * @param userName
     * @return
     */
    public User getUser(String userName){
        User user = null;
        String sql = "select * from user where name = ?";
        try{
            Connection c = getConnection();
            PreparedStatement ps = c.prepareStatement(sql);
            ps.setString(1, userName);
            ResultSet rs = ps.executeQuery();

            if(rs.next()){
                user = new User();
                user.setId(rs.getInt("id"));
                user.setName(rs.getString("name"));
                user.setPassword(rs.getString("password"));
                user.setSalt(rs.getString("salt"));
            }
        }catch (SQLException e){
            e.printStackTrace();
        }
        return user;
    }

DatabaseRealm登录认证的时候,密码比对要改为比对加密加盐后

    /**
     * 认证:对用户进行认证校验操作
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取用户名和密码
        UsernamePasswordToken t = (UsernamePasswordToken) token;
        String username = token.getPrincipal().toString();
        String password = new String(t.getPassword());

        //获取数据库中的密码、盐
        User user = new UserDao().getUser(username);
        String passwordDb = user.getPassword();
        String salt = user.getSalt();

        //加密 加盐后的密码
        String encodedPassword = new SimpleHash("md5",password,salt,2).toString();

        //如果数据库中密码为空就是用户不存在,如果不同就是密码错误,此处统一抛出AuthenticationException,而不是抛出具体错误原因,免得给破解者提供帮助信息
        if(passwordDb == null || !passwordDb.equals(encodedPassword))
            throw new AuthenticationException();
        //认证信息里存放账号密码,getName是当前Realm的继承方法,通常返回当前类名:databaseRealm
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName());
        return info;
    }

TestShiro测试

用之前的TestShiro就可以进行测试,只有新建的admin用户能够登录成功

//先创建一个加密 加盐的用户,然后用这个用户登录
new UserDao().createUser("admin", "123");

使用Shiro做密码校验

shiro提供的HashedCredentialsMatcher帮我们做密码校验

在创建SimpleAuthenticationInfo的时候,把数据库中取出来的密文以及盐作为参数传递进去。 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, passwordDb, ByteSource.Util.bytes(salt), getName());

    /**
     * 认证:对用户进行认证校验操作
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //获取用户名和密码
        UsernamePasswordToken t = (UsernamePasswordToken) token;
        String username = token.getPrincipal().toString();
        String password = new String(t.getPassword());

        //获取数据库中的密码、盐
        User user = new UserDao().getUser(username);
        String passwordDb = user.getPassword();
        String salt = user.getSalt();

        //认证信息里存放账号密码,getName是当前Realm的继承方法,通常返回当前类名:databaseRealm
        //使用Shiro提供的 HashedCredentialsMatcher 帮我们做密码校验:
        //这样通过shiro.ini里配置的 HashedCredentialsMatcher 进行自动校验
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, passwordDb, ByteSource.Util.bytes(salt), getName());
        return info;
    }

配置shiro.ini

为DatabaseRealm指定credentialsMatcher,其中就指定了算法是md5,次数为2,
storedCredentialsHexEncoded这个表示计算之后以密文为16进制。

[main]
# 使用Shiro提供的 HashedCredentialsMatcher 帮我们做密码校验:
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
credentialsMatcher.hashAlgorithmName=md5
credentialsMatcher.hashIterations=2
credentialsMatcher.storedCredentialsHexEncoded=true

# 自己配置的Realm类路径
databaseRealm=com.how2java.realm.DatabaseRealm
# 通过shiro.ini里配置的 HashedCredentialsMatcher 进行自动校验
databaseRealm.credentialsMatcher=$credentialsMatcher
securityManager.realms=$databaseRealm

TestShiro测试

用之前的TestShiro就可以进行测试,只有新建的admin用户能够登录成功

//先创建一个加密 加盐的用户,然后用这个用户登录
new UserDao().createUser("admin", "123");