由于最近在做一个项目,刚完成到登录注册,不想和以前的项目搬同样的砖了,想完成点不那么low的功能,像单点登录、权限控制等,于是就想起了Shiro框架。
任何一种技术总有个开始,又总是这么巧,每个开始总是个HelloWorld。官方给出的依赖:
示例代码:
public class FirstShiro { private static final transient Logger log = LoggerFactory.getLogger(FirstShiro.class); public static void main(String[] args) { // TODO Auto-generated method stub log.info("My First Apache Shiro Application"); System.exit(0); } } 复制代码
运行结果:
[main] INFO com.shiro.first.FirstShiro - My First Apache Shiro Application 复制代码
在没有Shiro的时候,我们在做项目中的登录、权限之类的功能有五花八门的实现方式,不同系统的做法不统一。但是有Shiro之后,大家就可以一致化地做权限系统,优点就是各自的代码不再晦涩难懂,有一套统一的标准。另外Shiro框架也比较成熟,能很好地满足需求。这就是我对Shiro的总结。
Shiro不仅不依赖任何容器,可以在EE环境下运行,也可以在SE环境下运行,在快速入门中,我在SE环境下体验了Shiro的登录验证、角色验证、权限验证功能。
[users] #用户 密码 角色 #博客管理员 Object=123456,BlogManager #读者 Reader=654321,SimpleReader #定义各种角色 [roles] #博客管理员权限 BlogManager=addBlog,deleteBlog,modifyBlog,readBlog #普通读者权限 SimpleReader=readBlog,commentBlog 复制代码
/** * @author Object * 用户实体类 */ 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; } } 复制代码
/** * 获取当前用户(Subject) * * @param user * @return */ public static Subject getSubject() { // 加载配置文件,获取SecurityManager工厂 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); // 从工厂中获取SecurityManager对象 SecurityManager securityManager = factory.getInstance(); // 通过SecurityUtil将SecurityManager对象放入全局对象 SecurityUtils.setSecurityManager(securityManager); // 全局对象通过SecurityManager生成Subject Subject subject = SecurityUtils.getSubject(); return subject; } 复制代码登录:
/** * 用户登录方法 * * @param user * @return */ public static boolean login(User user) { Subject subject = getSubject(); // 如果用户已经登录 则退出 if (subject.isAuthenticated()) { subject.logout(); } // 封装用户数据 UsernamePasswordToken token = new UsernamePasswordToken(user.getName(), user.getPassword()); // 验证用户数据 try { subject.login(token); } catch (AuthenticationException e) { // 登录失败 // e.printStackTrace();为了看结果,暂时不让它打印 return false; } return subject.isAuthenticated(); } 复制代码判断用户是否为某个角色:
/** * 判断用户是否拥有某个角色 * * @param user * @param role * @return */ public static boolean hasRole(User user, String role) { Subject subject = getSubject(); return subject.hasRole(role); } 复制代码判断用户是否拥有某项权限
/** * 判断用户是否拥有某种权限 * * @param user * @param permit * @return */ public static boolean isPermit(User user, String permit) { Subject subject = getSubject(); return subject.isPermitted(permit); } 复制代码有了这四个方法,我们就可以开始写测试类了。我会创建两个 在配置文件中的用户 —— Object and Reader 和一个 不在配置文件中的用户 —— Tom
public static void main(String[] args) { // 用户Object User object = new User(); object.setName("Object"); object.setPassword("123456"); // 用户Reader User reader = new User(); reader.setName("Reader"); // 错误的密码 reader.setPassword("654321"); // 不存在的用户 User tom = new User(); tom.setName("Tom"); tom.setPassword("123456"); List<User> users = new LinkedList<User>(); users.add(object); users.add(reader); users.add(tom); // 角色:BlogManager String blogManager = "BlogManager"; // 角色:SimpleReader String simpleReader = "SimpleReader"; List<String> roles = new LinkedList<String>(); roles.add(blogManager); roles.add(simpleReader); // 权限 String addBlog = "addBlog"; String deleteBlog = "deleteBlog"; String modifyBlog = "modifyBlog"; String readBlog = "readBlog"; String commentBlog = "commentBlog"; List<String> permits = new LinkedList<String>(); permits.add(addBlog); permits.add(deleteBlog); permits.add(modifyBlog); permits.add(readBlog); permits.add(commentBlog); /**************************** 开始验证 ****************************/ System.out.println("=========================验证用户是否登录成功========================="); // 验证用户是否登录成功 for (User u : users) { if (login(u)) { System.out.println("用户:" + u.getName() + " 登录成功 " + "密码为:" + u.getPassword()); } else { System.out.println("用户:" + u.getName() + " 登录失败 " + "密码为:" + u.getPassword()); } } System.out.println("=========================验证用户角色信息========================="); // 验证用户角色 for (User u : users) { for (String role : roles) { if (login(u)) { if (hasRole(u, role)) { System.out.println("用户:" + u.getName() + " 的角色是" + role); } } } } System.out.println("=========================验证用户权限信息========================="); for(User u:users) { System.out.println("========================="+u.getName()+"权限========================="); for(String permit:permits) { if(login(u)) { if(isPermit(u, permit)) { System.out.println("用户:"+u.getName() +" 有 "+permit+" 的权限 "); } } } } } 复制代码运行结果如下(红字是由于缺少部分jar,暂不解决):
到这里为止,已经完成了Shiro的入门。但是在实际项目中,我们不可能用配置文件配置用户权限,所以还是得结合数据库进行开发。
要结合数据库进行开发,得先理解一个概念 —— RABC 。
RBAC 是当下权限系统的设计基础,同时有两种解释: 一: Role-Based Access Control, 基于角色的访问控制 。 即:你要能够增删改查博客,那么当前用户就必须拥有博主这个角色。 二:Resource-Based Access Control, 基于资源的访问控制 。 即,你要能够读博客、评论博客,那么当前用户就必须拥有读者这样的权限。
所以,基于这个概念,我们的数据库将有:用户表、角色表、权限表、 用户——角色 关系表、 权限——角色 关系表,其中 用户 与 角色 关系为多对多,即一个用户可以对应多个角色,一个角色也可以由多个用户扮演, 权限 与 角色 关系也为多对多,即一个角色可以有多个权限,一个权限也可以赋予多个角色。
我使用的是MySQL,创建语句如下:
CREATE DATABASE shiro; USE shiro; CREATE TABLE user( id bigint primary key auto_increment, name varchar(16), password varchar(32) )charset=utf8 ENGINE=InnoDB; create table role ( id bigint primary key auto_increment, name varchar(32) ) charset=utf8 ENGINE=InnoDB; create table permission ( id bigint primary key auto_increment, name varchar(32) ) charset=utf8 ENGINE=InnoDB; create table user_role ( uid bigint, rid bigint, constraint pk_users_roles primary key(uid, rid) ) charset=utf8 ENGINE=InnoDB; create table role_permission ( rid bigint, pid bigint, constraint pk_roles_permissions primary key(rid, pid) ) charset=utf8 ENGINE=InnoDB; 复制代码
往数据库中插入数据:
INSERT INTO `user` VALUES (1,'Object','123456'); INSERT INTO `user` VALUES (2,'Reader','654321'); INSERT INTO `user_role` VALUES (1,1); INSERT INTO `user_role` VALUES (2,2); INSERT INTO `role` VALUES (1,'blogManager'); INSERT INTO `role` VALUES (2,'reader'); INSERT INTO `permission` VALUES (1,'addBlog'); INSERT INTO `permission` VALUES (2,'deleteBlog'); INSERT INTO `permission` VALUES (3,'modifyBlog'); INSERT INTO `permission` VALUES (4,'readBlog'); INSERT INTO `permission` VALUES (5,'commentBlog'); INSERT INTO `role_permission` VALUES (1,1); INSERT INTO `role_permission` VALUES (1,2); INSERT INTO `role_permission` VALUES (1,3); INSERT INTO `role_permission` VALUES (1,4); INSERT INTO `role_permission` VALUES (2,4); INSERT INTO `role_permission` VALUES (2,5); 复制代码
public class User { private int id; 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; } public int getId() { return id; } public void setId(int id) { this.id = id; } } 复制代码
[main] databaseRealm=com.shirotest.DatabaseRealm securityManager.realms=$databaseRealm 复制代码
public class ShiroDao { private static Connection connection = null; private static PreparedStatement preparedStatement = null; static { try { Class.forName("com.mysql.jdbc.Driver"); connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC", "root", "971103"); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } /** * 通过用户名获取密码 * * @param username * @return */ public static String getPassword(String username) { String sql = "select password from user where name = ?"; ResultSet rs = null; try { preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, username); rs = preparedStatement.executeQuery(); if (rs.next()) return rs.getString("password"); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } public static Set<String> getRoles(String username) { String sql = "select role.name " + "from role,user_role,user " + "where user.id=user_role.uid " + "and user_role.rid=role.id " + "and user.name = ?"; ResultSet rs = null; Set<String> set = new HashSet<>(); try { preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, username); rs = preparedStatement.executeQuery(); while(rs.next()) { set.add(rs.getString("name")); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return set; } public static Set<String> getPermits(String username) { String sql = "select permission.name " + "from" + " permission,role_permission,role ,user_role,user " + "where " + "permission.id = role_permission.pid " + "and role_permission.rid = role.id " + "and role.id = user_role.rid " + "and user_role.uid = user.id " + "and user.name = ?"; ResultSet rs = null; Set<String> set = new HashSet<>(); try { preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, username); rs = preparedStatement.executeQuery(); while (rs.next()) { set.add(rs.getString("name")); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return set; } public static void main(String[] args) { System.out.println("Object的角色:" + new ShiroDao().getRoles("Object")); System.out.println("Reader的角色:" + new ShiroDao().getRoles("Reader")); System.out.println("Object的权限:"+new ShiroDao().getPermits("Object")); System.out.println("Reader的权限:"+new ShiroDao().getPermits("Reader")); } } 复制代码
运行结果:
public class DatabaseRealm extends AuthorizingRealm{ /** *授权的方法 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) { //只有认证成功了,Shiro才会调用这个方法进行授权 //1.获取用户 String username = (String) principal.getPrimaryPrincipal(); //2.获取角色和权限列表 Set<String> roles = ShiroDao.getRoles(username); Set<String> permissions = ShiroDao.getPermits(username); //3.授权 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setRoles(roles); simpleAuthorizationInfo.setStringPermissions(permissions); return simpleAuthorizationInfo; } /** *验证用户名密码是否正确的方法 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //1.获取用户名密码 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; //获取用户名 String username = usernamePasswordToken.getUsername(); //获取密码 String password = usernamePasswordToken.getPassword().toString(); //获取数据库中的密码 String passwordInDatabase = ShiroDao.getPassword(username); //为空则表示没有当前用户,密码不匹配表示密码错误 if(null == passwordInDatabase||!password.equals(passwordInDatabase)) { throw new AuthenticationException(); } //认证信息:放用户名密码 getName()是父类的方法,返回当前类名 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,getName()); return simpleAuthenticationInfo; } } 复制代码
public class TestShiro { public static void main(String[] args) { // 用户Object User object = new User(); object.setName("Object"); object.setPassword("123456"); // 用户Reader User reader = new User(); reader.setName("Reader"); // 错误的密码 reader.setPassword("654321"); // 不存在的用户 User tom = new User(); tom.setName("Tom"); tom.setPassword("123456"); List<User> users = new LinkedList<User>(); users.add(object); users.add(reader); users.add(tom); // 角色:BlogManager String blogManager = "blogManager"; // 角色:SimpleReader String simpleReader = "reader"; List<String> roles = new LinkedList<String>(); roles.add(blogManager); roles.add(simpleReader); // 权限 String addBlog = "addBlog"; String deleteBlog = "deleteBlog"; String modifyBlog = "modifyBlog"; String readBlog = "readBlog"; String commentBlog = "commentBlog"; List<String> permits = new LinkedList<String>(); permits.add(addBlog); permits.add(deleteBlog); permits.add(modifyBlog); permits.add(readBlog); permits.add(commentBlog); /**************************** 开始验证 ****************************/ System.out.println("=========================验证用户是否登录成功========================="); // 验证用户是否登录成功 for (User u : users) { if (login(u)) { System.out.println("用户:" + u.getName() + " 登录成功 " + "密码为:" + u.getPassword()); } else { System.out.println("用户:" + u.getName() + " 登录失败 " + "密码为:" + u.getPassword()); } } System.out.println("=========================验证用户角色信息========================="); // 验证用户角色 for (User u : users) { for (String role : roles) { if (login(u)) { if (hasRole(u, role)) { System.out.println("用户:" + u.getName() + " 的角色是" + role); } } } } System.out.println("=========================验证用户权限信息========================="); for(User u:users) { System.out.println("========================="+u.getName()+"权限========================="); for(String permit:permits) { if(login(u)) { if(isPermitted(u, permit)) { System.out.println("用户:"+u.getName() +" 有 "+permit+" 的权限 "); } } } } } public static Subject getSubject() { Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //获取安全管理者实例 SecurityManager sm = factory.getInstance(); //将安全管理者放入全局对象 SecurityUtils.setSecurityManager(sm); //全局对象通过安全管理者生成Subject对象 Subject subject = SecurityUtils.getSubject(); return subject; } public static boolean login(User user) { Subject subject = getSubject(); if(subject.isAuthenticated()) { //如果登录了,就退出登录 subject.logout(); } //封装用户数据 AuthenticationToken token = new UsernamePasswordToken(user.getName(),user.getPassword()); try { subject.login(token); }catch(AuthenticationException e) { return false; } return subject.isAuthenticated(); } private static boolean hasRole(User user, String role) { Subject subject = getSubject(); return subject.hasRole(role); } private static boolean isPermitted(User user, String permit) { Subject subject = getSubject(); return subject.isPermitted(permit); } } 复制代码
最终测试结果:
我们在没有Shiro的时候,也会使用各种加密算法来对用户的密码进行加密,Shiro框架也提供了自己的一套加密服务,这里就说说MD5+盐。
在不加盐的MD5中,虽然密码也是使用非对称算法加密,同样也不能回转为明文,但是别人可以使用穷举法列出最常用的密码,例如12345 它加密后永远都是同一个密文,一些别有用心的人就可以通过这种常见密文得知你的密码是12345。但是加盐就不一样,他是在你的密码原文的基础上添加上一个随机数,这个随机数也会随之保存在数据库中,但是黑客拿到你的密码之后他并不知道哪个随机数是多少,所以就很难再破译密码。
操作一番。
首先要在数据库中加一个"盐"字段 ALTER TABLE user add column salt varchar(100)
同时在User实体类中加一个salt
private String salt; public String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } 复制代码
然后在ShiroDao中加一个注册用户的方法。
public static boolean registerUser(String username,String password) { /***********************************Shiro加密***********************************/ //获取盐值 String salt = new SecureRandomNumberGenerator().nextBytes().toString(); //加密次数 int times = 3; //加密方式 String type = "md5"; //加密后的最终密码 String lastPassword = new SimpleHash(type, password, salt, times).toString(); /***********************************加密结束***********************************/ String sql = "INSERT INTO user(name,password,salt)VALUES(?,?,?)"; try { PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, username); preparedStatement.setString(2, lastPassword); preparedStatement.setString(3, salt); if(preparedStatement.execute()) { return true; } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); return false; } return false; } 复制代码
同时加一个获取用户的方法:
public static User getUser(String username) { String sql = "select * from user where name = ?"; User user = new User(); try { PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, username); ResultSet resultSet = preparedStatement.executeQuery(); while(resultSet.next()) { user.setName(resultSet.getString("name")); user.setPassword(resultSet.getString("password")); user.setSalt(resultSet.getString("salt")); } } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } return user; } 复制代码
修改之前的DatabaseRealm类中的 验证用户 方法,加一个将用户输入的密码加密后与数据库中密码进行比对的逻辑。具体逻辑如下:
//1.获取用户名密码 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; //获取用户名 String username = usernamePasswordToken.getUsername(); //获取密码 String password = new String(usernamePasswordToken.getPassword()); System.out.println("明文密码:"+password); //获取数据库中的用户 User user = ShiroDao.getUser(usernamePasswordToken.getUsername()); //String passwordInDatabase = ShiroDao.getPassword(username); //将用户输入的密码做一个加密后与数据库中的进行比对 String passwordMd5 = new SimpleHash("md5", password, user.getSalt(), 3).toString(); System.out.println("salt:"+user.getSalt()); System.out.println("密文密码:"+passwordMd5); System.out.println("正在验证中......"); //为空则表示没有当前用户,密码不匹配表示密码错误 if(null == user.getPassword()||!passwordMd5.equals(user.getPassword())) { throw new AuthenticationException(); } //认证信息:放用户名密码 getName9()是父类的方法,返回当前类名 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,password,getName()); return simpleAuthenticationInfo; 复制代码
main测试:
ShiroDao.registerUser("Object2", "321321"); User object2 = new User(); object2.setName("Object2"); object2.setPassword("321321"); if (login(object2)) { System.out.println("登录成功"); } else { System.out.println("登录失败"); } 复制代码
最后结果:
数据库结果:
刚才我们是在doGetAuthenticationInfo方法中自己写了验证逻辑,再来捋一遍:
1.获取用户输入的密码 2.获取数据库中该用户的盐 3.将用户输入的密码进行加盐加密 4.将加密后的密码和数据库中的密码进行比对 复制代码
大概是要经历这么多步骤吧。其实Shiro提供了一个HashedCredentialsMatcher ,可以自动帮我们做这些工作。
步骤: 1.修改配置文件
[main] credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher credentialsMatcher.hashAlgorithmName=md5 #加密方式 credentialsMatcher.hashIterations=3 #刚才我们指定的加密次数 credentialsMatcher.storedCredentialsHexEncoded=true databaseRealm=com.shirotest.DatabaseRealm securityManager.realms=$databaseRealm 复制代码
2.修改doGetAuthenticationInfo方法
//1.获取用户名密码 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; //获取用户名 String username = usernamePasswordToken.getUsername(); //获取密码 String password = new String(usernamePasswordToken.getPassword()); System.out.println("明文密码:"+password); //获取数据库中的用户 User user = ShiroDao.getUser(usernamePasswordToken.getUsername()); //String passwordInDatabase = ShiroDao.getPassword(username); //将用户输入的密码做一个加密后与数据库中的进行比对 System.out.println("数据库中密码:"+user.getPassword()); String passwordMd5 = new SimpleHash("md5", password, user.getSalt(), 3).toString(); System.out.println("salt:"+user.getSalt()); System.out.println("密文密码:"+passwordMd5); System.out.println("正在验证中......"); /* * //为空则表示没有当前用户,密码不匹配表示密码错误 if(null == * user.getPassword()||!passwordMd5.equals(user.getPassword())) { throw new * AuthenticationException(); } */ //认证信息:放用户名密码 getName9()是父类的方法,返回当前类名 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,user.getPassword(),ByteSource.Util.bytes(user.getSalt()),getName()); return simpleAuthenticationInfo; 复制代码
主要是修改了验证信息,将 数据库中的密码和盐
传入,让它自行判断,我们就无需再写判断逻辑了。 SimpleAuthenticationInfo(username,user.getPassword(),ByteSource.Util.bytes(user.getSalt()),getName());
运行结果:
到这里为止,Shiro关于SE的部分应该就告一段落了,之后要开始学习关于集成Web和集成框架了,我觉得对于Shiro的架构及原理,得单独浏览一遍,因为到此为止我也只知道Shiro是怎么使用的,但是其中Realm类中的那两个方法,何时调用,为什么会调用,还有SimpleAuthenticationInfo返回后是怎么判断登录成功或者失败的,可以说是很模糊,学完集成框架后我应该会选择再看看其中的原理。
欢迎大家访问我的个人博客:Object's Blog(正在备案中)