业务需求:因目前系统已经集成shiro根据用户名和密码加盐加密后校验用户登录信息功能的前提下需要集成第三方单点登陆功能。根据业务需求校验通过单点登陆后进入该项目的时候通过用户名实现免密登陆。
思路:原始已完成了用户名密码登陆功能,并自定义了AuthorizingRealm实现doGetAuthenticationInfo、doGetAuthorizationInfo登陆验证和授权功能。新的免密登陆肯定是需要重新定义Realm实现登陆校验功能。
注:本文侧重于免密登陆的Realm实现,对原Realm的描述就不一一陈述了。
1、自定义UsernamePasswordToken,添加免密的构造方法,区分原正常的用户名密码的token。(添加了type原来标记是否是免密登陆)
package net.sinorock.aj.modules.base.core.security.shiro;
import lombok.Data;
import org.apache.shiro.authc.UsernamePasswordToken;
/**
* @Description:
* @program: aj-parent
* @Auther: MengyuWu
* @Date: 2019-8-11 14:29
**/
@Data
public class MyUserAuthenticationToken extends UsernamePasswordToken {
private String captcha;
private String token;
private static final long serialVersionUID = -2564928913725078138L;
//标识,是否免密登录
private String type;
public MyUserAuthenticationToken(String username,boolean noPwd) {
super(username, "", false, null);
this.type = "NOPWD";
}
public MyUserAuthenticationToken(String username, String password) {
super(username, password);
this.type = "HASPWD";
}
public MyUserAuthenticationToken(String username, String password, boolean rememberMe) {
super(username, password, rememberMe);
this.type = "HASPWD";
}
public MyUserAuthenticationToken(String username, String password, String captcha, boolean rememberMe) {
super(username, password, rememberMe);
setCaptcha(captcha);
this.type = "HASPWD";
}
public MyUserAuthenticationToken(String token){
this.token = token;
this.type = "HASPWD";
}
@Override
public Object getPrincipal() {
if(this.token != null){
return this.token;
}else{
return this.getUsername();
}
}
@Override
public Object getCredentials() {
if(this.token != null){
return this.token;
}else{
return this.getPassword();
}
}
}
2、自定义NoPwdRealm实现免密登陆的校验
package net.sinorock.aj.modules.base.core.security.shiro;
import java.util.*;
import org.apache.commons.collections.CollectionUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import lombok.extern.slf4j.Slf4j;
import net.sinorock.aj.common.constant.GlobalConstant;
import net.sinorock.aj.common.utils.UtilValidate;
import net.sinorock.aj.modules.base.core.security.redis.RedisSessionDAO;
import net.sinorock.aj.modules.base.entity.sys.SysUserEntity;
import net.sinorock.aj.modules.base.service.sys.*;
/**
* @Description: 身份校验核心类
* @program: aj-parent
* @Auther: CShi
* @Date: 2019-8-11 14:26
**/
@Slf4j
public class NoPwdRealm extends AuthorizingRealm
{
@Autowired
@Lazy
private SysUserService sysUserService;
@Autowired
private RedisSessionDAO redisSessionDAO;
@Autowired
@Lazy
private SysConfigService configService;
/**
* 认证信息(身份验证)
* Authentication 是用来验证用户身份
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException
{
if (log.isDebugEnabled())
{
log.info("用户登录:userShiroRealm.doGetAuthenticationInfo()");
}
// 获取用户的输入的账号
String username = (String)token.getPrincipal(); // token is UsernamePasswordToken
// 查询用户信息
SysUserEntity user = sysUserService.queryByUsername(username);
// 检查账号是否不存在或密码错误
if (user == null)
{
throw new UnknownAccountException("账号不存在,请检查账号是否正确");
}
String sysLockTimeStr = configService.getValue("sysLockTime");
int sysLockTimeInMinutes = 0;
if (UtilValidate.isNotEmpty(sysLockTimeStr))
{
sysLockTimeInMinutes = Integer.parseInt(sysLockTimeStr);
}
if (sysUserService.isForbidden(user))
{ // 检查账号是否被禁用
throw new DisabledAccountException("账号已被禁用,请联系管理员");
}
if (sysUserService.isLocked(user, sysLockTimeInMinutes))
{ // 检查账号是否被锁定
throw new LockedAccountException("账号已被锁定,请" + sysLockTimeInMinutes + "分钟后再试");
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, // 用户名
user.getPassword(), // 密码
null, // salt=username+salt
getName()); // realm name
return authenticationInfo;
}
/**
*
* 权限信息(授权):
* 1、如果用户正常退出,缓存自动清空;
* 2、如果用户非正常退出,缓存自动清空;
* 3、如果我们修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。
* (需要手动编程进行实现;放在service进行调用)
* 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,
* 调用clearCached方法;
* Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals)
throws AuthenticationException
{
/*
* 授权信息在缓存中,所以多次刷新页面,doGetAuthorizationInfo只执行一次, 缓存过期之后会再次执行。
*/
log.info("权限校验 --> userShiroRealm.doGetAuthorizationInfo()");
SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal();
String userId = user.getId();
List permsList = null;
// 用户权限列表
Set permsSet = new HashSet<>();
if (user.getIsAdmin() == GlobalConstant.SUPER_ADMIN)
{
permsSet = sysUserService.queryAllPermsForAdmin();
}
else
{
permsSet = sysUserService.queryAllPerms(userId);
}
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setStringPermissions(permsSet);
return authorizationInfo;
}
/**
* 清空当前用户权限信息
*/
public void clearCachedAuthorizationInfo()
{
PrincipalCollection principalCollection = SecurityUtils.getSubject().getPrincipals();
SimplePrincipalCollection principals = new SimplePrincipalCollection(principalCollection,
getName());
super.clearCachedAuthorizationInfo(principals);
}
/**
* 指定principalCollection 清除
*/
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principalCollection)
{
SimplePrincipalCollection principals = new SimplePrincipalCollection(principalCollection,
getName());
super.clearCachedAuthorizationInfo(principals);
}
/**
* 踢出用户
*
* @param userId 用户id
* @param isRemoveSession 是否移除session态,移除会强制该用户重新登录
*/
public void kickOutUser(long userId, boolean isRemoveSession)
{
Set sessionSet = getSessionByUserId(userId);
if (CollectionUtils.isEmpty(sessionSet))
{
return;
}
kickOutUser(sessionSet, isRemoveSession);
}
/**
* 踢出所有用户
*
* @param isRemoveSession 是否移除session态,移除会强制该用户重新登录
*/
public void kickOutAllUser(boolean isRemoveSession)
{
Collection sessionSet = redisSessionDAO.getActiveSessions();
kickOutUser((Set)sessionSet, isRemoveSession);
}
/**
* 踢出用户
*
* @param sessionSet session合集
* @param isRemoveSession 是否移除session态,移除会强制该用户重新登录
*/
public void kickOutUser(Set sessionSet, boolean isRemoveSession)
{
sessionSet.stream().forEach(session -> {
Object attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null)
{
return;
}
// 删除session
if (isRemoveSession)
{
redisSessionDAO.delete(session);
}
// 删除cache,在访问受限接口时会重新授权
clearCachedAuthorizationInfo((SimplePrincipalCollection)attribute);
});
}
/**
* 获取用户对应的所有登录态
*
* @param userId 帐号
* @return
*/
private Set getSessionByUserId(long userId)
{
Collection sessions = redisSessionDAO.getActiveSessions();
SysUserEntity user;
Object attribute;
Set sessionSet = new HashSet<>();
for (Session session : sessions)
{
attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null)
{
continue;
}
user = (SysUserEntity)((SimplePrincipalCollection)attribute).getPrimaryPrincipal();
if (user == null)
{
continue;
}
if (Objects.equals(user.getId(), userId))
{
sessionSet.add(session);
}
}
return sessionSet;
}
}
3、自定义ModularRealmAuthenticator实现通过不同的登陆类型使用不同的Realm校验
package net.sinorock.aj.modules.base.core.security.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.util.CollectionUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* @Description:
* @program: aj-parent
* @Auther: MengyuWu
* @Date: 2019-8-11 14:34
**/
@Slf4j
public class MyUserModularRealmAuthenticator extends ModularRealmAuthenticator
{
private Map definedRealms;
/**
* 多个realm实现
*/
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection realms,
AuthenticationToken token)
{
return super.doMultiRealmAuthentication(realms, token);
}
/**
* 调用单个realm执行操作
*/
@Override
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm,
AuthenticationToken token)
{
// 如果该realms不支持(不能验证)当前token
if (!realm.supports(token))
{
throw new ShiroException("token错误!");
}
AuthenticationInfo info = null;
try
{
info = realm.getAuthenticationInfo(token);
if (info == null)
{
throw new ShiroException("token不存在!");
}
}catch (LockedAccountException e){
throw e;
}
catch (DisabledAccountException e)
{
throw new DisabledAccountException(e.getMessage());
}
catch (Exception e)
{
throw new IncorrectCredentialsException("用户名或者密码错误! Exception: " + e.getMessage());
}
return info;
}
/**
* 判断登录类型执行操作
*/
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException
{
this.assertRealmsConfigured();
// 强制转换回自定义的CustomizedToken
MyUserAuthenticationToken userToken = (MyUserAuthenticationToken) authenticationToken;
// 登录类型
String loginType = userToken.getType();
// 所有Realm
Collection realms = getRealms();
// 登录类型对应的所有Realm
List typeRealms = new ArrayList<>();
for (Realm realm : realms) {
if("HASPWD".equals(loginType)){
//有密码登陆
if(realm.getName().contains("UserShiroRealm")){
typeRealms.add(realm);
}
}else{
//无密码登陆
if(realm.getName().contains("NoPwdRealm")){
typeRealms.add(realm);
}
}
}
// 判断是单Realm还是多Realm
if (typeRealms.size() == 1){
System.out.println("doSingleRealmAuthentication() execute ");
return doSingleRealmAuthentication(typeRealms.get(0), userToken);
}
else{
System.out.println("doMultiRealmAuthentication() execute ");
return doMultiRealmAuthentication(typeRealms, userToken);
}
}
/**
* 判断realm是否为空
*/
@Override
protected void assertRealmsConfigured()
throws IllegalStateException
{
this.definedRealms = this.getDefinedRealms();
if (CollectionUtils.isEmpty(this.definedRealms))
{
throw new ShiroException("值传递错误!");
}
}
public Map getDefinedRealms()
{
return this.definedRealms;
}
public void setDefinedRealms(Map definedRealms)
{
this.definedRealms = definedRealms;
}
}
4、在ShiroConfiguration配置中添加免密验证的Realm
/**
* 身份认证realm;
* @return
*/
@Bean(name = "noPwdRealm")
public NoPwdRealm noPwdRealm()
{
if (log.isDebugEnabled())
{
log.debug("ShiroConfiguration.noPwdRealm()");
}
NoPwdRealm noPwdReaml = new NoPwdRealm();
noPwdReaml.setCredentialsMatcher(retryLimitHashedCredentialsMatcher());
noPwdReaml.setCacheManager(redisCacheManager());
return noPwdReaml;
}
5、在ShiroConfiguration配置中的securityManager中shiroAuthorizerRealms添加新增的免密Realm
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManage()
{
if (log.isDebugEnabled())
{
log.debug("ShiroConfiguration.getDefaultWebSecurityManage()");
}
// 安全管理
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
Map shiroAuthenticatorRealms = new HashMap<>(5);
shiroAuthenticatorRealms.put("userShiroRealm", userShiroRealm());
shiroAuthenticatorRealms.put("noPwdReaml",noPwdRealm());
Collection shiroAuthorizerRealms = new ArrayList<>();
shiroAuthorizerRealms.add(userShiroRealm());
shiroAuthorizerRealms.add(noPwdRealm());
MyUserModularRealmAuthenticator myUserModularRealmAuthenticator = new MyUserModularRealmAuthenticator();
myUserModularRealmAuthenticator.setDefinedRealms(shiroAuthenticatorRealms);
myUserModularRealmAuthenticator.setAuthenticationStrategy(authenticationStrategy());
securityManager.setAuthenticator(myUserModularRealmAuthenticator);
securityManager.setRealms(shiroAuthorizerRealms);
securityManager.setSubjectFactory(new DefaultWebSubjectFactory());
// 缓存管理
securityManager.setCacheManager(redisCacheManager());
// securityManager.setSessionManager(defaultWebSessionManager());
securityManager.setSessionManager(sessionManager());
return securityManager;
}