说明
代码及部分相关资料根据慕课网 Mark 老师的视频 https://www.imooc.com/learn/977 进行整理.
其他资料:
shiro 官网 http://shiro.apache.org/
基础概念
Authenticate/Authentication(认证)
认证是指检查用户身份合法性, 通过校验用户输入的密码是否正确, 判断用户是否为本人.
有几个概念需要理解:
Principals (主体标识)
任何可以唯一地确定一个用户的属性都可以充当 principal, 例如: 邮箱, 手机号, 用户 ID 等, 这些都是与用户一一对应的, 可以唯一地确定一个用户.
credentials (主体凭证)
credentials 是能确认用户身份的东西, 可以是证书(Certificate), 也可以是密码(password).
token(令牌)
这里的 token 和 API 里的 token 有一点儿差别, 这里 token 是 principal 和 credential 的结合体或者说容器. 这里先讲一部分, 剩下的放到 "Subject" 讲解.
Authorize/Authorization(授权)
shiro 中的 "授权", 更贴切说法是 "鉴权", 即判定用户是否拥有某些权限, 至于拥有该权限在业务上有何意义, 则是由业务本身来决定.
关于 "授权",shiro 引入了两种概念:
Role (角色)
角色用来区分用户的类别. 角色与用户间是多对多的关系, 一个用户可以拥有多个角色, 如 Bob 可以同时是 admin(管理员)和 user(普通用户).
Permission (权限)
权限是对角色的具体的描述, 用于说明角色在业务上的特殊性. 如 admin(管理员)可以拥有 user:delete(删除用户),user:modify(修改用户信息)等的权限. 同样的, 角色与权限是多对多的数量关系.
shiro 权限可以分级, 使用 ":" 分割, 如 delete,user:delete,user:info:delete. 可以使用 "*" 作通配符, 例如可以给 admin 赋予操作用户的所有权限, 可以配置为 "user:*", 这样在授权时, isPermitted("user:123"),isPermitted("user:123:abc")都是返回 true; 如果配置为 "*:*:delete", 想要返回 true, 则需要类似这样的权限: isPermitted("123:abc:delete"),isPermitted("hello:321:delete").
Subject(主体)
Subject 对象用于应用程序与 shiro 的相关组件进行交互, 可以把它看作应用程序中 "用户" 的代理, 也可以将其视为 shiro 中的 "用户". 譬如在一个应用中, User 对象作为业务上以及程序中的 "用户", 在实现 shiro 的认证和授权时, 并不直接使用 User 对象与 shiro 组件进行交互, 而是把 User 对象的信息 (用户名和密码) 交给 Subject,Subject 调用自己的方法, 向 shiro 组件发起身份认证或授权.
如下是 Subject 接口提供的方法, 包括登录 (login), 退出(logout), 认证(isAuthenticated), 授权(checkPermission) 等:
接着上面继续讲 Token, 在图片中可以看到, login 方法需要传入一个 AuthenticationToken 类型参数, 这是一个接口, 点进去看是酱紫的:
这个接口有两个方法, 分别用来返回 principal 和 credential. 由此可见 Token 就是 principal 和 credential 的容器, 用于 Subject 提交 "登录" 请求时传递信息.
需要提醒, Subject 的这些方法调用 SecurityManager 进行实际上的认证和授权过程. Subject 只是业务程序与 SecurityManager 通信的门面.
SecurityManager
顾名思义, SecurityManager 是用来 manage(管理)的, 管理 shiro 认证授权的流程, 管理 shiro 组件, 管理 shiro 的一些数据, 管理 Session 等等.
如下是 SecurityManager 接口的继承关系:
SecurityManager 继承了 Authorizer(授权),Authenticator(认证), 和 SessionManager(Session 管理). 需要注意, 此处 Session 是指 Subject 与 SecurityManager 通信的 Session, 不能狭隘地理解为 WebSession.SecurityManager 继承了了这几个接口后, 又另外提供了 login,logout 和 createSubject 方法.
Realm(域)
与 Subject 和 SecurityManager 一样, Realm 是 shiro 中的三大核心组件之一. Realm 相当于 DAO, 用于获取与用户安全相关的数据(用户密码, 角色, 权限等). 当 Subject 发起认证和授权时, 实际上是调用其对应的 SecurityManager 的认证和授权的方法, 而 SecurityManager 则又是调用 Authenticator 和 Authorizer 的方法, 这两个类, 最后是通过 Realm 来获取主体的认证和授权信息.
shiro 的认证和授权过程如下所示:
使用 shiro 的基本流程
shiro 的使用其实是比较简单的, 只要熟记这几个步骤, 然后在代码中实现即可.
1. 创建 Realm
Realm 是一个接口, 其实现类有 SimpleAccountRealm, IniRealm, JdbcRealm 等, 实际应用中一般需要自定义实现 Realm, 自定义的 Realm 通常继承自抽象类 AuthorizingRealm, 这是一个比较完善的 Realm, 提供了认证和授权的方法.
2. 创建 SecurityManager 并配置环境
配置 SecurityManager 环境实际上是配置 Realm,CacheManager,SessionManager 等组件, 最基本的要配置 Realm, 因为安全数据是通过 Realm 来获取. 用 SecurityManager 的 setRealm(xxRealm)方法即可给 SecurityManager 设置 Realm. 可以为 SecurityManager 设置多个 Realm.
3. 创建 Subject
可以使用 SecurityUtils 创建 Subject.SecurityUtils 是一个抽象工具类, 其提供了静态方法 getSubject(), 用来创建一个与线程绑定的 Subject. 创建出来的 Subject 用 ThreadContext 类来存储, 该类也是一个抽象类, 它包含一个 Map<Object, Object > 类型的 ThreadLocal 静态变量, 该变量存储该线程对应的 SecurityManager 对象和 Subject 对象. 在 SecurityUtils 调用 getSubject 方法时, 实际上是调用 SecurityManager 的 CreateSubject()方法, 既然如此, 为什么还要通过 SecurityUtils 创建 Subject? 因为 SecurityUtils 不仅仅创建了 Subject 还将其与当前线程绑定, 而且, 使用 SecurityManager 的 CreateSubject()方法还要构建一个 SubjectContext 类型的参数.
4.Subject 提交认证和授权
Subject 的 login(Token token)方法可以提交 "登录"(或者说认证),token 就是待验证的用户信息 (用户名和密码等). 登录(认证) 成功后, 使用 Subject 的 ckeckRole(),checkPermission 等方法判断主体是否拥有某些角色, 权限, 以达到授权的目的. 再次提醒, Subject 不实现实际上的认证和授权过程, 而是交给 SecurityManager 处理.
shiro 认证授权示例
Realm 用的是 SimpleAccountRealm,SimpleAccountRealm 直接把用户认证数据存到实例中, SecurityManager 使用 DefaultSecurityManager, 使用 SecurityUtils 创建 Subject, Token 用 UsernamePasswordToken. 用 Junit 进行测试. maven 依赖如下:
- <!-- 单元测试 -->
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <version>4.13-beta-3</version>
- </dependency>
- <!--shiro 核心包 -->
- <dependency>
- <groupId>org.apache.shiro</groupId>
- <artifactId>shiro-core</artifactId>
- <version>1.4.0</version>
- </dependency>
shiro 认证示例
- AuthenticationTest.java:
- package com.lifeofcoding.shiro;
- import org.apache.shiro.SecurityUtils;
- import org.apache.shiro.authc.UsernamePasswordToken;
- import org.apache.shiro.mgt.DefaultSecurityManager;
- import org.apache.shiro.realm.SimpleAccountRealm;
- import org.apache.shiro.subject.Subject;
- import org.junit.Test;
- public class AuthenticationTest {
- @Test
- public void testAuthentication(){
- //1. 创建 Realm 并添加数据
- SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
- simpleAccountRealm.addAccount("java","123");
- //2. 创建 SecurityManager 并配置环境
- DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
- defaultSecurityManager.setRealm(simpleAccountRealm);
- //3. 创建 subject
- SecurityUtils.setSecurityManager(defaultSecurityManager);
- Subject subject = SecurityUtils.getSubject();
- //4.Subject 通过 Token 提交认证
- UsernamePasswordToken token = new UsernamePasswordToken("java","123");
- subject.login(token);
- // 验证认证情况
- System.out.println("isAuthenticated:"+ subject.isAuthenticated());
- // 退出登录 subject.logout();
- }
- }
shiro 授权示例
SimpleAccountRealm 添加用户角色和权限的方法比较简单, 可以自己琢磨. 此处的 Realm 改用 IniRealm,iniRealm 需要编写 INI 文件存储用户的信息, INI 文件放在 resource 文件夹下. 代码如下:
AuthorizationTest.java
- package com.lifeofcoding.shiro;
- import org.apache.shiro.SecurityUtils;
- import org.apache.shiro.authc.UsernamePasswordToken;
- import org.apache.shiro.mgt.DefaultSecurityManager;
- import org.apache.shiro.realm.text.IniRealm;
- import org.apache.shiro.subject.Subject;
- import org.junit.Test;
- import java.util.ArrayList;
- public class AuthorizationTest {
- @Test
- public void testAuthorization() {
- //1. 创建 Realm 并添加数据
- IniRealm iniRealm = new IniRealm("classpath:UserData.ini");
- //2. 创建 SecurityManager 并配置环境
- DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
- defaultSecurityManager.setRealm(iniRealm);
- //3. 创建 Subject
- SecurityUtils.setSecurityManager(defaultSecurityManager);
- Subject subject = SecurityUtils.getSubject();
- //4. 主体提交认证
- UsernamePasswordToken token = new UsernamePasswordToken("java", "123");
- subject.login(token);
- /* 以下为授权的几种方法 */
- //1. 直接判断是否有某权限, isPermitted 方法返回 boolean, 不抛异常
- System.out.println("user:login isPermitted:" + subject.isPermitted("user:login"));
- //2. 通过角色进行授权, 方法有返回值, 不抛异常
- subject.hasRole("user");// 判断主体是否有某角色
- subject.hasRoles(new ArrayList<String>() {// 返回 boolean 数组, 数组顺序与参数 Roles 顺序一致, 接受 List<String > 参数
- {
- add("admin");
- add("user");
- }
- });
- subject.hasAllRoles(new ArrayList<String>() {// 返回一个 boolean,Subject 包含所有 Roles 时才返回 true, 接受 Collection<String > 参数
- {
- add("admin");
- add("user");
- }
- });
- //3. 通过角色授权, 与上面大体相同, 不过这里的方法无返回值, 授权失败会抛出异常, 需做好异常处理
- subject.checkRole("user");
- subject.checkRoles("user", "admin");// 变参
- //4. 通过权限授权, 无返回值, 授权失败抛出异常
- subject.checkPermission("user:login");
- //INI 文件配置了 test 角色拥有 "prefix:*" 权限, 也就是所有以 "prefix" 开头的权限
- subject.checkPermission("prefix:123:456:......");
- //INI 文件配置了 test 角色拥有 "*:*:suffix" 权限, 意味着其拥有所有以 "suffix" 结尾的, 一共有三级的权限
- subject.checkPermission("1:2:suffix");
- subject.checkPermission("abc:123:suffix");
- subject.checkPermissions("user:login", "admin:login");// 变参
- //subject.checkPermission(Permission permission); 需要 Permission 接口的实现类对象作参数
- //subject.checkPermissions(Collection<Permission> permissions);
- }
- }
- user.INI:
- [users]
- java = 123,user,admin,test
- [roles]
- user = user:login,user:modify
- admin = user:delete,user:modify,admin:login
- test = prefix:*,*:*:suffix
INI 文件的 Demo
下面介绍其他的 Realm
JdbcRealm
JdbcRealm 包含默认的数据库查询语句, 直接使用即可, 但要注意创建的表结构要跟查询语句相对应. 当然也可以自己去自定义查询语句和数据库.
mavern 依赖:
- <!-- 数据库相关 -->
- <dependency>
- <groupId>MySQL</groupId>
- <artifactId>MySQL-connector-java</artifactId>
- <version>8.0.15</version>
- </dependency>
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid</artifactId>
- <version>1.1.6</version>
- </dependency>
默认查询语句
默认的查询语句
默认的'users'表结构
默认的'user_roles'表结构
默认的'roles_permissions'表结构
数据库 sql 语句
- JdbcRealmTest.java:
- package com.lifeofcoding.shiro;
- import com.alibaba.druid.pool.DruidDataSource;
- import org.apache.shiro.SecurityUtils;
- import org.apache.shiro.authc.UsernamePasswordToken;
- import org.apache.shiro.mgt.DefaultSecurityManager;
- import org.apache.shiro.realm.jdbc.JdbcRealm;
- import org.apache.shiro.subject.Subject;
- import org.junit.Test;
- public class JdbcRealmTest {
- DruidDataSource druidDataSource = new DruidDataSource();
- {
- druidDataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
- druidDataSource.setUsername("root");
- druidDataSource.setPassword("0113");
- }
- @Test
- public void testJdbcRealm(){
- //1. 创建 Realm 并添加数据
- JdbcRealm jdbcRealm = new JdbcRealm();
- jdbcRealm.setDataSource(druidDataSource);// 配置数据源
- jdbcRealm.setPermissionsLookupEnabled(true);// 设置允许查询权限, 否则 checkPermission 抛异常, 默认值为 false
- //2. 创建 SecurityManager 并配置环境
- DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
- defaultSecurityManager.setRealm(jdbcRealm);
- //3. 创建 subject
- SecurityUtils.setSecurityManager(defaultSecurityManager);
- Subject subject = SecurityUtils.getSubject();
- //4.Subject 通过 Token 提交认证
- UsernamePasswordToken token = new UsernamePasswordToken("java","123");
- subject.login(token);// 退出登录 subject.logout();
- // 验证认证与授权情况
- System.out.println("isAuthenticated:"+ subject.isAuthenticated());
- subject.hasRole("admin");
- subject.checkPermission("user:delete");
- }
- }
自定义查询语句
自定义的查询语句
自定义的'test_users'表结构
自定义的'test_user_roles'表结构
自定义的'test_roles_permissions'表结构
数据库 sql 语句
- MyJdbcRealmTest.java:
- import com.alibaba.druid.pool.DruidDataSource;
- import org.apache.shiro.SecurityUtils;
- import org.apache.shiro.authc.UsernamePasswordToken;
- import org.apache.shiro.mgt.DefaultSecurityManager;
- import org.apache.shiro.realm.jdbc.JdbcRealm;
- import org.apache.shiro.subject.Subject;
- import org.junit.Test;
- public class MyJdbcRealmTest {
- // 从数据库获取对应用户密码实现认证
- protected static final String AUTHENTICATION_QUERY = "select password from test_users where user_name = ?";
- // 从数据库中获取对应用户的所有角色
- protected static final String USER_ROLES_QUERY = "select role from test_user_roles where user_name = ?";
- // 从数据库中获取角色对应的所有权限
- protected static final String PERMISSIONS_QUERY = "select permission from test_roles_permissions where role = ?";
- DruidDataSource druidDataSource = new DruidDataSource();
- {
- druidDataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
- druidDataSource.setUsername("root");
- druidDataSource.setPassword("0113");
- }
- @Test
- public void testMyJdbcRealm(){
- //1. 创建 Realm 并设置数据库查询语句
- JdbcRealm jdbcRealm = new JdbcRealm();
- jdbcRealm.setDataSource(druidDataSource);// 配置数据源
- jdbcRealm.setPermissionsLookupEnabled(true);// 设置允许查询权限, 否则 checkPermission 抛异常, 默认值为 false
- jdbcRealm.setAuthenticationQuery(AUTHENTICATION_QUERY);// 设置用户认证信息查询语句
- jdbcRealm.setUserRolesQuery(USER_ROLES_QUERY);// 设置用户角色信息查询语句
- jdbcRealm.setPermissionsQuery(PERMISSIONS_QUERY);// 设置角色权限信息查询语句
- //2. 创建 SecurityManager 并配置环境
- DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
- defaultSecurityManager.setRealm(jdbcRealm);
- //3. 创建 subject
- SecurityUtils.setSecurityManager(defaultSecurityManager);
- Subject subject = SecurityUtils.getSubject();
- //4.Subject 通过 Token 提交认证
- UsernamePasswordToken token = new UsernamePasswordToken("java","123");
- subject.login(token);// 退出登录 subject.logout();
- // 验证认证与授权情况
- System.out.println("isAuthenticated:"+ subject.isAuthenticated());
- subject.hasRole("admin");
- subject.checkPermission("user:delete");
- }
- }
自定义 Realm
自定义 Realm, 可以继承抽象类 AuthorizingRealm, 实现其两个方法 --doGetAuthenticationInfo 和 doGetAuthorizationInfo, 分别用来返回 AuthenticationInfo(认证信息)和 AuthorizationInfo(授权信息).
如上所示, AuthenticationInfo 包含 principal 和 crendential, 和 token 一样, 不同的是, 前者是切切实实的在数据库或其他数据源中的数据, 而后者是用户输入的, 待校验的数据.
AuthorizationInfo 也是类似的, 包含用户的角色和权限信息.
可以用 SimpleAuthenticationInfo 和 SimpleAuthorizationInfo 来实现这两个方法:
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
- //1. 获取主体中的用户名
- String userName = (String) authenticationToken.getPrincipal();
- //2. 通过用户名获取密码, getPasswordByName 自定义实现
- String password = getPasswordByUserName(userName);
- if(null == password){
- return null;
- }
- // 构建 AuthenticationInfo 返回
- SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
- return authenticationInfo;
- }
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
- //1. 获取用户名. principal 为 Object 类型, 是用户唯一标识, 可以是用户名, 用户邮箱, 数据库主键等, 能唯一确定一个用户的信息.
- String userName = (String) principalCollection.getPrimaryPrincipal();
- //2. 获取角色信息, getRoleByUserName 自定义
- Set<String> roles = getRolesByUserName(userName);
- //3. 获取权限信息, getPermissionsByRole 方法同样自定义, 也可以通过用户名查找权限信息
- Set<String> permissions = getPermissionsByUserName(userName);
- //4. 构建认证信息并返回.
- SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
- simpleAuthorizationInfo.setStringPermissions(permissions);
- simpleAuthorizationInfo.setRoles(roles);
- return simpleAuthorizationInfo;
- }
完整的, 包含添加用户功能的自定义 Realm
- MyRealm.java: import org.apache.shiro.authc.AuthenticationException;
- import org.apache.shiro.authc.AuthenticationInfo;
- import org.apache.shiro.authc.AuthenticationToken;
- import org.apache.shiro.authc.SimpleAuthenticationInfo;
- import org.apache.shiro.authz.AuthorizationInfo;
- import org.apache.shiro.authz.SimpleAuthorizationInfo;
- import org.apache.shiro.realm.AuthorizingRealm;
- import org.apache.shiro.subject.PrincipalCollection;
- import org.apache.shiro.util.CollectionUtils;
- import java.util.HashSet;
- import java.util.Map;
- import java.util.Set;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.locks.ReadWriteLock;
- import java.util.concurrent.locks.ReentrantReadWriteLock;
- public class MyRealm extends AuthorizingRealm {
- /** 存储用户名和密码 */
- protected final Map < String,
- String > userMap;
- /** 存储用户及其对应的角色 */
- protected final Map < String,
- Set < String >> roleMap;
- /** 存储所有角色以及角色对应的权限 */
- protected final Map < String,
- Set < String >> permissionMap; {
- // 设置 Realm 名
- super.setName("MyRealm");
- }
- public MyRealm() {
- userMap = new ConcurrentHashMap < >(16);
- roleMap = new ConcurrentHashMap < >(16);
- permissionMap = new ConcurrentHashMap < >(16);
- }
- /**
- * 身份认证必须实现的方法
- * @param authenticationToken token
- * @return org.apache.shiro.authc.AuthenticationInfo
- */
- @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
- //1. 获取主体中的用户名
- String userName = (String) authenticationToken.getPrincipal();
- //2. 通过用户名获取密码, getPasswordByName 自定义实现
- String password = getPasswordByUserName(userName);
- if (null == password) {
- return null;
- }
- // 构建 AuthenticationInfo 返回
- SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, password, "MyRealm");
- return authenticationInfo;
- }
- /**
- * 用于授权
- * @return org.apache.shiro.authz.AuthorizationInfo
- */
- @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
- //1. 获取用户名. principal 为 Object 类型, 是用户唯一标识, 可以是用户名, 用户邮箱, 数据库主键等, 能唯一确定一个用户的信息.
- String userName = (String) principalCollection.getPrimaryPrincipal();
- //2. 获取角色信息, getRoleByUserName 自定义
- Set < String > roles = getRolesByUserName(userName);
- //3. 获取权限信息, getPermissionsByRole 方法同样自定义, 也可以通过用户名查找权限信息
- Set < String > permissions = getPermissionsByUserName(userName);
- //4. 构建认证信息并返回.
- SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
- simpleAuthorizationInfo.setStringPermissions(permissions);
- simpleAuthorizationInfo.setRoles(roles);
- return simpleAuthorizationInfo;
- }
- /**
- * 自定义部分, 通过用户名获取权限信息
- * @return java.util.Set<java.lang.String>
- */
- private Set < String > getPermissionsByUserName(String userName) {
- //1. 先通过用户名获取角色信息
- Set < String > roles = getRolesByUserName(userName);
- //2. 通过角色信息获取对应的权限
- Set < String > permissions = new HashSet < String > ();
- //3. 添加每个角色对应的所有权限
- roles.forEach(role - >{
- if (null != permissionMap.get(role)) {
- permissions.addAll(permissionMap.get(role));
- }
- });
- return permissions;
- }
- /**
- * 自定义部分, 通过用户名获取密码, 可改为数据库操作
- * @param userName 用户名
- * @return java.lang.String
- */
- private String getPasswordByUserName(String userName) {
- return userMap.get(userName);
- }
- /**
- * 自定义部分, 通过用户名获取角色信息, 可改为数据库操作
- * @param userName 用户名
- * @return java.util.Set<java.lang.String>
- */
- private Set < String > getRolesByUserName(String userName) {
- return roleMap.get(userName);
- }
- /**
- * 往 realm 添加账号信息, 变参不传值会接收到长度为 0 的数组.
- */
- public void addAccount(String userName, String password) throws UserExistException {
- addAccount(userName, password, (String[]) null);
- }
- /**
- * 往 realm 添加账号信息
- * @param userName 用户名
- * @param password 密码
- * @param roles 角色(变参)
- */
- public void addAccount(String userName, String password, String...roles) throws UserExistException {
- if (null != userMap.get(userName)) {
- throw new UserExistException("user \"" + userName + "\" exists");
- }
- userMap.put(userName, password);
- roleMap.put(userName, CollectionUtils.asSet(roles));
- }
- /**
- * 从 realm 删除账号信息
- * @param userName 用户名
- */
- public void delAccount(String userName) {
- userMap.remove(userName);
- roleMap.remove(userName);
- }
- /**
- * 添加角色权限, 变参不传值会接收到长度为 0 的数组.
- * @param roleName 角色名
- */
- public void addPermission(String roleName, String...permissions) {
- permissionMap.put(roleName, CollectionUtils.asSet(permissions));
- }
- /** 用户已存在异常 */
- public class UserExistException extends Exception {
- public UserExistException(String message) {
- super(message);
- }
- }
- }
测试代码
- MyRealmTest.java:
- import org.apache.shiro.SecurityUtils;
- import org.apache.shiro.authc.UsernamePasswordToken;
- import org.apache.shiro.mgt.DefaultSecurityManager;
- import org.apache.shiro.subject.Subject;
- import org.junit.Test;
- public class MyRealmTest {
- @Test
- public void testMyRealm(){
- //1. 创建 Realm 并添加数据
- MyRealm myRealm = new MyRealm();
- try {
- myRealm.addAccount("java", "123", "admin");
- }catch (Exception e){
- e.printStackTrace();
- }
- myRealm.addPermission("admin","user:delete");
- //2. 创建 SecurityManager 并配置环境
- DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
- defaultSecurityManager.setRealm(myRealm);
- //3. 创建 subject
- SecurityUtils.setSecurityManager(defaultSecurityManager);
- Subject subject = SecurityUtils.getSubject();
- //4.Subject 通过 Token 提交认证
- UsernamePasswordToken token = new UsernamePasswordToken("java","123");
- subject.login(token);// 退出登录 subject.logout();
- // 验证认证与授权情况
- System.out.println("isAuthenticated:"+ subject.isAuthenticated());
- subject.hasRole("admin");
- subject.checkPermission("user:delete");
- }
- }
加密
使用加密, 只需要在 Realm 返回的 AuthenticationInfo 添加用户密码对应的盐值:
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
- //1. 获取主体中的用户名
- String userName = (String) authenticationToken.getPrincipal();
- //2. 通过用户名获取密码, getPasswordByName 自定义实现
- String password = getPasswordByUserName(userName);
- if(null == password){
- return null;
- }
- //3. 构建 authenticationInfo 认证信息
- SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName,password,"MyRealm");
- // 设置盐值
- String salt = getSaltByUserName(userName);
- authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(salt));
- return authenticationInfo;
- }
并且在测试代码中给 Realm 设置加密:
- // 设置加密
- HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
- matcher.setHashAlgorithmName("MD5");// 设置加密算法
- matcher.setHashIterations(3);// 设置加密次数
- myEncryptedRealm.setCredentialsMatcher(matcher);
CredentialMatcher 用来匹配用户密码. shiro 通过 Realm 获取 AuthenticationInfo,AuthenticationInfo 里面包含该用户的 principal,credential,salt.principal 就是用户名或手机号或其他, credential 就是密码(加盐加密后, 存到数据源中的密码),salt 就是密码对应的 "盐".shiro 获取到这些信息之后, 利用 CredentialMatcher 中配置的信息(加密算法, 加密次数等), 对 token 中用户输入的, 待校验的密码, 进行加盐加密, 然后比对结果是否和 AuthenticationInfo 中的 credential 一致, 若一致, 则用户通过认证.
"加密" 算法一般用的是 hash 算法, hash 算法并不是用来加密的, 而是用来生成信息摘要, 该过程是不可逆的, 不能从结果逆推得出用户的密码的原文. 下文这一段话也是相对于 hash 算法而言, 其他算法不在考虑范围内. shiro 的 CredentialMatcher 并没有 "解密" 这个概念, 因为不能解密, 不能把数据库中 "加密" 后的密码还原, 只能对用户输入的密码, 进行一次相同的 "加密", 然后比对数据库的 "加密" 后的密码, 从而判断用户输入的密码是否正确.
必要地, 在存密码时, 要存储加盐加密后的密码:
- public void addAccount(String userName,String password,String... roles) throws UserExistException {
- if(null != userMap.get(userName)){
- throw new UserExistException("user \""+ userName +"\" exist");
- }
- // 如果配置的加密次数大于 0, 则进行加密
- if(iterations> 0){
- // 使用随机数作为密码, 可改为 UUID 或其它
- String salt = String.valueOf(Math.random()*10);
- saltMap.put(userName,salt);
- password = doHash(password, salt);
- }
- userMap.put(userName, password);
- roleMap.put(userName, CollectionUtils.asSet(roles));
- }
MyEncryptedRealm.java
MyEncryptedRealmTest.java
文件传送门
GitHub 地址
来源: https://www.cnblogs.com/life-of-coding/p/12173634.html