远方蔚蓝
一刹那情真,相逢不如不见

文章数量 126

访问次数 199827

运行天数 1437

最近活跃 2024-10-04 23:36:48

进入后台管理系统

Springboot整合shiro


Springboot版本2.1.4
public enum LoginType {
    /**
     * 通用
     */
    COMMON("common_realm"),
    /**
     * 用户密码登录
     */
    USER_PASSWORD("user_password_realm"),
    /**
     * 手机验证码登录
     */
    USER_PHONE("user_phone_realm");
    private String type;
    private LoginType(String type) {
        this.type = type;
    }
    public String getType() {
        return type;
    }
    @Override
    public String toString() {
        return this.type.toString();
    }
}
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.authc.UsernamePasswordToken;
public class ShiroToken extends UsernamePasswordToken {
	private static final long serialVersionUID = 6358520744796868017L;
	
	/**
	 * 登录方式
	 */
    private LoginType loginType;
    
    /**
     * 登录验证码
     */
	private String code;
	
	/**
	 * 保存请求的request
	 */
	private HttpServletRequest request;
	public ShiroToken(LoginType loginType, String username, String password, String code) {
        super(username, password);
        this.loginType = loginType;
        this.code = code;
        this.setRememberMe(true); // 记住我的cookie生效
    }
	
	public ShiroToken(HttpServletRequest request, LoginType loginType, String username, String password, String code) {
        super(username, password);
        this.loginType = loginType;
        this.code = code;
        this.setRememberMe(true); // 记住我的cookie生效
        this.request = request;
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
	public LoginType getLoginType() {
		return loginType;
	}
	public void setLoginType(LoginType loginType) {
		this.loginType = loginType;
	}
	public static long getSerialversionuid() {
		return serialVersionUID;
	}
	public HttpServletRequest getRequest() {
		return request;
	}
	public void setRequest(HttpServletRequest request) {
		this.request = request;
	}
}
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.SessionListener;
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.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import wst.st.site.constant.constant.SiteWebConst;
import wst.st.site.shiro.LoginType;
import wst.st.site.shiro.override.MyCredentialsMatcher;
import wst.st.site.shiro.override.MyModularRealmAuthenticator;
import wst.st.site.shiro.override.MySessionManager;
import wst.st.site.shiro.override.MyShiroSessionListener;
import wst.st.site.shiro.realm.AuthorizationRealm;
import wst.st.site.shiro.realm.UserPasswordRealm;
import wst.st.site.shiro.realm.UserPhoneRealm;
import wst.st.site.shiro.realm.filter.AccessFormAuthenticationFilter;
import wst.st.site.shiro.realm.filter.KickoutAccessControlFilter;
/**
 * shiro配置
 * @author wst
 *
 */
@Configuration
@ConfigurationProperties(prefix = "shiro")
public class ShiroConfiguration {
	private static Logger log = LoggerFactory.getLogger(ShiroConfiguration.class);
	@Value("${spring.redis.host}")
	private String redisHost;
	@Value("${spring.redis.port}")
	private int redisPort;
	@Value("${spring.redis.password}")
	private String redisPassword;
	/**
	 * 匿名请求过滤路径
	 */
	private String[] requestFilterUrlAnon;
	
	/**
	 * 请求过滤路径
	 */
	private List<Map<String, String>> requestFilterUrlCustoms;
	
	/**
	 * LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类,
	 * 负责org.apache.shiro.util.Initializable类型bean的生命周期的,初始化和销毁。
	 * 主要是AuthorizingRealm类的子类,以及EhCacheManager类。
	 * 注:此方法需要用static作为修饰词,否则无法通过@Value()注解的方式获取配置文件的值
	 * @author wst 2019412日 下午4:52:18
	 * @return
	 */
	@Bean
	public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
		log.info("ShiroConfiguration.getLifecycleBeanPostProcessor()");
		return new LifecycleBeanPostProcessor();
	}
	/**
	 * 自定义的Realm管理,主要针对多realm
	 * @author wst 2019412日 下午4:52:34
	 * @return
	 */
	@Bean
	public MyModularRealmAuthenticator myModularRealmAuthenticator() {
		MyModularRealmAuthenticator customizedModularRealmAuthenticator = new MyModularRealmAuthenticator();
		// 设置realm判断条件
		customizedModularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
		return customizedModularRealmAuthenticator;
	}
	/**
	 * 统一角色授权控制Realm
	 * @author wst 2019412日 下午4:42:48
	 * @return
	 */
	@Bean
	public AuthorizingRealm authorizingRealm() {
		AuthorizationRealm authorizationRealm = new AuthorizationRealm();
		authorizationRealm.setName(LoginType.COMMON.getType());
		return authorizationRealm;
	}
	/**
	 * 密码登录realm
	 * @author wst 2019412日 下午4:52:49
	 * @return
	 */
	@Bean
	public UserPasswordRealm userPasswordRealm() {
		UserPasswordRealm userPasswordRealm = new UserPasswordRealm();
		userPasswordRealm.setName(LoginType.USER_PASSWORD.getType());
		// 自定义的密码校验器
		userPasswordRealm.setCredentialsMatcher(credentialsMatcher());
		return userPasswordRealm;
	}
	/**
	 * 手机号验证码登录realm
	 * @author wst 2019412日 下午4:52:57
	 * @return
	 */
	@Bean
	public UserPhoneRealm userPhoneRealm() {
		UserPhoneRealm userPhoneRealm = new UserPhoneRealm();
		userPhoneRealm.setName(LoginType.USER_PHONE.getType());
		return userPhoneRealm;
	}
	/**
	 * SecurityManager,权限管理,这个类组合了登陆,登出,权限,session的处理,是个比较重要的类
	 * @author wst 2019412日 下午4:53:05
	 * @return
	 */
	@Bean
	public SecurityManager securityManager() {
		log.info("ShiroConfiguration.getDefaultWebSecurityManager()");
		DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
		// 设置realm.
		securityManager.setAuthenticator(myModularRealmAuthenticator());
		List<Realm> realms = new ArrayList<>();
		// 统一角色权限控制realm
		realms.add(authorizingRealm());
		// 用户密码登录realm
		realms.add(userPasswordRealm());
		// 用户手机号验证码登录realm
		realms.add(userPhoneRealm());
		securityManager.setRealms(realms);
		// 用户授权/认证信息Cache, 采用EhCache 缓存
		// securityManager.setCacheManager(ehCacheManager());
		// 注入记住我管理器;
		securityManager.setRememberMeManager(rememberMeManager());
		// 自定义缓存实现 使用redis
		securityManager.setCacheManager(redisCacheManager());
		// 自定义session管理 使用redis
		securityManager.setSessionManager(sessionManager());
		return securityManager;
	}
	/**
	 * ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter
	 * 它主要保存了三项数据,securityManager,filters,filterChainDefinitionManager
	 * @author wst 2019412日 下午4:53:16
	 * @return
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilter() {
		log.info("ShiroConfiguration.shiroFilter()");
		ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
		shiroFilterFactoryBean.setSecurityManager(securityManager());
		// 未授权跳转界面;
		// shiroFilterFactoryBean.setUnauthorizedUrl("/403");
		// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
		// shiroFilterFactoryBean.setLoginUrl("/account/login");
		// 登录成功后要跳转的链接
		// shiroFilterFactoryBean.setSuccessUrl("/index");
		// 过滤器
		Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
		
		// 可以匿名请求的url 静态资源
		if(requestFilterUrlAnon != null){
			for(int i = 0; i < requestFilterUrlAnon.length; i ++){
				log.info("anon --> {}", requestFilterUrlAnon[i]);
				filterChainDefinitionMap.put(requestFilterUrlAnon[i], "anon"); 
			}
		}
		
		// 需过滤的url
		if(requestFilterUrlCustoms != null){
			for(int i = 0; i < requestFilterUrlCustoms.size(); i ++){
				log.info(requestFilterUrlCustoms.get(i).get("filter") + " --> {}", requestFilterUrlCustoms.get(i).get("url"));
				filterChainDefinitionMap.put(requestFilterUrlCustoms.get(i).get("url"), requestFilterUrlCustoms.get(i).get("filter")); 
			}
		}
		
		// filterChainDefinitionMap.put("/account/logout", "logout"); 
		// 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
		// filterChainDefinitionMap.put("/**", "authc");
		// 加载shiroFilter权限控制规则
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
		// 自定义过滤器
		Map<String, Filter> filter = shiroFilterFactoryBean.getFilters();
		// 限制同一帐号同时在线的个数。
		filter.put("kickout", kickoutSessionControlFilter());
		// 登陆验证
		filter.put("authc", new AccessFormAuthenticationFilter());
		// 此处需要添加一个kickout,上面添加的自定义拦截器才能生效
		// filterChainDefinitionMap.put("/account/login", "kickout");// 表示需要认证才可以访问
		return shiroFilterFactoryBean;
		// rest: 例如/admins/user/**=rest[user],根据请求的方法,相当于/admins/user/**=perms[user:method],其中method为post,get,delete等。
		// port:例如/admins/user/**=port[8081],当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
		// perms:例如/admins/user/**=perms[user:add:*],perms参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
		// roles:例如/admins/user/**=roles[admin],参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如/admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
		// anon: 例如/admins/**=anon 没有参数,表示可以匿名使用。
		// authc: 例如/admins/user/**=authc表示需要认证才能使用,没有参数。
		// authcBasic:例如/admins/user/**=authcBasic没有参数表示httpBasic认证。
		// ssl: 例如/admins/user/**=ssl没有参数,表示安全的url请求,协议为https。
		// user: 例如/admins/user/**=user没有参数表示必须存在用户,当登入操作时不做检查。
		//
		// 这些过滤器分为两组,一组是认证过滤器,一组是授权过滤器。其中anon,authcBasic,auchc,user是第一组,perms,roles,ssl,rest,port是第二组。
		//
		// 注释支持
		// @RequiresAuthentication 验证用户是否登录,等同于方法subject.isAuthenticated()
		// 结果为true时;
		// @RequiresUser
		// 验证用户是否被记忆,user有两种含义:一种是成功登录的(subject.isAuthenticated()结果为true)另外一种是被记忆的(subject.isRemembered()结果为true);
		// @RequiresGuest 验证是否为匿名请求;
		// @RequiresRoles 必须要有角色;
		// @RequiresPermissions 必须要有权限;
		// 1、一个URL可以配置多个 Filter,使用逗号分隔
		// 2、当设置多个过滤器时,全部验证通过,才视为通过
		// 3、部分过滤器可指定参数,如 perms,roles
	}
	/**
	 * 自定义密码校验
	 * @author wst 2019412日 上午11:46:06
	 * @return
	 */
	@Bean
	public MyCredentialsMatcher credentialsMatcher() {
		return new MyCredentialsMatcher();
	}
	/**
	 * 授权所用配置
	 * 
	 * @author wst 2019412日 下午4:53:56
	 * @return
	 */
	@Bean
	@DependsOn({ "lifecycleBeanPostProcessor" })
	public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
		DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
		defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
		return defaultAdvisorAutoProxyCreator;
	}
	/**
	 * 开启Shiro的注解(@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
	 * 使授权注解起作用不如不想配置可以在pom文件中加入 <dependency>
	 * <groupId>org.springframework.boot</groupId>
	 * <artifactId>spring-boot-starter-aop</artifactId> </dependency>
	 * 
	 * @author wst 2019412日 下午4:54:06
	 * @return
	 */
	@Bean
	public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
		log.info("ShiroConfiguration.authorizationAttributeSourceAdvisor()");
		AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
		authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
		return authorizationAttributeSourceAdvisor;
	}
	/**
	 * 限制同一账号登录同时登录人数控制
	 * @author wst 2019412日 下午4:54:15
	 * @return
	 */
	@Bean
	public KickoutAccessControlFilter kickoutSessionControlFilter() {
		KickoutAccessControlFilter kickoutSessionControlFilter = new KickoutAccessControlFilter();
		kickoutSessionControlFilter.setCache(redisCacheManager());
		kickoutSessionControlFilter.setSessionManager(sessionManager());
		kickoutSessionControlFilter.setKickoutAfter(false);
		kickoutSessionControlFilter.setMaxSession(SiteWebConst.Shiro.Config.Login_max_session);
		kickoutSessionControlFilter.setKickoutUrl(SiteWebConst.Shiro.Config.Login_kickout_url);
		return kickoutSessionControlFilter;
	}
	/**
	 * EhCacheManager 缓存管理 ehCache实现
	 * @author wst 2019412日 下午4:54:23
	 * @return
	 */
	@Bean
	@DependsOn("lifecycleBeanPostProcessor")
	public EhCacheManager ehCacheManager() {
		log.info("ShiroConfiguration.ehCacheManager()");
		EhCacheManager cacheManager = new EhCacheManager();
		cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");
		return cacheManager;
	}
	/**
	 * 配置RedisCacheManager 缓存管理
	 * @author wst 2019412日 下午4:54:32
	 * @return
	 */
	public RedisCacheManager redisCacheManager() {
		RedisCacheManager redisCacheManager = new RedisCacheManager();
		redisCacheManager.setRedisManager(redisManager());
		redisCacheManager.setPrincipalIdFieldName("id");
		redisCacheManager.setKeyPrefix(SiteWebConst.Shiro.Key.Shiro_redis_cache_key_prefix); // 设置前缀
		return redisCacheManager;
	}
	/**
	 * RedisSessionDAO shiro sessionDao层的实现
	 * @author wst 2019412日 下午4:54:42
	 * @return
	 */
	@Bean
	@DependsOn("lifecycleBeanPostProcessor")
	public RedisSessionDAO redisSessionDAO() {
		RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
		redisSessionDAO.setRedisManager(redisManager());
		redisSessionDAO.setKeyPrefix(SiteWebConst.Shiro.Key.Shiro_redis_session_key_prefix);
		return redisSessionDAO;
	}
	/**
	 * SessionManager
	 * @author wst 2019412日 下午4:54:52
	 * @return
	 */
	@Bean
	@DependsOn("lifecycleBeanPostProcessor")
	public MySessionManager sessionManager() {
		MySessionManager sessionManager = new MySessionManager();
		
		Collection<SessionListener> listeners = new ArrayList<>();
		
		// 保存sessionId的cookie
		SimpleCookie sessionIdCookie = new SimpleCookie();
		sessionIdCookie.setMaxAge(SiteWebConst.Cookies.Config.Time_out);
		sessionIdCookie.setName(SiteWebConst.Cookies.Key.Shiro_session_id_cookie);
		sessionIdCookie.setPath("/");
		sessionIdCookie.setHttpOnly(true);
		
		sessionManager.setSessionIdCookie(sessionIdCookie);
		
        listeners.add(new MyShiroSessionListener());
        sessionManager.setSessionListeners(listeners);
		sessionManager.setSessionDAO(redisSessionDAO());
		
		//url中是否显示session Id
		sessionManager.setSessionIdUrlRewritingEnabled(false);
		
		// 删除失效的session
		sessionManager.setDeleteInvalidSessions(true);
		// 是否在cookie中获取sessionId
        sessionManager.setSessionIdCookieEnabled(true);
		// session 有效时间
		sessionManager.setGlobalSessionTimeout(SiteWebConst.Shiro.Config.Session_time_out); // 毫秒
		
		return sessionManager;
	}
	/**
	 * 配置shiro-redis
	 * @author wst 2019412日 下午4:55:00
	 * @return
	 */
	public RedisManager redisManager() {
		RedisManager redisManager = new RedisManager();
		redisManager.setHost(redisHost);
		redisManager.setPort(redisPort);
		redisManager.setTimeout(1800); // 设置过期时间
		redisManager.setPassword(redisPassword);
		return redisManager;
	}
	
	/**
	 * 记住我Cookie
     * remenberMeCookie是一个实现了将用户名保存在客户端的一个cookie,与登陆时的cookie是两个simpleCookie。
     * 登陆时会根据权限去匹配,如是user权限,则不会先去认证模块认证,而是先去搜索cookie中是否有rememberMeCookie,
     * 如果存在该cookie,则可以绕过认证模块,直接寻找授权模块获取角色权限信息。
     * 如果权限是authc,则仍会跳转到登陆页面去进行登陆认证.
	 * @author 2019411日 下午8:50:53
	 * @return
	 */
	@Bean
	public SimpleCookie rememberMeCookie() {
		log.info("ShiroConfiguration.rememberMeCookie()");
		// 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
		SimpleCookie simpleCookie = new SimpleCookie();
		// 记住我cookie生效时间,单位秒
		simpleCookie.setMaxAge(SiteWebConst.Cookies.Config.Time_out);
		simpleCookie.setName("rememberMe");
		simpleCookie.setPath("/");
		simpleCookie.setHttpOnly(true);
		return simpleCookie;
	}
	/**
	 * cookie管理对象;记住我Cookie
	 * @author wst 2019412日 下午4:55:10
	 * @return
	 */
	@Bean
	public CookieRememberMeManager rememberMeManager() {
		log.info("ShiroConfiguration.rememberMeManager()");
		CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
		cookieRememberMeManager.setCookie(rememberMeCookie());
		cookieRememberMeManager.setCipherKey(org.apache.shiro.codec.Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
		return cookieRememberMeManager;
	}
	/**
	 * ShiroDialect,为了在thymeleaf里使用shiro的标签的bean
	 * 
	 * @author wst 2019412日 下午4:55:16
	 * @return
	 */
	@Bean
	public ShiroDialect shiroDialect() {
		log.info("ShiroConfiguration.shiroDialect()");
		return new ShiroDialect();
	}
	
	public String[] getRequestFilterUrlAnon() {
		return requestFilterUrlAnon;
	}
	public void setRequestFilterUrlAnon(String[] requestFilterUrlAnon) {
		this.requestFilterUrlAnon = requestFilterUrlAnon;
	}
	public List<Map<String, String>> getRequestFilterUrlCustoms() {
		return requestFilterUrlCustoms;
	}
	public void setRequestFilterUrlCustoms(List<Map<String, String>> requestFilterUrlCustoms) {
		this.requestFilterUrlCustoms = requestFilterUrlCustoms;
	}
}
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wst.st.site.constant.enums.ResponseCodeEnum;
import wst.st.site.constant.message.SiteResponseMessage;
import wst.st.site.data.entity.SiteAccount;
import wst.st.site.exception.SiteShiroAuthenticationException;
import wst.st.site.shiro.ShiroToken;
import wst.st.site.tools.MD5Util;
 
/**
 * 自定义密码校验
 * @author wst
 *
 */
public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
 
	Logger log = LoggerFactory.getLogger(MyCredentialsMatcher.class);
			
	/**
	 * 所需要的信息如何得到可以在wst.st.site.shiro.realm.UserPasswordRealm类中找到
	 */
	@Override
	public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
		ShiroToken stoken = (ShiroToken) token;
		log.info("LoginType --------------------> {}", stoken.getLoginType().getType());
		
		// 根据不同realm使用不同的校验方式
		switch(stoken.getLoginType()){
			case USER_PHONE:
				break;
			case USER_PASSWORD:
				// 获得用户输入的密码:(可以采用加盐(salt)的方式去检验)
				String inPassword = new String(stoken.getPassword());
				// 获得数据库中的密码
				SiteAccount account = (SiteAccount) info.getPrincipals().getPrimaryPrincipal();
				String dbPassword = (String) info.getCredentials();
				// 进行密码的比对
				// 如果是手机登录,传递过来的是已经加密的验证码,只需要把用户输入的验证码加密进行校验即可
				String md5pwd = MD5Util.md5ByDynamicSalt2(inPassword, account.getSalt() + account.getStr()); // MD5加盐加密
				if(!dbPassword.equals(md5pwd)){
					throw new SiteShiroAuthenticationException(SiteResponseMessage.Auth.USERNAME_PASSWORD_ERROR);
				}
				break;
			case COMMON:
			default:
				throw new SiteShiroAuthenticationException(ResponseCodeEnum.SHIRO_ERROR.getDesc());
		}
		return Boolean.TRUE;
	}
}
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 wst.st.site.shiro.LoginType;
import wst.st.site.shiro.ShiroToken;
import java.util.Collection;
import java.util.HashMap;
/**
 * 自定义多realm登录策略
 * @author wst
 *
 */
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
	@Override
	protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)throws AuthenticationException {
		// 判断getRealms()是否返回为空
		assertRealmsConfigured();
		// 所有Realm
		Collection<Realm> realms = getRealms();
		// 登录类型对应的所有Realm
		HashMap<String, Realm> realmHashMap = new HashMap<>(realms.size());
		for (Realm realm : realms) {
			realmHashMap.put(realm.getName(), realm);
		}
		ShiroToken token = (ShiroToken) authenticationToken;
		// 登录类型
		LoginType loginType = token.getLoginType();
		if (realmHashMap.get(loginType.getType()) != null) {
			return doSingleRealmAuthentication(realmHashMap.get(loginType.getType()), token);
		} else {
			return doMultiRealmAuthentication(realms, token);
		}
	}
}
import java.io.Serializable;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
//import org.apache.shiro.session.Session;
//import org.apache.shiro.session.UnknownSessionException;
//import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
//import org.apache.shiro.web.session.mgt.WebSessionKey;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import wst.st.site.constant.constant.SiteWebConst;
/**
 * 自定义sessionId获取
 * Shiro的sessionId只有在调用Session session = SecurityUtils.getSubject().getSession(); 的时候才会生成sessionId
 * 然后通过写cookie的方式写到浏览器
 * @author wst
 *
 */
public class MySessionManager extends DefaultWebSessionManager {
	 
	private Logger log = LoggerFactory.getLogger(MySessionManager.class);
	
    private static final String AUTHORIZATION = SiteWebConst.Shiro.Config.SessionId_authorization;
 
    private static final String REFERENCED_SESSION_ID_SOURCE = SiteWebConst.Shiro.Config.Referenced_session_id_source;
 
    public MySessionManager() {
        super();
        setGlobalSessionTimeout(SiteWebConst.Shiro.Config.Session_time_out);
    }
 
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
    	// ajax请求  获取请求头,或者请求参数中的Token
    	String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        // String id = StringUtils.isEmpty(WebUtils.toHttp(request).getHeader(AUTHORIZATION)) ? request.getParameter(AUTHORIZATION) : WebUtils.toHttp(request).getHeader(AUTHORIZATION);
    	log.info("sessionId ----------> {}", sessionId);
        // 如果请求头中有 Authorization 则其值为sessionId
        if (!StringUtils.isEmpty(sessionId) && !"undefined".equalsIgnoreCase(sessionId)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        } else {
            // 否则按默认规则从cookie取sessionId
        	sessionId = (String) super.getSessionId(request, response);
        	return sessionId;
        }
    }
    
    /**
     * 获取session 优化单次请求需要多次访问redis的问题
     *
     * @param sessionKey
     * @return
     * @throws UnknownSessionException
     */
//    @Override
//    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
//        Serializable sessionId = getSessionId(sessionKey);
// 
//        ServletRequest request = null;
//        if (sessionKey instanceof WebSessionKey) {
//            request = ((WebSessionKey) sessionKey).getServletRequest();
//        }
// 
//        if (request != null && null != sessionId) {
//            Object sessionObj = request.getAttribute(sessionId.toString());
//            if (sessionObj != null) {
//                return (Session) sessionObj;
//            }
//        }
// 
//        Session session = super.retrieveSession(sessionKey);
//        if (request != null && null != sessionId) {
//            request.setAttribute(sessionId.toString(), session);
//        }
//        return session;
//    }
}
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
public class MyShiroSessionListener implements SessionListener{
	private final AtomicInteger sessionCount = new AtomicInteger(0);
	
	@Override
	public void onStart(Session session) {
		sessionCount.incrementAndGet();
	}
	@Override
	public void onStop(Session session) {
		sessionCount.decrementAndGet();
		
	}
	@Override
	public void onExpiration(Session session) {
		sessionCount.decrementAndGet();
	}
}
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wst.st.site.clazz.response.SiteLoginUserInfor;
/**
 * 统一角色授权控制Realm
 * @author wst
 *
 */
public class AuthorizationRealm extends AuthorizingRealm {
	private static Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);
    /**
	 * 设置realm的名称
	 */
    @Override
    public String getName() {
        return LoginType.COMMON.getType();
    }
    
    /**
     * 授权用户权限
     * 只有在成功登录后,当需要检测用户权限的时候才会调用此方法,如方法标注注解@RequiresPermissions("user:list")@RequiresRoles("guest")
     * Shiro 的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
     * 当访问到页面的时候,链接配置了相应的权限或者 Shiro 标签才会执行此方法否则不会执行。
     * 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回 null 即可。
     * 在这个方法中主要是使用类:SimpleAuthorizationInfo进行角色的添加和权限的添加。
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    	log.info("##################执行Shiro权限认证##################");
        Object principal = principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        if (principal instanceof SiteLoginUserInfor) {
        	SiteLoginUserInfor account = (SiteLoginUserInfor) principal;
            if(account != null){
            	info.addStringPermission("设置权限");
            	info.addRole("设置角色");
            }
        }
        return info;
    }
    /**
     * 认证信息.(身份验证) : Authentication 是用来验证用户身份
     *
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException{
        return null;
    }
}
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.extern.slf4j.Slf4j;
import wst.st.site.clazz.response.ServerResponse;
import wst.st.site.clazz.response.SiteLoginUserInfor;
import wst.st.site.constant.constant.RedisKeyConstant;
import wst.st.site.constant.message.SiteResponseMessage;
import wst.st.site.data.dao.StSiteAccountMapper;
import wst.st.site.exception.SiteShiroAuthenticationException;
import wst.st.site.redis.Jedis.RedisDatabaseConstants;
import wst.st.site.redis.Jedis.RedisUtil;
import wst.st.site.service.IUserInfoService;
import wst.st.site.shiro.LoginType;
import wst.st.site.shiro.ShiroToken;
import wst.st.site.tools.StringUtils;
/**
 * 用户密码登录realm
 */
@Slf4j
public class UserPasswordRealm extends AuthorizingRealm {
	
	private static Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);
	
	@Autowired
	private StSiteAccountMapper stSiteAccountMapper;
	@Autowired
	private IUserInfoService userInfoService;
	@Autowired
	private RedisUtil redis;
	
     /**
	 * 设置realm的名称
	 */
    @Override
    public String getName() {
        return LoginType.USER_PASSWORD.getType();
    }
    @Override
    public boolean supports(AuthenticationToken token) {
        if (token instanceof ShiroToken) {
            return ((ShiroToken) token).getLoginType() == LoginType.USER_PASSWORD;
        } else {
            return false;
        }
    }
    @Override
    public void setAuthorizationCacheName(String authorizationCacheName) {
        super.setAuthorizationCacheName(authorizationCacheName);
    }
    @Override
    protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }
    /**
     * 验证用户身份
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    	log.info("##################执行Shiro身份认证##################");
        ShiroToken token = (ShiroToken) authenticationToken;
       
        // 获取shiro的Session 获取session中保存的验证码
//        Session session = SecurityUtils.getSubject().getSession();
//        String severAuthCode = (String) session.getAttribute(SiteWebConst.Shiro.Key.Auth_code_session_key);
        // 获取保存在redis中的验证码
        String severAuthCode = redis.get(RedisKeyConstant.getAuth_code_key(token.getRequest()), RedisDatabaseConstants.DATEBASE_1);
        
        // 用户输入的验证码
        String clientAuthCode = token.getCode();
        
        if(StringUtils.isBlank(clientAuthCode)) {
            throw new SiteShiroAuthenticationException(SiteResponseMessage.Auth.VERIFICATION_CODE_ERROR);
        }
        
        if(StringUtils.isBlank(clientAuthCode) || !clientAuthCode.equalsIgnoreCase(severAuthCode)) {
            throw new SiteShiroAuthenticationException(SiteResponseMessage.Auth.VERIFICATION_CODE_ERROR);
        }
        
        // 验证成功后删除session中保存的验证码, 也可以不删除,时间到了自动删除
//        session.remove?Attribute(SiteWebConst.Shiro.Key.Auth_code_session_key);
        redis.del(RedisDatabaseConstants.DATEBASE_1, RedisKeyConstant.getAuth_code_key(token.getRequest()));
        
        // 查询数据库是否存在
        // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
        SiteLoginUserInfor account = stSiteAccountMapper.getAccountByAccount(token.getUsername());
       
        if(account == null){
            throw new SiteShiroAuthenticationException(SiteResponseMessage.Auth.USERNAME_NOT_EXISTS);
        }
        
        // 用户为禁用状态
        if (account.getStstus() != 1) {
        	throw new SiteShiroAuthenticationException(SiteResponseMessage.Auth.USERNAME_FORBIDDEN);
        }
        
        // 获取昵称
		ServerResponse<String> nicknameData = userInfoService.getSiteUserNickname(account.getUserId());
		if(nicknameData.getStatus() == 0){
			account.setNickname(nicknameData.getData());
		}
		
		// 设置sessionId
		account.setSessionId(SecurityUtils.getSubject().getSession().getId().toString());
     			
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
        		account, //用户
        		account.getPassword(), //密码
                getName()  //realm name
        );
        return authenticationInfo;
    }
    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }
}
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.extern.slf4j.Slf4j;
import wst.st.site.clazz.response.ServerResponse;
import wst.st.site.clazz.response.SiteLoginUserInfor;
import wst.st.site.constant.constant.SiteWebConst;
import wst.st.site.constant.message.SiteResponseMessage;
import wst.st.site.data.dao.StSiteAccountMapper;
import wst.st.site.exception.SiteShiroAuthenticationException;
import wst.st.site.service.IUserInfoService;
import wst.st.site.shiro.LoginType;
import wst.st.site.shiro.ShiroToken;
import wst.st.site.tools.MD5Util;
/**
 * 手机验证码登录realm
 */
@Slf4j
public class UserPhoneRealm extends AuthorizingRealm {
	
	private static Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);
	@Autowired
	private StSiteAccountMapper stSiteAccountMapper;
	@Autowired
	private IUserInfoService userInfoService;
	
     /**
	 * 设置realm的名称
	 */
    @Override
    public String getName() {
        return LoginType.USER_PHONE.getType();
    }
    @Override
    public boolean supports(AuthenticationToken token) {
        if (token instanceof ShiroToken) {
            return ((ShiroToken) token).getLoginType() == LoginType.USER_PHONE;
        } else {
            return false;
        }
    }
    @Override
    public void setAuthorizationCacheName(String authorizationCacheName) {
        super.setAuthorizationCacheName(authorizationCacheName);
    }
    @Override
    protected void clearCachedAuthorizationInfo(PrincipalCollection principals) {
        super.clearCachedAuthorizationInfo(principals);
    }
    /**
     * 认证信息.(身份验证) : Authentication 是用来验证用户身份
     *
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
    	log.info("##################执行Shiro身份认证##################");
        ShiroToken token = (ShiroToken) authcToken;
       
        // session 保存的验证码
        String severAuthCode = (String) SecurityUtils.getSubject().getSession().getAttribute(SiteWebConst.Shiro.Key.Auth_code_session_key);
        
        // 用户输入的验证码
        String clientAuthCode = token.getCode();
       
        if(StringUtils.isBlank(clientAuthCode)) {
            throw new SiteShiroAuthenticationException("验证码不能为空!");
        }
       
        if(!clientAuthCode.equalsIgnoreCase(severAuthCode)) {
            throw new SiteShiroAuthenticationException("验证码错误,请重新输入!");
        }
        // 查询数据库是否存在
        // 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
        // MyCredentialsMatcher里进行了账号密码登录的校验,判断的用户输入的密码是要加密的,而这里传递过去的是验证码,
        // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        SiteLoginUserInfor account = stSiteAccountMapper.getAccountByAccount(token.getUsername());
        
        // 用户名不存在
        if(account == null){
            throw new SiteShiroAuthenticationException(SiteResponseMessage.Auth.USERNAME_NOT_EXISTS);
        }
       
        // 用户名为禁用状态
        if (account.getStstus() != 1) {
        	throw new SiteShiroAuthenticationException(SiteResponseMessage.Auth.USERNAME_FORBIDDEN);
        }
        
        // 获取昵称
 		ServerResponse<String> nicknameData = userInfoService.getSiteUserNickname(account.getUserId());
 		if(nicknameData.getStatus() == 0){
 			account.setNickname(nicknameData.getData());
 		}
 		
 		// 设置sessionId
 		account.setSessionId(SecurityUtils.getSubject().getSession().getId().toString());
     		
        // 因此这里把验证码加密一遍, 然后在MyCredentialsMatcher与加密后的用户输入的验证码进行校验
        String md5AuthCode = MD5Util.md5ByDynamicSalt2(severAuthCode, account.getSalt() + account.getStr()); // MD5加盐加密
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
        		account, //用户
        		md5AuthCode, // 验证码
                getName()  //realm name
        );
        return authenticationInfo;
    }
    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
}
import java.io.PrintWriter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import wst.st.site.clazz.response.ServerResponse;
import wst.st.site.constant.constant.SiteWebConst;
import wst.st.site.constant.message.SiteResponseMessage;
import wst.st.site.tools.JsonUtil;
/**
 * 登录认证 对请求进行校验,如果没有通过校验的会跳转到指定的loginUrl(默认/login.jsp,可以自定义)
 * 重写方法是为了返回json而不是默认的重定向
 * @author wst
 *
 */
public class AccessFormAuthenticationFilter extends FormAuthenticationFilter {
	private static final Logger log = LoggerFactory.getLogger(AccessFormAuthenticationFilter.class);
	
	@Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //Always return true if the request's method is OPTIONS
        if (request instanceof HttpServletRequest) {
            if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) {
                return Boolean.TRUE;
            }
        }
        return super.isAccessAllowed(request, response, mappedValue);
    }
	
    /**
     * 未登录或无权限处理
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    	HttpServletRequest req = (HttpServletRequest)request;
    	log.info("未登录或者无权限处理 ---- {}", req.getRequestURI());
    	if(isAjax(request)){
    		
    	}
    	HttpServletResponse resp = (HttpServletResponse) response;
    	// 设置响应头允许跨越响应
    	resp.addHeader(SiteWebConst.Common.Header.Access_control_allow_origin, req.getHeader(SiteWebConst.Common.Header.Access_control_allow_origin));
    	resp.addHeader(SiteWebConst.Common.Header.Access_control_allow_credentials, req.getHeader(SiteWebConst.Common.Header.Access_control_allow_credentials));
    	resp.setCharacterEncoding("utf-8");
    	resp.setContentType("application/json; charset=utf-8");
		PrintWriter out = resp.getWriter();
		out.print(JsonUtil.toJson(ServerResponse.createByErrorMessage(SiteResponseMessage.Auth.NOT_LOGIN_OR_EXPIRE)));  //"url已过期"
		out.flush();
		out.close();
        return Boolean.FALSE;
    }
    /**
     * 是否ajax请求
     * @author wst 2019413日 下午4:56:59
     * @param request
     * @return
     */
    private boolean isAjax(ServletRequest request){
        String header = ((HttpServletRequest) request).getHeader("X-Requested-With");
        if("XMLHttpRequest".equalsIgnoreCase(header)){
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }
   
}
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import com.alibaba.fastjson.JSON;
import wst.st.site.clazz.response.SiteLoginUserInfor;
import wst.st.site.constant.constant.SiteWebConst;
/**
 * 自定义拦截器,用于限制用户登录人数
 * @author wst
 *
 */
public class KickoutAccessControlFilter extends AccessControlFilter {
    private String kickoutUrl; //踢出后到的地址
    private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
    private int maxSession = 1; //同一个帐号最大会话数 默认1
 
    private SessionManager sessionManager;
    private Cache<String, Deque<Serializable>> cache;
 
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return false;
    }
 
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        if(!subject.isAuthenticated() && !subject.isRemembered()) {
            //如果没有登录,直接进行之后的流程
            return true;
        }
 
        Session session = subject.getSession();
        // 获取到登录的账号
        // TODO 在eureka微服务中 这里强转时会报错,去除热服务的jar依赖后正常,可能与热部署有关
        SiteLoginUserInfor account = (SiteLoginUserInfor) subject.getPrincipal();
        String username = account.getAccount();
        Serializable sessionId = session.getId();
 
        //读取缓存   没有就存入
        Deque<Serializable> deque = cache.get(username);
 
        //如果此用户没有session队列,也就是还没有登录过,缓存中没有
        //就new一个空队列,不然deque对象为空,会报空指针
        if(deque == null){
            deque = new LinkedList<Serializable>();
        }
 
        //如果队列里没有此sessionId,且用户没有被踢出;放入队列
        if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
            //将sessionId存入队列
            deque.push(sessionId);
            //将用户的sessionId队列缓存
            cache.put(username, deque);
        }
 
        //如果队列里的sessionId数超出最大会话数,开始踢人
        while(deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            if(kickoutAfter) { //如果踢出后者
                kickoutSessionId = deque.removeFirst();
                //踢出后再更新下缓存队列
                cache.put(username, deque);
            } else { //否则踢出前者
                kickoutSessionId = deque.removeLast();
                //踢出后再更新下缓存队列
                cache.put(username, deque);
            }
 
            try {
                //获取被踢出的sessionId的session对象
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if(kickoutSession != null) {
                    //设置会话的kickout属性表示踢出了
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {//ignore exception
 
            }
        }
 
        //如果被踢出了,直接退出,重定向到踢出后的地址
        if (session.getAttribute("kickout") != null) {
            //会话被踢出了
            try {
                //退出登录
                subject.logout();
            } catch (Exception e) { //ignore
            }
            saveRequest(request);
 
            Map<String, String> resultMap = new HashMap<String, String>();
            //判断是不是Ajax请求
            if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
                resultMap.put("user_status", "300");
                resultMap.put("message", "您已经在其他地方登录,请重新登录!");
                //输出json串
                out(response, resultMap);
            }else{
                //重定向
                WebUtils.issueRedirect(request, response, kickoutUrl);
            }
            return false;
        }
        return true;
    }
 
    private void out(ServletResponse hresponse, Map<String, String> resultMap)
            throws IOException {
        try {
            hresponse.setCharacterEncoding("UTF-8");
            PrintWriter out = hresponse.getWriter();
            out.println(JSON.toJSONString(resultMap));
            out.flush();
            out.close();
        } catch (Exception e) {
            System.err.println("KickoutSessionFilter.class 输出JSON异常,可以忽略。");
        }
    }
 
    public String getKickoutUrl() {
        return kickoutUrl;
    }
 
    public void setKickoutUrl(String kickoutUrl) {
        this.kickoutUrl = kickoutUrl;
    }
 
    public boolean isKickoutAfter() {
        return kickoutAfter;
    }
 
    public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }
 
    public int getMaxSession() {
        return maxSession;
    }
 
    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }
 
    public SessionManager getSessionManager() {
        return sessionManager;
    }
 
    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }
 
    public Cache<String, Deque<Serializable>> getCache() {
        return cache;
    }
 
    public void setCache(CacheManager cacheManager) {
        this.cache = cacheManager.getCache(SiteWebConst.Shiro.Key.Shiro_redis_cache_key_prefix);
    }
}
    /**
	 * 登陆
	 * @author wst 20181016日 下午10:17:44
	 * @param account 账号
	 * @param password 密码
	 * @param code 验证码
	 * @return
	 */
	@ResponseBody
	@RequestMapping(value = "login", method = RequestMethod.POST)
	ServerResponse<SiteAccount> login(HttpServletRequest request, String account, String password, String code){
		ShiroToken shiroToken = new ShiroToken(request, LoginType.USER_PASSWORD, account, password, code);
		
		//登录不在该处处理,交由shiro处理
		Subject subject = SecurityUtils.getSubject();
		subject.login(shiroToken);
		if (subject.isAuthenticated()) {
			String sessionId = (String) subject.getSession().getId();
			SiteLoginUserInfor userInfo = (SiteLoginUserInfor) subject.getPrincipal();
			userInfo.setSalt(null);
			userInfo.setStr(null);
			userInfo.setPassword(null);
			userInfo.setSessionId(sessionId);
			
			// 发送日志
			queueProducer.sendLogMessage(SiteWebConst.Common.QueueMessageType.Message_log_opertion, 
					SiteWebConst.Common.OperateAction.login, userInfo.getUserId().toString(), null, null);
			
		    return ServerResponse.createBySuccess(SiteResponseMessage.Auth.LOGIN_SUCCESS, userInfo);
		}else{
		    return ServerResponse.createByErrorMessage(ResponseCodeEnum.SHIRO_ERROR.getDesc());
		}
	}
	
	/**
	 * 退出
	 * @author wst 20181016日 下午10:17:44
	 * @param account 账号
	 * @param password 密码
	 * @return
	 */
	@ResponseBody
	@RequestMapping(value = "logout", method = RequestMethod.POST)
	ServerResponse<String> logOut(){
		SecurityUtils.getSubject().logout();
		return ServerResponse.createBySuccess(SiteResponseMessage.Auth.LOGOUT_SUCCESS, SiteWebConst.Cookies.Key.Shiro_session_id_cookie);
	}
 
     /**
	 * 获取shiro sessionId
	 * @author wst 2019418日 下午1:54:37
	 * @return
	 */
	@ResponseBody
	@RequestMapping(value = "getShiroSessionId", method = RequestMethod.POST)
	ServerResponse<Map<String, Object>> getShiroSessionId(){
		try {
			Subject subject = SecurityUtils.getSubject();
			String sessionId = (String) subject.getSession().getId();
			Map<String, Object> backData = new HashMap<String, Object>();
			Map<String, String> sessionMap = new HashMap<String, String>();
			sessionMap.put("_key", SiteWebConst.Cookies.Key.Shiro_session_id_cookie);
			sessionMap.put("_value", sessionId);
			
			System.out.println( "sessionId -- >" + sessionId);
			SiteWebConst.setCookie(response, "99999", "testCookieAndHeaders");
			Cookie[] cookies = SiteWebConst.getCookieArray(request);
			if(cookies != null){
				for(int i = 0; i < cookies.length; i ++){
					System.out.println( "cookies -- >" + cookies[i].getValue());
				}
			}
			System.out.println( "header -- >" + request.getHeader(SiteWebConst.Shiro.Config.SessionId_authorization));
			
			// 获取登陆的用户信息
			SiteLoginUserInfor user = (SiteLoginUserInfor) subject.getPrincipal();
			
			// 从redis中获取访问数量
			String visit = redis.get(RedisKeyConstant.Site_visit_number_key, RedisDatabaseConstants.DATEBASE_1);
			
			backData.put("sessionMap", sessionMap);
			backData.put("loginMap", user);
			backData.put("visit", visit);
			return ServerResponse.createBySuccess(SiteResponseMessage.Common.GET_DATA_SUCCESS, backData);
		} catch (Exception e) {
			e.printStackTrace();
			return ServerResponse.createByErrorMessage(SiteResponseMessage.Common.GET_DATA_FAIL);
		}
	}
import java.io.Serializable;
import java.util.Date;
public class SiteAccount implements Serializable {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = -5290258498172598335L;
	public SiteAccount(){}
	protected  Integer id;
	protected Integer userId;
	protected String account;
	protected String password;
	protected String salt;
	protected String str;
	protected Integer ststus;
	protected Integer privilege;
	protected Date updateTime;
	protected Date createTime;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public Integer getUserId() {
        return userId;
    }
    public void setUserId(Integer userId) {
        this.userId = userId;
    }
    public String getAccount() {
        return account;
    }
    public void setAccount(String account) {
        this.account = account == null ? null : account.trim();
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password == null ? null : password.trim();
    }
    public String getSalt() {
        return salt;
    }
    public void setSalt(String salt) {
        this.salt = salt == null ? null : salt.trim();
    }
    public String getStr() {
        return str;
    }
    public void setStr(String str) {
        this.str = str == null ? null : str.trim();
    }
    public Integer getStstus() {
        return ststus;
    }
    public void setStstus(Integer ststus) {
        this.ststus = ststus;
    }
    public Integer getPrivilege() {
        return privilege;
    }
    public void setPrivilege(Integer privilege) {
        this.privilege = privilege;
    }
    public Date getUpdateTime() {
        return updateTime;
    }
    public void setUpdateTime(Date updateTime) {
        this.updateTime = updateTime;
    }
    public Date getCreateTime() {
        return createTime;
    }
    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}
import java.io.Serializable;
import wst.st.site.data.entity.SiteAccount;
/**
 * 用户登录成功后的信息
 * @author wst
 *
 */
public class SiteLoginUserInfor extends SiteAccount implements Serializable {
	/**
	 * 
	 */
	private static final long serialVersionUID = -7029736560050494826L;
	/**
	 * sessionId
	 */
	private String sessionId;
	
	/**
	 * 用户昵称
	 */
	private String nickname;
	public String getSessionId() {
		return sessionId;
	}
	public void setSessionId(String sessionId) {
		this.sessionId = sessionId;
	}
	public String getNickname() {
		return nickname;
	}
	public void setNickname(String nickname) {
		this.nickname = nickname;
	}
	
	
	// TODO 作为保存到shiro 里的Principal 继承的父类和子类都需要实现Serializable, 以及子类需要有如下方法,否在报错
	// authCacheKey or id
	// We need a field to identify this Cache Object in Redis. So you need to defined an id field which you can get unique id to identify this principal. 
	// For example, if you use UserInfo as Principal class, the id field maybe userId, userName, email, etc. For example, getUserId(), getUserName(), getEmail(), etc.
	// Default value is authCacheKey or id, that means your principal object has a method called "getAuthCacheKey()" or "getId()"
	@Override
	public Integer getId() {
		return super.getId();
	}
}
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">
    <diskStore path="java.io.tmpdir"/>
    <!--
       name:缓存名称。
       maxElementsInMemory:缓存最大数目
       maxElementsOnDisk:硬盘最大缓存个数。
       eternal:对象是否永久有效,一但设置了,timeout将不起作用。
       overflowToDisk:是否保存到磁盘,当系统当机时
       timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
       timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
       diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
       diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
       diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
       memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
        clearOnFlush:内存数量最大时是否清除。
         memoryStoreEvictionPolicy:
            Ehcache的三种清空策略;
            FIFO,first in first out,这个是大家最熟的,先进先出。
            LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
            LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
    -->
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
    />
    <!-- 登录记录缓存锁定10分钟 -->
    <cache name="passwordRetryCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>
</ehcache>