这里只是shiro简单描述,具体请看教程资源
什么是shiro
Shiro 是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大
shiro核心架构
Subject
Subject即主体
,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
SecurityManager
SecurityManager即安全管理器
,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。
SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
Authenticator(认证)
Authenticator即认证器
,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
Authorizer(授权)
Authorizer即授权器
,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
Realm(领域)
Realm即领域
,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
- 注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。(包括调用Authenticator和Authorizer的代码)
SessionManager(安全管理器)
sessionManager即会话管理
,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
其他构件暂时不需要知道
认证与授权
认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
关键对象
- Subject:主体
访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
- Principal:身份信息
是主体(subject)进行身份认证的标识,标识必须具有唯一性
,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)通常是用户名。
- credential:凭证信息
是只有主体自己知道的安全信息,如密码、证书等。通常是密码
认证流程
授权流程
代码过程分析(非加密)
main方法大致流程
- 创建securityManager
- 创建Realm
- 将Realm注入securityManager
- 在SecurityUtils中设置securityManager
- 获取主体对象
- 创建token令牌(UsernamePasswordToken)(模拟从前端获取的token)
- 调用subject.login(token);//用户登录
- 登陆会根据不同情况抛出错误(都继承自AuthenticationException)进行处理
public class TestAuthenticator {
public static void main(String[] args) {
//创建securityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//将安全工具类中设置默认安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//创建token令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaochen", "123");
try {
subject.login(token);//用户登录
System.out.println("登录成功~~");
}
//这些错误抛出顺序有要求的
catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!!");
}catch(……){
……
}
}
}
自定义Realm的代码分析
第一部分是认证doGetAuthenticationInfo
- 认证需要返回一个认证信息AuthenticationInfo(是一个接口)
一般返回其实现类SimpleAuthenticationInfo
对于无加密的情况,SimpleAuthenticationInfo构造一般包含三个
Object principal(身份), Object credentials(凭证), String realmName(Realm的name)
/**
*认证
*上边的程序使用的是Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,
*大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm
*/
public class CustomerRealm extends AuthorizingRealm {
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
if("xiaochen".equals(principal)){
return new SimpleAuthenticationInfo(principal,"123",this.getName());
}
return null;
}
}
第二部分是授权doGetAuthorizationInfo
- 授权需要返回一个授权信息AuthorizationInfo(接口)
一般返回其实现类SimpleAuthorizationInfo,获得主要身份,为授权信息添加角色访问权限或者资源访问权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
System.out.println("primaryPrincipal = " + primaryPrincipal);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addStringPermission("user:update:*");
simpleAuthorizationInfo.addStringPermission("product:*:*");
return simpleAuthorizationInfo;
}
在main中登陆成功后(前面的代码和上面一样)
……
//认证通过
if(subject.isAuthenticated()){
//基于角色权限管理
boolean admin = subject.hasRole("admin");
System.out.println(admin);
boolean permitted = subject.isPermitted("product:create:001");
System.out.println(permitted);
}
加密
一般在涉及到密码存储问题上,应该加密 / 生成密码摘要存储,而不是存储明文密码
存储密码在shiro中涉及的位置就是Realm中
因此在Realm的认证中一般使用md5+salt对密码进行加密
md5是一个算法,salt相当与加密的一个因子,再通过不同的散列次数对密码进行加密
//认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String principal = (String) token.getPrincipal();
if("xiaochen".equals(principal)){
/**
*使用散列算法(MD5)对密码进行加密(在加密对时候一般加一个只有系统知道对salt(盐),
* 增加安全性)再hash1024次转换成String
*/
//password本来是123
String salt = "Q4F%";
SimpleHash hash = new SimpleHash("MD5", "123", "Q4F%",1024);
String password=hash.toString();
String md5 = new Md5Hash("123","Q4F%",1024).toString();
//String password = "3c88b338102c1a343bcb88cd3878758e";
System.out.println(password);
System.out.println(md5);
/**
* SimpleAuthenticationInfo(Object principal, Object hashedCredentials,
* ByteSource credentialsSalt, String realmName)
*/
return new SimpleAuthenticationInfo(principal,password,
ByteSource.Util.bytes(salt),this.getName());
}
return null;
}
在main方法中,需要创建HashedCredentialsMatcher注入到Realm,HashedCredentialsMatcher用于解密,salt不用设置,会通过SimpleAuthenticationInfo自动获取
public static void main(String[] args) {
//创建securityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置为自定义realm获取认证数据
CustomerRealm_md5_salt customerRealm = new CustomerRealm_md5_salt();
//设置md5加密,用于验证时解码密码(这只是加密解密的一种,shiro提供了更多的方式,我不会就不写了)
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");//设置加密算法
credentialsMatcher.setHashIterations(1024);//设置散列次数(Real中的密码是散列1024次后得到的)
customerRealm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(customerRealm);
…………
…………
}
密码重试次数限制
如在 1 个小时内密码最多重试 5 次,如果尝试次数超过 5 次就锁定 1 小时,1 小时后可再次重试,如果还是重试失败,可以锁定如 1 天,以此类推,防止密码被暴力破解。我们通过继承 HashedCredentialsMatcher,且使用 Ehcache 记录重试次数和超时时间。
如果密码输入正确清除 cache 中的记录;否则 cache 中的重试次数 +1,如果超出 5 次那么抛出异常表示超出重试次数了。
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String)token.getPrincipal();
//retry count + 1
Element element = passwordRetryCache.get(username);
if(element == null) {
element = new Element(username , new AtomicInteger(0));
passwordRetryCache.put(element);
}
AtomicInteger retryCount = (AtomicInteger)element.getObjectValue();
if(retryCount.incrementAndGet() > 5) {
//if retry count > 5 throw
throw new ExcessiveAttemptsException();
}
boolean matches = super.doCredentialsMatch(token, info);
if(matches) {
//clear retry count
passwordRetryCache.remove(username);
}
return matches;
}
具体代码看教程资源xxx.md
文档有些细节有问题,但不是大问题,自行斟酌
教程资源(免费)
shiro简单实现gitee(附有资源)