在阅读下文之前,请认准了两个单词,认证 (authentication) 和授权 (authorization),前者是对你的身份进行确认,后者是对你的权限进行确认。例如,前者:你是这个小区的人。后者:你只有 404 房间的权限。
流程概括
token
,token
一般封装着用户名,密码等信息。Subject
门面获取到封装着用户的数据的标识 token
。Subject
把标识token交给 SecurityManager
,在 SecurityManager
安全中心中,它将标识 token
委托给认证器 Authenticator
进行身份验证。认证器的作用一般是用来指定如何验证,它规定本次认证用到哪些 Realm
。Authenticator
将传入的标识 token,与数据源 Realm
对比,验证其是否合法shiro-01authenticator
创建项目,指定 JDK 编译版本,引入 shiro 依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xn2001</groupId>
<artifactId>shiro-01authenticator</artifactId>
<version>1.0-SNAPSHOT</version>
<name>shiro-01authenticator</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
</project>
编写 resoureses/shiro.ini
#声明用户账号和密码
[users]
#账号=密码
#用户名:jay 密码:123
jay=123
编写 HelloShiro 测试类
首先创建 DefaultSecurityManager
实例,其中可以传入两个参数,Realm
和 Collection<Realm>
,前面我们提到 Realm 领域,指的是用户账号权限信息。我们可以看一下它的实现类。
IniRealm
和 PropertiesRealm
看起来就是去读取配置信息的,我们当然是选择 IniRealm
来作为参数传入DefaultSecurityManager
。
//构建默认的安全示例并传入ini数据源(用户权限信息)
DefaultSecurityManager securityManager = new DefaultSecurityManager(new IniRealm("classpath: shiro.ini"));
//使用 SecurityUtils 工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
接下来我们去获取 Subject
主题对象
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
根据该对象我们可以去进行登陆操作,subject.login(AuthenticationToken token)
,因此我们需要去构造用户信息,也就是创建 AuthenticationToken
示例。
//构建用户,指定用户名密码,进行测试
UsernamePasswordToken user = new UsernamePasswordToken("jay", "123");
登录测试
//测试登录操作
subject.login(user);
System.out.println("是否登录成功:" + subject.isAuthenticated());
完整的代码
public class HelloShiro {
@Test
public void shiroLogin() {
//构建默认的安全示例并传入ini数据源(用户权限信息)
DefaultSecurityManager securityManager = new DefaultSecurityManager(new IniRealm("classpath: shiro.ini"));
//使用 SecurityUtils 工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建用户,指定用户名密码,进行测试
UsernamePasswordToken user = new UsernamePasswordToken("jay", "123");
//测试登录操作
subject.login(user);
System.out.println("是否登录成功:" + subject.isAuthenticated());
}
}
打印结果
Realm
是一个接口,在它的类图中我们也不难猜到,一般在真实的项目中,我们不会直接实现 Realm
接口,而是直接继承 AuthorizingRealm
,能够继承到认证与授权功能。它需要强制重写两个方法。
public class DefinitionRealm extends AuthorizingRealm {
/**
* 鉴权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* 认证方法
* @param token 传递登录 token
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
return null;
}
}
shiro-02realm
创建项目,指定 JDK 编译版本,引入 shiro 依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xn2001</groupId>
<artifactId>shiro-02realm</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
</dependencies>
</project>
这一次我们去自定义授权认真,模拟访问数据库来获取密码。根据上文,我们当然是先写一个类继承于 AuthorizingRealm
,然后重写 doGetAuthenticationInfo
方法。
DefinitionRealm
public class DefinitionRealm extends AuthorizingRealm {
/**
* 鉴权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/**
* 认证方法
*
* @param token 传递登录 token
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
// 实例化数据源访问接口
SecurityService securityService = new SecurityServiceImpl();
String password = securityService.findPasswordByLoginName(username);
if ("".equals(password) || password == null) {
throw new UnknownAccountException("账户不存在");
}
// 传递账号和密码
return new SimpleAuthenticationInfo(username, password, getName());
}
}
在代码中我们写到了一个 SecurityService
,这其实是我们模拟的一个数据库访问。理论上我们应该调用 Dao
,根据用户名去查询密码,但这里为了演示方便直接访问一个字符串了。
public interface SecurityService {
/**
* 查询用户密码
* @param loginName 用户名
* @return 密码
*/
String findPasswordByLoginName(String loginName);
}
public class SecurityServiceImpl implements SecurityService {
@Override
public String findPasswordByLoginName(String loginName) {
return "123456";
}
}
测试方法,注意这里 DefaultSecurityManager
就不用去加载 ini 文件了,直接使用 setRealm(Realm realm)
设置自定义的 DefinitionRealm
即可。
public class HelloShiro {
@Test
public void shiroLogin() {
//构建默认的安全示例并传入ini数据源(用户权限信息)
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//设置Realm
securityManager.setRealm(new DefinitionRealm());
//使用 SecurityUtils 工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建用户,指定用户名密码,进行测试
UsernamePasswordToken user = new UsernamePasswordToken("jay", "123456");
//测试登录操作
subject.login(user);
System.out.println("是否登录成功:" + subject.isAuthenticated());
}
}
Shiro 提供了 base64 和 16进制字符串编码/解码的API支持,方便一些编码解码操作。
Shiro 内部的一些数据的【存储/表示】都使用了 base64 和 16进制字符串。
对应的分别是 Base64
和 Hex
工具类,使用较为简单,直接调用静态方法即可。
public class EncodesUtil {
/**
* @Description HEX-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeHex(byte[] input){
return Hex.encodeToString(input);
}
/**
* @Description HEX-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeHex(String input){
return Hex.decode(input);
}
/**
* @Description Base64-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeBase64(byte[] input){
return Base64.encodeToString(input);
}
/**
* @Description Base64-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeBase64(String input){
return Base64.decode(input);
}
}
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,所以直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如salt(即盐);这样散列的对象是“密码+salt”,这样生成的散列值相对来说更难破解。
Shiro 支持的散列算法:
Md2Hash
,Md5Hash
,Sha1Hash
,Sha256Hash
,Sha384Hash
,Sha512Hash
使用也是较为简单,只需要 new SimpleHash()
传入参数即可。
public class DigestsUtil {
// 加密形式
private static final String SHA1 = "SHA-1";
// 加密次数
private static final Integer ITERATIONS = 512;
/**
* @param input 需要散列字符串
* @param salt 盐字符串
*/
public static String sha1(String input, String salt) {
/*
* algorithmName:加密形式
* source:原始明文密码
* salt:盐值
* hashIterations:加密次数
*/
return new SimpleHash(SHA1, input, salt, ITERATIONS).toString();
}
/**
* 随机获得 salt 盐字符串
*/
public static String generateSalt() {
SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
return randomNumberGenerator.nextBytes().toHex();
}
/**
* 生成密码字符密文和 salt
*/
public static Map<String, String> encryptPassword(String passwordPlain) {
Map<String, String> map = new HashMap<>();
String salt = generateSalt();
String password = sha1(passwordPlain, salt);
map.put("salt", salt);
map.put("password", password);
return map;
}
}
public class HelloShiro {
@Test
public void DigestsUtilTest() {
Map<String, String> admin = DigestsUtil.encryptPassword("admin");
admin.forEach((k, v) -> System.out.println(k + ": " + v));
}
}
基于上面第二个 Realm 项目
接下来我们在 realm 中使用上面的密码加密,我们将上面写好的 DigestsUtil
复制到 shiro-02realm
项目,使用它创建出密码为 "123456" 的 password
密文和 salt
密文
在 SecurityServiceImpl 中我们当然是返回使用工具生成后 Map
。
public class SecurityServiceImpl implements SecurityService {
@Override
public Map<String, String> findPasswordByLoginName(String loginName) {
return DigestsUtil.encryptPassword("123456");
}
}
调整 DefinitionRealm
类,我们需要在构造方法中指定密码匹配的方式。
public DefinitionRealm() {
//指定密码匹配方式为sha1
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(DigestsUtil.SHA1);
//指定密码迭代次数
matcher.setHashIterations(DigestsUtil.ITERATIONS);
//使用父类方法使匹配方式生效
setCredentialsMatcher(matcher);
}
然后修改 doGetAuthenticationInfo()
方法,将 Map
中的数据拿出来传入 SimpleAuthenticationInfo
中。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
// 实例化数据源访问接口
SecurityService securityService = new SecurityServiceImpl();
Map<String, String> map = securityService.findPasswordByLoginName(username);
if (map.isEmpty()) {
throw new UnknownAccountException("账户不存在");
}
String password = map.get("password");
String salt = map.get("salt");
// 参数1 缓存对象
// 参数2 明文密码
// 参数3 字节salt
// 参数4 当前DefinitionRealm名称
return new SimpleAuthenticationInfo(username, password, ByteSource.Util.bytes(salt), getName());
}
运行登陆方法,代码同之前无区别。
@Test
public void shiroLogin() {
//构建默认的安全示例并传入ini数据源(用户权限信息)
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//设置Realm
securityManager.setRealm(new DefinitionRealm());
//使用 SecurityUtils 工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建用户,指定用户名密码,进行测试
UsernamePasswordToken user = new UsernamePasswordToken("jay", "123456");
//测试登录操作
subject.login(user);
System.out.println("是否登录成功:" + subject.isAuthenticated());
}
与开头讲到的身份认证同等重要的是身份授权,判断的是权限。
流程概括
Subject.isPermitted/hasRole
接口,委托给 SecurityManager
。SecurityManager
接着会委托给内部组件 Authorizer
。Authorizer
再将其请求委托给我们的Realm去做;所以 Realm
才是主角。Realm
将用户请求的参数封装成权限对象。再从我们重写的 doGetAuthorizationInfo
方法中获取从数据库中查询到的权限集合。Realm
将用户传入的权限对象,与从数据库中查出来的权限对象,进行对比。如果用户传入的权限对象在从数据库中查出来的权限对象中,则返回 true,否则返回 false。进行授权操作的前提:用户必须通过了认证。
在基于上面的代码,我们继续去学习授权认证。我们是否还记得我们在自定义的 Realm(DefinitionRealm)
中还有一个方法没学习,那就是 doGetAuthorizationInfo()
。此方法的传入的参数 PrincipalCollection principals
,是一个包装对象,它表示 "用户认证凭证信息"。本质就是 doGetAuthenticationInfo(
) 方法第一个参数,在我们例子中就是 username
。你可以通过这个包装对象的 getPrimaryPrincipal()
方法拿到此值,然后再从数据库中拿到对应的角色和资源,构建 SimpleAuthorizationInfo
。
/**
* 鉴权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//拿到用户认证凭证信息
String username = (String) principals.getPrimaryPrincipal();
//从数据库中查询对应的角色和资源
SecurityService securityService = new SecurityServiceImpl();
List<String> roles = securityService.findRoleByLoginName(username);
List<String> permissions = securityService.findPermissionByLoginName(username);
//构建资源校验
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(roles);
authorizationInfo.addStringPermissions(permissions);
return authorizationInfo;
}
在 SecurityService
中添加两个新接口
/**
* @param loginName 登录名称
* @return
* @Description 查找角色按用户登录名
*/
List<String> findRoleByLoginName(String loginName);
/**
* @param loginName 登录名称
* @return
* @Description 查找资源按用户登录名
*/
List<String> findPermissionByLoginName(String loginName);
实现方法如下,为了演示方便我们不去查询数据库。
@Override
public List<String> findRoleByLoginName(String loginName) {
List<String> list = new ArrayList<>();
list.add("admin");
list.add("dev");
return list;
}
@Override
public List<String> findPermissionByLoginName(String loginName) {
List<String> list = new ArrayList<>();
list.add("order:add");
list.add("order:list");
list.add("order:del");
return list;
}
完整的一个测试检验
public class HelloShiro {
public Subject shiroLogin(String username, String password) {
//构建默认的安全示例并传入ini数据源(用户权限信息)
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//设置Realm
securityManager.setRealm(new DefinitionRealm());
//使用 SecurityUtils 工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建用户,指定用户名密码,进行测试
UsernamePasswordToken user = new UsernamePasswordToken(username, password);
//测试登录操作
subject.login(user);
// 返回门面
return subject;
}
@Test
public void testPermissionRealm() {
Subject subject = shiroLogin("jay", "123456");
//判断用户是否已经登录
System.out.println("是否登录成功:" + subject.isAuthenticated());
//---------检查当前用户的角色信息------------
System.out.println("是否有管理员角色:" + subject.hasRole("admin"));
//---------如果当前用户有此角色,无返回值。若没有此权限,则抛 UnauthorizedException------------
try {
subject.checkRole("coder");
System.out.println("有coder角色");
} catch (Exception e) {
System.out.println("没有coder角色");
}
//---------检查当前用户的权限信息------------
System.out.println("是否有查看订单列表资源:" + subject.isPermitted("order:list"));
//---------如果当前用户有此权限,无返回值。若没有此权限,则抛 UnauthorizedException------------
try {
subject.checkPermissions("order:add", "order:del");
System.out.println("有添加和删除订单资源");
} catch (Exception e) {
System.out.println("没有有添加和删除订单资源");
}
}
}
我们可以看出来,鉴权需要实现 doGetAuthorizationInfo()
,以门面 subject
中的一系列方法进行鉴权。checkXxx()
等方法会抛出异常,isXxx()
和 hasXxx()
等方法则返回布尔值。
以上就是 Shiro 入门几个重要的概念和基本使用,写的不对的地方请多谅解。
版权属于:乐心湖's Blog
本文链接:https://cloud.tencent.com/developer/article/1792272
声明:博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!