由于最近在做一个项目,刚完成到登录注册,不想和以前的项目搬同样的砖了,想完成点不那么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 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 users = new LinkedList();
users.add(object);
users.add(reader);
users.add(tom);
// 角色:BlogManager
String blogManager = "BlogManager";
// 角色:SimpleReader
String simpleReader = "SimpleReader";
List roles = new LinkedList();
roles.add(blogManager);
roles.add(simpleReader);
// 权限
String addBlog = "addBlog";
String deleteBlog = "deleteBlog";
String modifyBlog = "modifyBlog";
String readBlog = "readBlog";
String commentBlog = "commentBlog";
List permits = new LinkedList();
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 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 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 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 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 roles = ShiroDao.getRoles(username);
Set 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 users = new LinkedList();
users.add(object);
users.add(reader);
users.add(tom);
// 角色:BlogManager
String blogManager = "blogManager";
// 角色:SimpleReader
String simpleReader = "reader";
List roles = new LinkedList();
roles.add(blogManager);
roles.add(simpleReader);
// 权限
String addBlog = "addBlog";
String deleteBlog = "deleteBlog";
String modifyBlog = "modifyBlog";
String readBlog = "readBlog";
String commentBlog = "commentBlog";
List permits = new LinkedList();
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 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