什么是Shiro
Shiro是一个强大的简单易用的Java安全框架,主要用来更便捷的认证,授权,加密,会话管理。Shiro首要的和最重要的目标就是容易使用并且容易理解。Shiro官网
Shiro是一个有许多特性的全面的安全框架,下面这幅图可以了解Shiro的特性:
可以看出shiro除了基本的认证,授权,会话管理,加密之外,还有许多额外的特性。
Shiro架构
Shiro有三个主要的概念:Subject
,SecurityManager
,Realms
,下面这幅图可以看到这些原件之间的交互。
- Subject:翻译为主角,当前参与应用安全部分的主角。可以是用户,可以试第三方服务,可以是cron 任务,或者任何东西。主要指一个正在与当前软件交互的东西。
所有Subject都需要SecurityManager,当你与Subject进行交互,这些交互行为实际上被转换为与SecurityManager的交互
- SecurityManager:安全管理员,Shiro架构的核心,它就像Shiro内部所有原件的保护伞。然而一旦配置了SecurityManager,SecurityManager就用到的比较少,开发者大部分时间都花在Subject上面。
请记得,当你与Subject进行交互的时候,实际上是SecurityManager在背后帮你举起Subject来做一些安全操作。
- Realms:Realms作为Shiro和你的应用的连接桥,当需要与安全数据交互的时候,像用户账户,或者访问控制,Shiro就从一个或多个Realms中查找。
Shiro提供了一些可以直接使用的Realms,如果默认的Realms不能满足你的需求,你也可以定制自己的Realms
核心概念
- 主体:即访问应用的用户,在Shiro中使用Subject代表用户,用户只有授权后才允许访问相应的资源。
- 资源:在应用中用户可以访问的任何东西。比如页面、图片、信息等等的一些数据。
- 权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力,即对资源的CRUD。
- 角色:代表了操作的集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限。
认证
创建一个maven项目,引入如下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.1</version> </dependency>
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.21</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.21</version> <scope>compile</scope> </dependency>
|
然后再创建一个子项目shiro_01_Authentication_ini,在资源文件夹下创建一个shiro.ini文件,使用ini文件来模拟数据库。
1 2 3 4
| [users] zhangsan = 123456 lisi = 123456
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class TestAuthenticationApp { private static final transient Logger log = LoggerFactory.getLogger(TestAuthenticationApp.class); public static void main(String[] args) { log.info("My First Shiro App"); String password = "123456"; String username = "zhangsan"; Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); AuthenticationToken token = new UsernamePasswordToken(username, password); try{ subject.login(token); log.info("认证通过"); }catch (AuthenticationException e){ log.info("认证不通过"); } } }
|
其中的setSecurityManager
是把当前的安全管理器绑定到当前线程,这样每一个用户的认证操作就对应一个线程,就不会反复地创建线程;
AuthenticationException
是UnknownAccountException
(用户不存在)和IncorrectCredentialsException
(密码错误)的父类,所以也可以改成
1 2 3 4 5 6 7 8 9
| try { subject.login(token); System.out.println("认证成功"); }catch (UnknownAccountException e){ System.out.println("用户不存在"); }catch (IncorrectCredentialsException e){ System.out.println("密码不正确"); }
|
授权
创建一个子项目shiro_02_Authorization_ini,修改ini文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| [users] zhangsan = 123456, role1 lisi = 123456, role2 wangwu = 123456, role3 zhaoliu = 123456, role2, role3 sunqi = 123456, role4
[roles] role1 = user:query, user:update, user:delete, user:add, user:export role2 = user:query, user:add role3 = user:query, user:export
role4 = *:*
|
可以在上面认证代码后面添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| System.out.println("是否认证:"+subject.isAuthenticated()); if (subject.isAuthenticated()){ System.out.println("==========权限判断=========="); boolean permitted = subject.isPermitted("user:query"); System.out.println("是否有user:query的权限:"+permitted); boolean[] permitted1 = subject.isPermitted("user:query", "user:add", "user:export", "user:delete"); for (boolean b : permitted1) { System.out.println(b); } System.out.println("==========角色判断=========="); boolean role2 = subject.hasRole("role2"); System.out.println("是否有role2的角色:"+role2);
List<String> roles = Arrays.asList("role1", "role2", "role3", "role4"); boolean[] hasRoles = subject.hasRoles(roles); for (boolean hasRole : hasRoles) { System.out.println(hasRole); } }
subject.logout(); System.out.println("是否认证:"+subject.isAuthenticated());
|
使用自定义的Realm
创建UserRealm,并用Service来模拟从数据库中取值。
realm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| public class UserRealm extends AuthorizingRealm { private UserService userService = new UserServiceImpl(); private RoleService roleService = new RoleServiceImpl(); private PermissionService permissionService = new PermissionServiceImpl();
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = token.getPrincipal().toString(); Object credentials = token.getCredentials(); System.out.println(username); System.out.println(credentials);
User user = userService.queryUserByName(username); if (user!=null){ List<String> roles = roleService.queryRolesByName(username); List<String> permissions = permissionService.queryPermissionsByName(username); ActiveUser activeUser = new ActiveUser(user, roles, permissions);
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(activeUser, user.getPwd(), this.getName()); return info; }else{ return null; } }
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal(); List<String> permissions = activeUser.getPermissions(); if (permissions.size()>0) info.addStringPermissions(permissions);
List<String> roles = activeUser.getRoles(); if (roles.size()>0) info.addRoles(roles);
return info; } }
|
- 修改主函数中的代码,在安全管理器中设置Realm,把自定义的UserRealm对象注入。
- 当主函数进行身份认证时,则会走
doGetAuthenticationInfo
里的逻辑,在创建SimpleAuthenticationInfo
对象时,会进行密码匹配,然后返回匹配结果,如果返回null则会抛出UnknownAccountException
异常,如果匹配不成功则会抛出IncorrectCredentialsException
异常。
- 当认证通过后就会进行授权,则会走
doGetAuthorizationInfo
里的逻辑,在这里面则可以为用户添加角色、权限。
doGetAuthorizationInfo
传入的参数就是在认证逻辑里面SimpleAuthenticationInfo
的第一个参数。
entity
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class User { private Integer id; private String username; private String pwd; private Date createTime; }
public class ActiveUser { private User user = new User(); private List<String> roles = new ArrayList<>(); private List<String> permissions = new ArrayList<>(); }
|
service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class UserServiceImpl implements UserService { @Override public User queryUserByName(String username) { switch (username){ case "zhangsan": return new User(1, username, "123456", new Date()); case "lisi": return new User(2, username, "123456", new Date()); case "sunqi": return new User(3, username, "123456", new Date()); } return null; } }
public class PermissionServiceImpl implements PermissionService { @Override public List<String> queryPermissionsByName(String name) { return Arrays.asList("user:query", "user:add", "user:export"); } }
public class RoleServiceImpl implements RoleService { @Override public List<String> queryRolesByName(String name) { return Arrays.asList("role1", "role2", "role3"); } }
|
加密
出于安全着想,我们对用户的密码可以使用某种算法进行加密,加密后的密文存储在数据库中,用户通过输入明文,由后端进行散列加密后,与数据库中的密文进行匹配,如果匹配成功则认证成功。在Shiro中,有几种常见的加密算法:Md2、Md5、Sha1、Sha256等,它们都是可以加入散列值和增加散列次数使密文更难破解。创建一个子项目shiro_05_Authorization_md5:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class MD5Utils { public static void main(String[] args) { String source = "123456"; Md5Hash hash = new Md5Hash(source); System.out.println("使用Md5加密后的结果:"+hash.toString());
Md5Hash hash1 = new Md5Hash(source, "北京武汉"); System.out.println("Md5加密并散列后的结果:" + hash1.toString());
Md5Hash hash2 = new Md5Hash(source, "我最帅", 2); System.out.println("Md5加密并散列2次后的结果:"+hash2.toString()); }
public static String md5(String source, Object salt, Integer hashIterations){ return new Md5Hash(source, salt, hashIterations).toString(); }
public static String sha1(String source, Object salt, Integer hashIterations){ return new Sha1Hash(source, salt, hashIterations).toString(); } }
|
在认证和授权中集成加密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| public class UserRealm extends AuthorizingRealm { private UserService userService = new UserServiceImpl(); private RoleService roleService = new RoleServiceImpl(); private PermissionService permissionService = new PermissionServiceImpl(); public UserRealm() { HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5"); credentialsMatcher.setHashIterations(2); setCredentialsMatcher(credentialsMatcher); }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = token.getPrincipal().toString(); Object credentials = token.getCredentials(); System.out.println(username); System.out.println(credentials);
User user = userService.queryUserByName(username); if (user!=null){ List<String> roles = roleService.queryRolesByName(username); List<String> permissions = permissionService.queryPermissionsByName(username); ActiveUser activeUser = new ActiveUser(user, roles, permissions);
ByteSource credentialsSalt = ByteSource.Util.bytes("我最帅");
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(activeUser, user.getPwd(), credentialsSalt, this.getName()); return info; }else{ return null; } }
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal(); List<String> permissions = activeUser.getPermissions(); if (permissions.size()>0) info.addStringPermissions(permissions);
List<String> roles = activeUser.getRoles(); if (roles.size()>0) info.addRoles(roles); return info; } }
|
在UserRealm的构造器中设置了加密算法和散列次数,而散列值是在SimpleAuthenticationInfo
中的credentialsSalt添加的,一般散列值都会存在数据库中,而加密的规则会在后端代码中设置,这里有三种设置方式:1、如上在构造器中
- 在主函数中设置加密规则
1 2 3 4 5
| UserRealm userRealm = new UserRealm(); HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5"); credentialsMatcher.setHashIterations(2); userRealm.setCredentialsMatcher(credentialsMatcher);
|
- 在ini文件中设置规则,即把加密规则保存在数据库中。
参考网址:简书