Loading... # Shiro入门使用 > 在阅读下文之前,请认准了两个单词,认证 (authentication) 和授权 (authorization),前者是对你的身份进行确认,后者是对你的权限进行确认。例如,前者:你是这个小区的人。后者:你只有 404 房间的权限。 ## 身份认证 > 流程概括 1. **Shiro** 把用户的数据封装成标识 `token`,`token` 一般封装着用户名,密码等信息。 2. 使用 `Subject` 门面获取到封装着用户的数据的标识 `token`。 3. `Subject` 把标识token交给 `SecurityManager`,在 `SecurityManager` 安全中心中,它将标识 `token` 委托给认证器 `Authenticator` 进行身份验证。认证器的作用一般是用来指定如何验证,它规定本次认证用到哪些 `Realm`。 4. 认证器 `Authenticator` 将传入的标识 token,与数据源 `Realm` 对比,验证其是否合法 > shiro-01authenticator 创建项目,指定 JDK 编译版本,引入 shiro 依赖。 ```xml <?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 ```ini #声明用户账号和密码 [users] #账号=密码 #用户名:jay 密码:123 jay=123 ``` 编写 HelloShiro 测试类  首先创建 `DefaultSecurityManager` 实例,其中可以传入两个参数,`Realm` 和 `Collection<Realm>`,前面我们提到 Realm 领域,指的是用户账号权限信息。我们可以看一下它的实现类。  `IniRealm` 和 `PropertiesRealm` 看起来就是去读取配置信息的,我们当然是选择 `IniRealm` 来作为参数传入`DefaultSecurityManager`。 ```java //构建默认的安全示例并传入ini数据源(用户权限信息) DefaultSecurityManager securityManager = new DefaultSecurityManager(new IniRealm("classpath: shiro.ini")); //使用 SecurityUtils 工具生效安全管理器 SecurityUtils.setSecurityManager(securityManager); ``` 接下来我们去获取 `Subject` 主题对象 ```java //使用SecurityUtils工具获得主体 Subject subject = SecurityUtils.getSubject(); ``` 根据该对象我们可以去进行登陆操作,`subject.login(AuthenticationToken token)`,因此我们需要去构造用户信息,也就是创建 `AuthenticationToken` 示例。 ```java //构建用户,指定用户名密码,进行测试 UsernamePasswordToken user = new UsernamePasswordToken("jay", "123"); ``` 登录测试 ```java //测试登录操作 subject.login(user); System.out.println("是否登录成功:" + subject.isAuthenticated()); ``` 完整的代码 ```java 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` 是一个接口,在它的类图中我们也不难猜到,一般在真实的项目中,我们不会直接实现 `Realm` 接口,而是直接继承 `AuthorizingRealm`,能够继承到认证与授权功能。它需要强制重写两个方法。 ```java 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 <?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 ```java 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`,根据用户名去查询密码,但这里为了演示方便直接访问一个字符串了。 ```java public interface SecurityService { /** * 查询用户密码 * @param loginName 用户名 * @return 密码 */ String findPasswordByLoginName(String loginName); } ``` ```java public class SecurityServiceImpl implements SecurityService { @Override public String findPasswordByLoginName(String loginName) { return "123456"; } } ``` 测试方法,注意这里 `DefaultSecurityManager` 就不用去加载 ini 文件了,直接使用 `setRealm(Realm realm)` 设置自定义的 `DefinitionRealm` 即可。 ```java 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` 工具类,使用较为简单,直接调用静态方法即可。 ```java 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()` 传入参数即可。 ```java 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; } } ``` ```java 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 项目 接下来我们在 realm 中使用上面的密码加密,我们将上面写好的 `DigestsUtil` 复制到 `shiro-02realm` 项目,使用它创建出密码为 "123456" 的 `password` 密文和 `salt` 密文 在 SecurityServiceImpl 中我们当然是返回使用工具生成后 `Map`。 ```java public class SecurityServiceImpl implements SecurityService { @Override public Map<String, String> findPasswordByLoginName(String loginName) { return DigestsUtil.encryptPassword("123456"); } } ``` 调整 `DefinitionRealm` 类,我们需要在构造方法中指定密码匹配的方式。 ```java public DefinitionRealm() { //指定密码匹配方式为sha1 HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(DigestsUtil.SHA1); //指定密码迭代次数 matcher.setHashIterations(DigestsUtil.ITERATIONS); //使用父类方法使匹配方式生效 setCredentialsMatcher(matcher); } ``` 然后修改 `doGetAuthenticationInfo()` 方法,将 `Map` 中的数据拿出来传入 `SimpleAuthenticationInfo` 中。 ```java @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()); } ``` 运行登陆方法,代码同之前无区别。 ```java @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()); } ``` ## 身份授权 与开头讲到的身份认证同等重要的是身份授权,判断的是权限。 > 流程概括 1. 首先调用 `Subject.isPermitted/hasRole` 接口,委托给 `SecurityManager`。 2. `SecurityManager` 接着会委托给内部组件 `Authorizer`。 3. `Authorizer` 再将其请求委托给我们的Realm去做;所以 `Realm` 才是主角。 4. `Realm` 将用户请求的参数封装成权限对象。再从我们重写的 `doGetAuthorizationInfo` 方法中获取从数据库中查询到的权限集合。 5. `Realm` 将用户传入的权限对象,与从数据库中查出来的权限对象,进行对比。如果用户传入的权限对象在从数据库中查出来的权限对象中,则返回 true,否则返回 false。 **进行授权操作的前提:用户必须通过了认证。** 在基于上面的代码,我们继续去学习授权认证。我们是否还记得我们在自定义的 `Realm(DefinitionRealm)` 中还有一个方法没学习,那就是 `doGetAuthorizationInfo()`。此方法的传入的参数 `PrincipalCollection principals`,是一个包装对象,它表示 "用户认证凭证信息"。本质就是 `doGetAuthenticationInfo(`) 方法第一个参数,在我们例子中就是 `username`。你可以通过这个包装对象的 `getPrimaryPrincipal()` 方法拿到此值,然后再从数据库中拿到对应的角色和资源,构建 `SimpleAuthorizationInfo`。 ```java /** * 鉴权方法 */ @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` 中添加两个新接口 ```java /** * @param loginName 登录名称 * @return * @Description 查找角色按用户登录名 */ List<String> findRoleByLoginName(String loginName); /** * @param loginName 登录名称 * @return * @Description 查找资源按用户登录名 */ List<String> findPermissionByLoginName(String loginName); ``` 实现方法如下,为了演示方便我们不去查询数据库。 ```java @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; } ``` 完整的一个测试检验 ```java 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 入门几个重要的概念和基本使用,写的不对的地方请多谅解。 <hr class="content-copyright" style="margin-top:50px" /><blockquote class="content-copyright" style="font-style:normal"><p class="content-copyright">版权属于:乐心湖's Blog</p><p class="content-copyright">本文链接:<a class="content-copyright" href="https://www.xn2001.com/archives/634.html">https://www.xn2001.com/archives/634.html</a></p><p class="content-copyright">声明:博客所有文章除特别声明外,均采用 <a href="https://creativecommons.org/licenses/by-sa/4.0/deed.zh" target="_blank" rel="nofollow noopener noopener" one-link-mark="yes">CC BY-SA 4.0 协议</a> ,转载请注明出处!</p></blockquote> 腾讯云社区邀请各位技术博主加入,福利多多噢! Last modification:March 6th, 2021 at 01:26 pm © 允许规范转载 Support 如果觉得我的文章对你有用,请随意赞赏 ×Close Appreciate the author Sweeping payments Pay by AliPay Pay by WeChat
One comment