技术背景, 控制转发用的是springMVC,持久化层使用Mybatis,缓存用redis,前台框架easyUI,自动化构建项目使用maven
一、导包
maven引入使用shiro所需的包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.1</version>
</dependency>
二、登陆
@RequestMapping(value = "/login")
public OperationPrompt login(String userID, String password,boolean rememberMe)
throws NoSuchAlgorithmException {
OperationPrompt op = null;
String encodePassword = setEncrypting(password);//加密密码
try{
UsernamePasswordToken token = new UsernamePasswordToken(userID,encodePassword, false);
token.setRememberMe(rememberMe);//设置记住我 自动登录
SecurityUtils.getSubject().login(token);
op = new OperationPrompt("用户登录成功", true);
}catch(AuthenticationException ae){
logger.error(ae);
op = new OperationPrompt(ae.getMessage(), false);
return op;
}catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//查出用户所有角色的权限(包括会员等级的高级设置)
List<Permission> permissions = new ArrayList<Permission>();
com.isoftstone.securityframework.api.domain.Permission p = new com.isoftstone.securityframework.api.domain.Permission();
p.setId(1);
permissions.add(p);
//避免空指针
jedisPoolManager.set(SerializeUtils.serialize("permission:"+userID), SerializeUtils.serialize(permissions));
//登陆成功后强制加载shiro权限缓存 避免懒加载 先清除
restAuthRealm.forceShiroToReloadUserAuthorityCache();
return op;
}
这是登陆的controller层方法,前台请求传入用户名和密码,还有是否记住我(shiro提供rememberMe功能),token携带用户名和密码交给SecurityUtils.getSubject()的主体,调用login登陆方法,调用login之后,shiro会委托SecurityManager进行身份认证,然后SecurityManager又会交给认证器Authenticator根据认证策略Authentication strategy进行认证逻辑,Authentication strategy认证策略就是有一个或者多个realm域来实现,因此最终的认证逻辑是写在我们自定义的realm域中,代码如下。
/**
* user login
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken authToken = (UsernamePasswordToken) token;
String accountId = authToken.getUsername();
String password = String.valueOf(authToken.getPassword());
// 登陆方法需要添加 平台参数 系统参数过滤
AccountQuery query = new AccountQuery();
query.setAccountId(accountId);
query.setEmail(accountId);
query.setMoblie(accountId);
query.setPlatformId(platformLabel);
query.setSubSystemId(systemLabel);
//获取账户信息
com.isoftstone.securityframework.api.domain.Account domainAccount =
(com.isoftstone.securityframework.api.domain.Account)accountManagerImpl.getAccount(accountId,platformLabel,systemLabel);
//判断用户是否存在
if (null == domainAccount){
throw new UnknownAccountException(String.format("账号[%s]不存在!", accountId));
}
//检查用户密码是否匹配
if (!domainAccount.getPassword().equals(password)){
throw new IncorrectCredentialsException (String.format("[%s]密码错误!", accountId));
}
//检查账号是否激活
if(STATUS_NOTACTIVATED == domainAccount.getStatus()){
throw new AuthenticationException (String.format("用户名[%s]未激活!", accountId));
}
//检查账号是否已冻结
if(STATUS_FREEZEEXCED == domainAccount.getStatus()){
throw new AuthenticationException (String.format("账号[%s]已冻结", accountId));
}
//检查账号身份是否冻结
AccountCommon accountCommon = domainAccount.getAccCommon();
if(accountCommon!=null){
if(accountCommon.getIsBuyer()!=4 &&accountCommon.getIsSaler()!=4){
throw new AuthenticationException (String.format("账号[%s]已经被冻结", accountId));
}
}
//设置登录时间
domainAccount.setLastLoginTime(DateUtils.getToday(DateUtils.TIMEF_FORMAT));
accountManagerImpl.modify(domainAccount);
Account authAccount = new Account();
this.copyPropertiesToAuthAccount(domainAccount,authAccount);
//设置已认证的用户信息到用户对象中
SimpleAuthenticationInfo simpleAuthInfo = new SimpleAuthenticationInfo(authAccount,authAccount.getPassword(),getName());
return simpleAuthInfo;
}
我们自定义的这个realm(restAuthRealm)继承AuthorizingRealm,认证逻辑写在doGetAuthenticationInfo方法中,根据前台传入的用户名和其他条件从数据库中查找出账号与前台传入的密码比对,判断是否为系统合法用法。当然还可以有其他的判断,比如这里的账号是否激活,是否冻结,是否存在。认证完成之后将已认证的用户信息返回。根据我的理解,返回的该用户信息应该存储在了redis的缓存中,以便系统其他地方用到当前登陆人的信息。
接着,我们回到上面controller层的login方法,用户名、密码认证通过后,我们需要把原来缓存中的权限信息清除,也就是clear掉原来的授权信息SimpleAuthorizationInfo。
//登陆成功后强制加载shiro权限缓存 避免懒加载 先清除
restAuthRealm.forceShiroToReloadUserAuthorityCache();
完成这个操作,需要在我们自己定义的realm(restAuthRealm)中实现clearCachedAuthorizationInfo()方法,代码如下:
/**
* 更新用户授权信息缓存.
*/
public void clearCachedAuthorizationInfo(Object principal) {
SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, getName());
clearCachedAuthorizationInfo(principals);
}
//登陆成功后强制加载shiro权限缓存 避免懒加载 先清除
public void forceShiroToReloadUserAuthorityCache(){
this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipal());
this.isPermitted(SecurityUtils.getSubject().getPrincipals(),"强制加载缓存,避免懒加载"+ System.currentTimeMillis());
}
登陆成功后,清除原来的缓存授权信息。这里还调用了this.isPermitted()这个方法,它的作用是鉴权时用来判断是否有权限,那为什么会在清除缓存时调用?翻阅源码,原来isPermitted()的执行是这样的,它会先去缓存中获取授权信息(一般是权限信息),如果缓存中没有,那么它会调用doGetAuthenticationInfo(AuthenticationToken token)获取授权信息,然后用获取到的授权信息(权限)和传进来的权限标识比较,相等则表示具有该权限。由于我们在登陆成功后,首先是清理掉了原来的缓存授权信息(权限信息),因此我们又得往缓存中加入最新的授权信息以保证每次登陆进系统时都是最新的权限。根据isPermitted()的这个特点,这里一调用它,shiro就会去执行doGetAuthenticationInfo(AuthenticationToken token)方法(因为缓存中已经没有授权信息了),我们在自定义的realm(restAuthRealm)中重写了这个方法,代码如下:
/**
* user permission query(Get authorization info from Cache first or get info from remote service )
*/
@SuppressWarnings("unchecked")
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
Account account1 = (Account)getAvailablePrincipal(principals);
com.isoftstone.securityframework.api.domain.Account account = (com.isoftstone.securityframework.api.domain.Account) accountManagerImpl.get(account1.getId());
List<com.isoftstone.securityframework.api.Permission> perms = new ArrayList<Permission>();
//从数据库中查询权限
perms = rolePermissionRealm.getSubjectPermission(account);
Set<String> permSet = new HashSet<String>();
if(null != perms){
for (Permission perm : perms) {
String permissionName = perm.getPermissionName();
//将前缀去掉
int beginIndex = platformLabel.length() +systemLabel.length()+2;
permissionName = permissionName.substring(beginIndex);
permSet.add(permissionName);
}
}
SimpleAuthorizationInfo simpleAuthInfo = new SimpleAuthorizationInfo();
simpleAuthInfo.setStringPermissions(permSet);
return simpleAuthInfo;
}
doGetAuthenticationInfo(AuthenticationToken token)其实就是shiro的授权,它从数据库中获取当前登陆用户的权限,并把它放在授权信息SimpleAuthorizationInfo当中返回,存储到缓存中以便鉴权时使用。
至此,登陆成功,shiro完成了两件事,其一,前台传过来的用户名和密码认证通过,其二,清除了原来缓存(redis)中的授权信息(权限),加上了当前用户最新的权限信息,方便后面的鉴权。
鉴权方面,我会另外再写一篇博客来和大家讨论。
存在不足:1、登陆时记住我这个功能没有详细描述
2、isPermitted鉴权判断,权限标识未做说明