原文参考转载自:https://www.cnblogs.com/skyLogin/p/10871347.html
--------------------------- 本文根据原文内容,结合自身实际配置步骤进行编写。 ----------------------------
场景:
最近做项目时,多个项目需要用到shiro多用户类型登录,每个角色都在自己独立的库表中,以前有了解过,shiro支持这种业务场景,及多realm验证及鉴权,简单的理解就是,在我们代码中有两套 shiro 鉴权 ,百度找大神的解决办法,看完后,结合本地一些实际进行更改。
这次涉及到的两个用户类型,一个是客户(Customer),一个是公司管理员(Admin),两种账号分别存在不同的表中,登录页面,可以采用多个登录框,或者直接就是多个登录页,我这次选用的是多个登录页,然后每个登录页分别调用自身对应的角色的 LoginController,在 Controller 中,分别调用自身的shiro Realm 进行登录验证,后续操作也是分别调用自身的shiroRealm进行鉴权。
对于当前登录的用户类型(Customer 和 Admin),我刚开始是直接写字符串,后面觉得不妥,于是最后整理的时候,改成常量类的形式,也建议大家一开始就写成常量类的形式,这样做,便于后续的修改和使用,下面具体内容中为便于查看,就没做更改。
shiro 配置多realm 步骤:
1. 分别新建两个类型的 ShiroRealm,我这边是 ShiroRealmCustomer 和 ShiroRealmAdmin,注意类名命名,后面会根据登录用户类型和类名进行匹配区分,决定是使用哪个realm处理业务,下面是 ShiroRealmCustomer 的大致代码,两个类代码结构一样,都是继承 AuthorizeingRealm ,重写对应的鉴权和登录逻辑
/**
* SHIRO客户处理.
*
* @author He 123
* @since 2019-07-03
*/
public class ShiroRealmCustomer extends AuthorizingRealm {
@Resource
private ICustomerInfoService iCustomerInfoService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//鉴权逻辑
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken token2 = (UsernamePasswordToken) token;
String username=token2.getUsername();
/****************登录逻辑******************/
EntityWrapper ew = new EntityWrapper();
ew.eq("account", username)
.eq("del", Consts.System.y);
CustomerInfo customerInfo = iCustomerInfoService.selectOne(ew);
if (null == customerInfo) {
throw new UnknownAccountException();
} else if (customerInfo.getState().equals(Consts.System.n)){
throw new AccountException("当前账号已被锁定,请联系管理员处理");
}
//设置当前登录的用户类型 客户
customerInfo.setUserType(20);
//shiro校验密码
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
JSON.toJSONString(customerInfo), //用户名
customerInfo.getLoginPassword(), //密码
// ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
getName() //realm name
);
return authenticationInfo;
}
/**
* RealmSecurityManager rsm = (RealmSecurityManager)SecurityUtils.getSecurityManager();
AuthRealm authRealm = (AuthRealm)rsm.getRealms().iterator().next();
authRealm.clearAuthz();
*/
public void clearAuthor(){
this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
public void clearAuthen(){
this.clearCachedAuthenticationInfo(SecurityUtils.getSubject().getPrincipals());
}
}
2. 新建org.apache.shiro.authc.UsernamePasswordToken的子类,用来标识当前登录类型,配合后面新建的 org.apache.shiro.authc.pam.ModularRealmAuthenticator 的子类决定使用哪个 realm。。
在我当前项目中就是用来表示当前登录的是客户登录还是管理员登录
import org.apache.shiro.authc.UsernamePasswordToken;
/**
* @Auther: He
* @Create_Date: 2019/7/3 11:29
*/
public class ShiroUsernamePasswordToken extends UsernamePasswordToken {
/*
* 当前登录用户类型
*/
private String userType;
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
public ShiroUsernamePasswordToken(String username, String password, String userType) {
super(username, password);
this.userType = userType;
}
}
3. 新建 org.apache.shiro.authc.pam.ModularRealmAuthenticator的子类,并重写doAuthenticate()方法,通过第二步中的 userType 判断决定使用对应的Realm处理。
这边判断的方法是: 根据我们设定好的登录类型和我们第一步中新建的 Realm 的类名进行字符串匹配,类名中存在登录类型字符串,就使用当前 Realm。
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
/**
* @Auther: He
* @Create_Date: 2019/7/3 11:33
*/
public class ShiroRealmAuthenticator extends ModularRealmAuthenticator {
private Logger logger = LoggerFactory.getLogger(ShiroRealmAuthenticator.class);
/**
* 根据用户类型判断使用哪个Realm
*/
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
super.assertRealmsConfigured();
// 强制转换回自定义的CustomizedToken
ShiroUsernamePasswordToken token = (ShiroUsernamePasswordToken) authenticationToken;
// 登录类型
String userType = token.getUserType();
// 所有Realm
Collection realms = getRealms();
// 登录类型对应的所有Realm
Collection typeRealms = new ArrayList<>();
for (Realm realm : realms) {
//根据登录类型和Realm的名称进行匹配区分
if(realm.getName().contains(userType)){
typeRealms.add(realm);
}
}
// 判断是单Realm还是多Realm,有多个Realm就会使用所有配置的Realm。 只有一个的时候,就直接使用当前的Realm。
if (typeRealms.size() == 1) {
logger.info("doSingleRealmAuthentication() execute ");
return doSingleRealmAuthentication(typeRealms.iterator().next(), token);
} else {
logger.info("doMultiRealmAuthentication() execute ");
return doMultiRealmAuthentication(typeRealms, token);
}
}
}
4.修改 ShiroCofing
4.1 修改 securityManager ,修改为添加多个 realm
修改前,单个 realm:
@Bean
public ShiroRealm myShiroRealm(){
ShiroRealm myShiroRealm = new ShiroRealm();
return myShiroRealm;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(shiroCacheManager());
return securityManager;
}
修改后,增加多个realm设置,增加 modularRealmAuthenticator() ,针对多realm的管理
@Bean
public ShiroRealmAdmin myShiroRealm(){
ShiroRealmAdmin myShiroRealm = new ShiroRealmAdmin();
return myShiroRealm;
}
@Bean("shiroRealmCustomer")
public ShiroRealmCustomer shiroRealmCustomer() {
return new ShiroRealmCustomer();
}
/**
* 系统自带的Realm管理,主要针对多realm 认证
*/
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator() {
//自己重写的ModularRealmAuthenticator
UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List realms = new ArrayList<>();
realms.add(myShiroRealm());
realms.add(shiroRealmCustomer());
securityManager.setAuthenticator(modularRealmAuthenticator()); // 需要再realm定义之前
securityManager.setRealms(realms);
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(shiroCacheManager());
return securityManager;
}
/**
* 页面上使用shiro标签
* @return
*/
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
5.上述配置的Authenticator主要针对登陆认证,对于授权时没有控制的,使用资源注入时会发现,使用的是myShiroRealmSHOP的doGetAuthorizationInfo方法(上面SHOP的定义在前),没有走对应的realm的授权,产生问题错乱,新建 org.apache.shiro.authz.ModularRealmAuthorizer子类
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.shiro.authz.ModularRealmAuthorizer;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
/**
* @Auther: He
* @Create_Date: 2019/7/5 11:55
*/
public class UserModularRealmAuthorizer extends ModularRealmAuthorizer {
@Override
public boolean isPermitted(PrincipalCollection principals, String permission) {
assertRealmsConfigured();
// 所有Realm
Collection realms = getRealms();
HashMap realmHashMap = new HashMap<>(realms.size());
for (Realm realm : realms) {
if (realm.getName().contains("Admin")) {
realmHashMap.put("ShiroRealmAdmin", realm);
} else if (realm.getName().contains("Customer")) {
realmHashMap.put("ShiroRealmCustomer", realm);
}
}
Set realmNames = principals.getRealmNames();
if (realmNames != null) {
String realmName = null;
Iterator it = realmNames.iterator();
while (it.hasNext()) {
realmName = ConvertUtils.convert(it.next());
if (realmName.contains("Admin")) {
return ((ShiroRealmAdmin) realmHashMap.get("ShiroRealmAdmin")).isPermitted(principals, permission);
} else if (realmName.contains("Customer")) {
return ((ShiroRealmCustomer) realmHashMap.get("ShiroRealmCustomer")).isPermitted(principals, permission);
}
break;
}
}
return false;
}
}
6.修改 ShiroConfig 增加多realm下的授权控制
securityManager.setAuthorizer(modularRealmAuthorizer()); // 新增授权控制
/**
* 系统自带的Realm管理,主要针对多realm 认证
*/
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator() {
//自己重写的ModularRealmAuthenticator
UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List realms = new ArrayList<>();
realms.add(myShiroRealm());
realms.add(shiroRealmCustomer());
securityManager.setAuthenticator(modularRealmAuthenticator()); // 需要再realm定义之前
// 新增下面这个授权控制
securityManager.setAuthorizer(modularRealmAuthorizer());
securityManager.setRealms(realms);
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(shiroCacheManager());
return securityManager;
}
7. 分别修改对应的登录 Controller ,分别设置当前的登录类型。
ShiroUsernamePasswordToken就是在第2步中新建的UsernamePasswordToken的子类
userType 就是当前我们定义的登录类型,和第1步介绍中所说,会和角色的Realm名称进行匹配,比如我这边的 Customer ,对应的 Realm 就是 ShiroRealmCustomer
例如下面的是客户登录,管理员类似
/**
* 客户登录
* @param request
* @param customerInfo
* @return
*/
@RequestMapping("/login")
public String customerLogin(HttpServletRequest request, @RequestBody CustomerInfo customerInfo) {
Subject subject = SecurityUtils.getSubject();
subject.login(new ShiroUsernamePasswordToken(customerInfo.getAccount(), customerInfo.getPassword(), Consts.UserType.CUSTOMER));
Session session = SecurityUtils.getSubject().getSession();
return getResult(Result.OK, "登录成功", session.getId(),"/index");
}
至此,shiro多realm配置就差不多完成了,可以直接启动项目测试一下了。
剩下的工作比如:完善各自角色的Realm 中的 doGetAuthorizationInfo() 权限校验方法,改造自己项目中的菜单,资源,角色、缓存等等就根据实际情况,增加当前登录的角色作为条件进行筛选。
多说一句,我在用户登录登录成功后,默认会往用户登录成功后的实体类中保存我们当前登录的用户类型,跟随当前的登录信息进行保存,这样在后续开发中,如果有需要,我们可以直接从当前登录用户对象中获取当前的用户类型。
获取当前登录用户JSONObject,因为我当前项目中不同的用户类型对应不同的实体类,所以我没有转换成实体类的形式,而是直接返回JSONObject,这样更为通用些。
public static JSONObject getCurrentObject() throws UnauthenticatedException {
Object principal = SecurityUtils.getSubject().getPrincipal();
if(principal!=null){
JSONObject currentObj = JSONObject.parseObject(String.valueOf(principal));
return currentObj;
}else{
throw new UnauthenticatedException();
}
}