近期用springboot整合shiro,进行了一个小项目。因为shiro只是一个Java安全框架,并不会做重复登录的拦截,当一个用户登录成功后(chrome登录),再用另外一个浏览器登录(IE登录)或者另外一台电脑进行登录,两个都会登录成功,两个不同的session。(题外话:同一个浏览器登录后,再打开一个标签页访问项目会直接进入登录后跳转的页面,两者是同一个session。)
预期目的:不允许重复登录,预设两个方法解决,方法一、当第二次登录时,把第一个session剔除;方法二、当第二次登录时,给出提示“用户已登录”,停留在登录页面。
废话了一大堆,下面就开始真正的表演,因为是springboot所以省去很多xml配置,全部交给Spring管理,shiro的配置如下:
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro的配置
*
* @Author Rocky
* @Create 2018-11-06 10:20
*/
@Configuration
public class ShiroConfig {
@Bean(name = "sessionDAO")
public MemorySessionDAO getMemorySessionDAO() {
return new MemorySessionDAO();
}
@Bean(name = "sessionIdCookie")
public SimpleCookie getSimpleCookie() {
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("SHRIOSESSIONID");
return simpleCookie;
}
//配置shiro session 的一个管理器
@Bean(name = "sessionManager")
public DefaultWebSessionManager getDefaultWebSessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(getMemorySessionDAO());
sessionManager.setSessionIdCookie(getSimpleCookie());
return sessionManager;
}
//配置session的缓存管理器
@Bean(name = "shiroCacheManager")
public MemoryConstrainedCacheManager getMemoryConstrainedCacheManager() {
return new MemoryConstrainedCacheManager();
}
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// 登录配置
shiroFilter.setLoginUrl("/login");
shiroFilter.setSuccessUrl("/");
shiroFilter.setUnauthorizedUrl("/error?code=403");
// 自定义过滤器
Map filtersMap = new LinkedHashMap();
filtersMap.put("myLoginFilter", new MyLoginFilter());
shiroFilter.setFilters(filtersMap);
// 拦截配置
Map filterChainDefinitions = new LinkedHashMap<>();
filterChainDefinitions.put("/assets/**", "anon");
filterChainDefinitions.put("/module/**", "anon");
filterChainDefinitions.put("/api/**", "anon");
filterChainDefinitions.put("/druid/**", "anon");
filterChainDefinitions.put("/login", "anon");
filterChainDefinitions.put("/logout", "anon");
filterChainDefinitions.put("/**", "myLoginFilter,authc");
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitions);
return shiroFilter;
}
@Bean(name = "userRealm")
@DependsOn("lifecycleBeanPostProcessor")
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(credentialsMatcher());
return userRealm;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
securityManager.setCacheManager(cacheManager());
securityManager.setSessionManager(getDefaultWebSessionManager());
return securityManager;
}
@Bean(name = "cacheManager")
public EhCacheManager cacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:shiro/ehcache-shiro.xml");
return cacheManager;
}
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5"); //散列算法
credentialsMatcher.setHashIterations(3); //散列次数
return credentialsMatcher;
}
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
LifecycleBeanPostProcessor lifecycleBeanPostProcessor = new LifecycleBeanPostProcessor();
return lifecycleBeanPostProcessor;
}
/**
* shiro里实现的Advisor类,用来拦截注解的方法 .
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
}
我的UserRealm如下:
import com.wf.ew.common.utils.StringUtil;
import com.wf.ew.system.model.Authorities;
import com.wf.ew.system.model.Role;
import com.wf.ew.system.model.User;
import com.wf.ew.system.service.AuthoritiesService;
import com.wf.ew.system.service.RoleService;
import com.wf.ew.system.service.UserService;
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.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Shiro认证和授权
*
* @Author Rocky
* @Create 2018-11-06 10:28
*/
public class UserRealm extends AuthorizingRealm {
@Autowired
@Lazy
private UserService userService;
@Autowired
@Lazy
private RoleService roleService;
@Autowired
@Lazy
private AuthoritiesService authoritiesService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 角色
List userRoles = roleService.getByUserId(user.getUserId());
Set roles = new HashSet<>();
for (int i = 0; i < userRoles.size(); i++) {
roles.add(String.valueOf(userRoles.get(i).getRoleId()));
}
authorizationInfo.setRoles(roles);
// 权限
List authorities = authoritiesService.listByUserId(user.getUserId());
Set permissions = new HashSet<>();
for (int i = 0; i < authorities.size(); i++) {
String authority = authorities.get(i).getAuthority();
if (StringUtil.isNotBlank(authority)) {
permissions.add(authority);
}
}
authorizationInfo.setStringPermissions(permissions);
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
User user = userService.getByUsername(username);
if (user == null) {
throw new UnknownAccountException(); // 账号不存在
}
if (user.getState() != 0) {
throw new LockedAccountException(); // 账号被锁定
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getUsername()), getName());
return authenticationInfo;
}
}
controller具体写法如下:
/**
* 登录
*
* @Author Rocky
* @Create 2018-11-06 10:46
*/
@ResponseBody
@PostMapping("/login")
public JsonResult doLogin(String username, String password, String code, HttpServletRequest request) {
if (StringUtil.isBlank(username, password)) {
return JsonResult.error("账号密码不能为空");
}
if (!CaptchaUtil.ver(code, request)) {
CaptchaUtil.clear(request);
return JsonResult.error("验证码不正确");
}
try {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
subject.login(token);
Collection sessions = sessionDAO.getActiveSessions();
if (subject.isAuthenticated()) {
for (Session session : sessions) {
//方法一、当第二次登录时,给出提示“用户已登录”,停留在登录页面
if(username.equals(session.getAttribute("loginedUser"))){
subject.logout();
throw new RuntimeException("用户已登录");
}
//方法二、当第二次登录时,把第一个session剔除
// if(username.equals(session.getAttribute("loginedUser"))){
// session.setTimeout(0);
// }
}
}
subject.getSession().setTimeout(1000*60*1);
userService.update(getLoginUserId(),subject.getSession().getId().toString());
subject.getSession().setAttribute("loginedUser",username);
addLoginRecord(getLoginUserId(), request);
return JsonResult.ok("登录成功");
} catch (IncorrectCredentialsException ice) {
return JsonResult.error("密码错误");
} catch (UnknownAccountException uae) {
return JsonResult.error("账号不存在");
} catch (LockedAccountException e) {
return JsonResult.error("账号被锁定");
} catch (ExcessiveAttemptsException eae) {
return JsonResult.error("操作频繁,请稍后再试");
} catch (RuntimeException e){
return JsonResult.error(e.getMessage());
}
}
目前方法一和方法二都有一个共同问题:效率!!!!没错,如果有一亿用户在线,那就要遍历一亿次,可怕啊!本人目前还没有想到更好的解决方法,如果有大神有好的方法万望赐教!!
说完两种方法的通病再来单独分析一下方法一和方法二的不足,方法一:如果操作人员不是常规退出,而是通过浏览器清除全部的浏览数据,刷新页面虽然返回登录页面,但是再用刚才的账号登录时会提示已经登录,因为之前的账号shiro还是一直认证通过状态,只能等session超时了才能登录(如果设置session超时时间为2小时或更久那就太悲剧了);方法二:虽然不会产生方法一的问题,但是两个人AB公用一个账户,AB同时登录,AB同时处在下图的页面,A修改了demo的状态为锁定,而B用户的页面不会改变仍是正常的状态。
好了,以上就是springboot集成shiro禁止用户多点登录或重复登录剔除,有不足的地方请各位大神多多指教!