【Shiro】Apache Shiro架构之身份认证(Authentication)
【Shiro】Apache Shiro架构之权限认证(Authorization)
【Shiro】Apache Shiro架构之集成web
【Shiro】Apache Shiro架构之实际运用(整合到Spring中)
之前写的博客里都是使用.ini文件来获取信息的,包括用户信息,角色信息,权限信息等。进入系统时,都是从.ini文件这读取进入的。实际中除非这个系统特别特别简单,否则一般都不是这样干的,这些信息都是需要在数据库中进行维护的,所以就需要用到自定义realm了。
写在前面:这篇博文是基于上一篇Shiro集成web基础之上修改的。
1. 数据库建表
首先在数据库中新建三个表:t_user,t_role和t_permission,分别存储用户信息,角色信息和权限信息,建表语句如下:
CREATE TABLE `t_role` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `rolename` varchar(20) DEFAULT NULL COMMENT '角色名称', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户主键', `username` varchar(20) NOT NULL COMMENT '用户名', `password` varchar(20) NOT NULL COMMENT '密码', `role_id` int(11) DEFAULT NULL COMMENT '外键关联role表', PRIMARY KEY (`id`), KEY `role_id` (`role_id`), CONSTRAINT `t_user_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 CREATE TABLE `t_permission` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `permissionname` varchar(50) NOT NULL COMMENT '权限名', `role_id` int(11) DEFAULT NULL COMMENT '外键关联role', PRIMARY KEY (`id`), KEY `role_id` (`role_id`), CONSTRAINT `t_permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
每个表中我添加了一些测试数据,如下:
2. 自定义realm
自定义realm中需要操作数据库,所有首先得先写一个dao,使用的是原始的jdbc,主要是下面的自定义realm。
public class UserDao { //根据用户名查找用户 public User getByUsername(Connection conn, String username) throws Exception { User resultUser = null; String sql = "select * from t_user where username=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ResultSet rs = ps.executeQuery(); if(rs.next()) { resultUser = new User(); resultUser.setId(rs.getInt("id")); resultUser.setUsername(rs.getString("username")); resultUser.setPassword(rs.getString("password")); } return resultUser; } //根据用户名查找改用户所拥有的角色 public Set<String> getRoles(Connection conn, String username) throws Exception { Set<String> roles = new HashSet<String>(); String sql = "select * from t_user u, t_role r where u.role_id=r.id and u.username=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ResultSet rs = ps.executeQuery(); while(rs.next()) { roles.add(rs.getString("rolename")); } return roles; } //根据用户名查找该用户角色所拥有的权限 public Set<String> getPerms(Connection conn, String username) throws Exception { Set<String> perms = new HashSet<String>(); String sql = "select * from t_user u, t_role r, t_permission p where u.role_id=r.id and p.role_id=r.id and u.username=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ResultSet rs = ps.executeQuery(); while(rs.next()) { perms.add(rs.getString("permissionname")); } return perms; } }
有了dao了,接下来就可以写自定义的realm了,自定义realm需要继承AuthorizingRealm类,因为该类封装了很多方法,它也是一步步继承自Realm类的,继承了AuthorizingRealm类后,需要重写两个方法:
doGetAuthenticationInfo()方法:用来验证当前登录的用户,获取认证信息
doGetAuthorizationInfo()方法:用来为当前登陆成功的用户授予权限和角色(已经登陆成功了)
下面来看一下具体的实现:
public class MyRealm extends AuthorizingRealm { private UserDao userDao = new UserDao(); // 为当前登陆成功的用户授予权限和角色,已经登陆成功了 @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); //获取用户名 SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); Connection conn = null; try { conn = DbUtil.getConnection(); authorizationInfo.setRoles(userDao.getRoles(conn, username)); //设置角色 authorizationInfo.setStringPermissions(userDao.getPerms(conn, username)); //设置权限 } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { DbUtil.closeConnection(conn); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } return authorizationInfo; } // 验证当前登录的用户,获取认证信息 @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); // 获取用户名 Connection conn = null; try { conn = DbUtil.getConnection(); User user = userDao.getByUsername(conn, username); // 仅仅是根据用户名查出的用户信息,不涉及到密码 if (user != null) { AuthenticationInfo authcInfo = new SimpleAuthenticationInfo( user.getUsername(), user.getPassword(), "myrealm"); return authcInfo; } else { return null; } } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { DbUtil.closeConnection(conn); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } return null; } }
从上面两个方法中可以看出:验证身份的时候是根据用户输入的用户名先从数据库中查出该用户名对应的用户,这时候并没有涉及到密码,也就是说到这一步的时候,即使用户输入的密码不对,也是可以查出来该用户的,然后将该用户的正确信息封装到authcInfo 中返回给Shiro,接下来就是Shiro的事了,它会根据这里面的真实信息与用户前台输入的用户名和密码进行校验, 这个时候也要校验密码了,如果校验通过就让用户登录,否则跳转到指定页面。同理,权限验证的时候也是先根据用户名获取与该用户名有关的角色和权限,然后封装到authorizationInfo中返回给Shiro。
3. 修改ini文件
在该配置文件中,[users]和[roles]的信息就可以删掉了,因为这些信息都是从数据库中维护的,另外还要在文件中指定我们自定义的realm的完全限定名,并且指定securityManager的realm使用我们自定义的realm,如下:
[main]
authc.loginUrl=/login
roles.unauthorizedUrl=/unauthorized.jsp
perms.unauthorizedUrl=/unauthorized.jsp
#定义自己的realm
myRealm=demo.shiro.realm.MyRealm
securityManager.realms=$myRealm
#定义请求的地址需要做什么验证
[urls]
/login=anon
/admin=authc
/student=roles[teacher]
/teacher=perms["user:create"]
dbutil.java
package demo.shiro.servlet; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; public class DbUtil { private static String driver = null; private static String url = null; private static String username = null; private static String password = null; //static语句块初始化字段信息 static{ try{ //读取db.properties文件中的数据库连接信息 InputStream in = DbUtil.class.getClassLoader().getResourceAsStream("db.properties"); Properties prop = new Properties(); prop.load(in); //获取数据库连接驱动 driver = prop.getProperty("driver"); //获取数据库连接URL地址 url = prop.getProperty("url"); //获取数据库连接用户名 username = prop.getProperty("username"); //获取数据库连接密码 password = prop.getProperty("password"); //加载数据库驱动 Class.forName(driver); }catch (Exception e) { throw new ExceptionInInitializerError(e); } } /** * @Method: getConnection * @Description: 获取数据库连接对象 * @Anthor: * * @return Connection数据库连接对象 * @throws SQLException */ public static Connection getConnection() throws SQLException{ return DriverManager.getConnection(url, username,password); } /** * @Method: release * @Description: 释放资源, * 要释放的资源包括Connection数据库连接对象,负责执行SQL命令的Statement对象,存储查询结果的ResultSet对象 * @Anthor:孤傲苍狼 * * @param conn * @param st * @param rs */ public static void release(Connection conn,Statement st,ResultSet rs){ if(rs!=null){ try{ //关闭存储查询结果的ResultSet对象 rs.close(); }catch (Exception e) { e.printStackTrace(); } rs = null; } if(st!=null){ try{ //关闭负责执行SQL命令的Statement对象 st.close(); }catch (Exception e) { e.printStackTrace(); } } if(conn!=null){ try{ //关闭Connection数据库连接对象 conn.close(); }catch (Exception e) { e.printStackTrace(); } } } public static void main(String[] args) { try { System.out.println(new DbUtil().getConnection().getClass().getName()); } catch (SQLException e) { e.printStackTrace(); } } }
user.java
public class User { private Integer id; private String username; private String password; //省略get set }
这样我们自定义的realm就搞定了,根据配置文件,当我们请求…/admin的时候会进行身份认证,所以会进入LoginServlet中,当调用currentUser.login(token);
的时候,就会进入我们自定义的realm中的doGetAuthenticationInfo方法进行身份初始化,然后交给Shiro去验证。当我们请求…./student的时候,也会先进行身份验证,就是上面的过程,然后验证通过,当我们再次请求…/student的时候,就会进入我们自定义的realm中的doGetAuthorizationInfo方法进行权限的初始化,然后交给Shiro去验证。
下一篇博文我将总结一下如何将Shiro运用到实际项目中,即如何将Shiro整合到spring中。