Shiro认证
1. 认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
2. Shiro 中认证的关键对象
- Subject 主体:访问系统的用户,主体可以是用户、程序等等,进行认证的都称为主体;
- Principal 身份信息:是主体(Subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)
- Credential 凭证信息:是只有主体自己知道的安全信息,如密码、证书等。
3. 认证流程
4. 认证开发
4.1 模拟数据库中已存储用户信息【创建 shiro.ini 文件】
1 2 3 4
| [users] wangwu=123 zhangsan=123456 lisi=789
|
4.2 开发认证代码
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
| public class TestAuthenticator {
public static void main(String[] args) { DefaultSecurityManager securityManager = new DefaultSecurityManager(); securityManager.setRealm(new IniRealm("classpath:shiro.ini")); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("wangwu", "123"); try { System.out.println("认证状态: " + subject.isAuthenticated()); subject.login(token); System.out.println("认证状态: " + subject.isAuthenticated()); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("认证失败: 用户名不存在~"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("认证失败: 密码错误~"); } } }
|
- DisabledAccountException(帐号被禁用)
- LockedAccountException(帐号被锁定)
- ExcessiveAttemptsException(登录失败次数过多)
- ExpiredCredentialsException(凭证过期)
4.3 自定义 Realm
上边的程序使用的是 Shiro 自带的 IniRealm,IniRealm 从 shiro.ini 配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义 Realm。
1)Shiro 自定义 Realm
2)根据认证源码使用的是 SimpleAccountRealm 【只保留认证和授权这两部分代码】
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
| public class SimpleAccountRealm extends AuthorizingRealm { protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) { throw new LockedAccountException("Account [" + account + "] is locked."); } if (account.isCredentialsExpired()) { String msg = "The credentials for account [" + account + "] are expired"; throw new ExpiredCredentialsException(msg); }
}
return account; }
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = getUsername(principals); USERS_LOCK.readLock().lock(); try { return this.users.get(username); } finally { USERS_LOCK.readLock().unlock(); } } }
|
3)自定义 Realm
由于 Realm
相当于 Datasource 数据源,SecurityManager 进行安全认证需要通过 Realm 获取用户身份数据,所以可以自定义 Realm 获取用户身份的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class CustomerRealm extends AuthorizingRealm {
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("===================="); return null; }
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); System.out.println("principal = " + principal); if ("xiaochen".equalsIgnoreCase(principal)) { SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("wangwu", "123", this.getName()); return simpleAuthenticationInfo; } return null; } }
|
4)使用自定义 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
| public class TestCustomerRealm {
public static void main(String[] args) { DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(new CustomerRealm());
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
AuthenticationToken token = new UsernamePasswordToken("wangwu", "123"); try { subject.login(token); } catch (UnknownAccountException e) { System.out.println("用户名不正确"); e.printStackTrace(); } catch (IncorrectCredentialsException e) { System.out.println("用户密码不正确"); e.printStackTrace(); } }
}
|
MD5+Salt
为什么需要MD5与盐
在TestCustomerRealm中的AuthenticationToken代表的是用户登录信息,在Shiro的认证过程中会与Realm的AuthenticationInfo(即数据库身份信息)相比对,如果不做任何加密处理。数据库信息与用户登录信息特别是密码完全一致,这样如果数据库泄露了,对应的用户信息就会受到极大威胁。
因而数据库并不会直接存有用户明文密码,而是在用户注册时将明文密码经过一系列加密算法获得的匿名密码存入数据库。当用户登录时会在业务层以相同算法算取匿名密码,再与数据库比对。MD5,Salt就是常用的加密算法。
注意的是,Shiro默认情况下是通过AuthenticatingRealm.class下的assertCredentialsMatch方法实现用户信息与数据库信息比对的,且比对方法为Strings.equals()。如果采用加密算法,数据库匿名密码与用户明文密码肯定对不上,因而我们需要更改默认比对方式。
Realm
因为是由Realm携带数据库信息,通过Realm的改造来更改默认比对方式。
1 2 3 4 5 6 7
| HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5");
CustomRealm customRealm = new CustomRealm(); customRealm.setCredentialsMatcher(credentialsMatcher); securityManager.setRealm(customerMD5Realm);
|
这里还只是MD5,如果要加盐,不需要更改Realm,只需要再把带回来的AuthenticationInfo加上盐值就行了,因为Shiro会自动把盐值加在明文密码前再进行MD5计算的。
1 2 3 4 5 6 7 8 9 10 11
| @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String principal = (String)authenticationToken.getPrincipal(); if("xry".equals(principal)){ SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( "xry","8a83592a02263bfe6752b2b5b03a4799",ByteSource.Util.bytes("X0*7ps"),this.getName()); return simpleAuthenticationInfo; } return null; }
|
Hash散列
更深层次地,经过MD5和Salt后通过Hash散列再多次散列计算后就能得到更为复杂的密码。
需要对HashedCredentialsMatcher设置散列次数:
1
| credentialsMatcher.setHashIterations(1024);
|
Shiro授权
1. 授权
授权即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
2. 关键对象
授权可简单理解为 who 对 what(which)进行 How 操作;
Who,即主体(Subject),主体需要访问系统中的资源;
What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为 t01 的商品为资源实例,编号 001 的商品也属于资源实例;
How,权限 / 许可(Permission),规定了主体对资源的操作许可, 权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为 001 用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
3. 授权流程
4. 授权方式
- 基于角色的访问控制【RBAC 基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制】
1 2 3
| if(subject.hasRole("admin")){ }
|
- 基于资源的访问控制【RBAC 基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制】
1 2 3 4 5 6
| if(subject.isPermission("user:update:01")){ } if(subject.isPermission("user:update:*")){ }
|
5. 权限字符串
权限字符串的规则是【资源标识符:操作:资源实例标识符】意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用 * 通配符。
例子:
- 用户创建权限:user : create,或 user : create : *
- 用户修改实例001的权限:user : update : 001
- 用户实例001的所有权限:user : *:001
6. 开发授权代码
自定义 Realm 代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class CustomerMD5Realm extends AuthorizingRealm {
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String principal = (String) principalCollection.getPrimaryPrincipal(); System.out.println("principal:"+principal);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRoles(Arrays.asList("user","admin")); simpleAuthorizationInfo.addStringPermissions(Arrays.asList("user:update:*","admin:*:*","developer:*:01")); return simpleAuthorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); System.out.println("principal = " + principal); return null; } }
|
授权处理
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
| public class TestCustomerMD5Authenticator {
public static void main(String[] args) { DefaultSecurityManager securityManager = new DefaultSecurityManager();
CustomerMD5Realm customerMD5Realm = new CustomerMD5Realm(); HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("md5"); credentialsMatcher.setHashIterations(1024); customerMD5Realm.setCredentialsMatcher(credentialsMatcher); securityManager.setRealm(customerMD5Realm);
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
try { System.out.println("认证状态: " + subject.isAuthenticated()); subject.login(token); System.out.println("认证状态: " + subject.isAuthenticated()); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("认证失败: 用户名不存在~"); } catch (IncorrectCredentialsException e) { e.printStackTrace(); System.out.println("认证失败: 密码错误~"); }
if (subject.isAuthenticated()) { System.out.println(subject.hasRole("super")); System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user"))); boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "super")); for (boolean aBoolean : booleans) { System.out.println(aBoolean); } System.out.println(subject.isPermitted("user:update:01")); boolean[] permitted = subject.isPermitted("user:*:01", "order:*:02"); for (boolean b : permitted) { System.out.println(b); } } } }
|