系统中用了shiro做权限控制和身份认证(其实身份认证可以用jwt的,这在我以后的博客中会写到)。本来是单一系统。但是现在要做成分布式的。所以就只能用到session共享。其实不用spring-session也能实现session共享,只需要将session存入redis即可。但是spring-session作为现成的框架,把许多底层的东西都已经封装了,不用白不用啊。spring-session通过适配器模式、责任链模式、装饰者模式等对httpsession做了封装。
我是将shiro和spring-session/redis分别建成了两个子项目,然后通过springcloud集成的如下:
对于shiro的项目,首先你应该引入shiro的jar包,并配置好shiro。
maven:
org.apache.shiro
shiro-spring-boot-web-starter
${shiro-spring-boot-web-starter.version}
shiro的配置类,核心的就两个,分别是继承AuthorizingRealm的ShiroRealm和ShiroConfig
package com.lenovoedu.config;
import com.lenovoedu.constants.ShiroConstants;
import com.lenovoedu.model.sys.model.SysUser;
import com.lenovoedu.shiro.service.IPermitService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 项目名称:
* 类名称:ShiroRealm
* 类描述:shiro AuthorizingRealm继承实现
* 创建人:YuanGL
* 创建时间:2019年4月1日16:17:46
* version 2.0
*/
@Component("authorizer")
public class ShiroRealm extends AuthorizingRealm {
private static Logger logger = LoggerFactory.getLogger(ShiroRealm.class);
@Autowired
private IPermitService permitService;
private static HashedCredentialsMatcher hc;
static {
hc = new HashedCredentialsMatcher();
hc.setHashIterations(3);
hc.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
}
public ShiroRealm(){
super(new MemoryConstrainedCacheManager(),hc);
}
/**
* 授权
* @param principals 用户信息
* @return AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
String username = (String)getAvailablePrincipal(principals);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(permitService.getSysUserRolesByEmail(username));
info.setStringPermissions(permitService.getSysUserAuthCodesByEmail(username));
return info;
}
/**
* 验证
* @param token 用户权限信息
* @return AuthenticationInfo
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
SysUser sysUser = permitService.getUserByEmail(usernamePasswordToken.getUsername());
if(sysUser==null){
throw new UnknownAccountException();
}
// 检查用户状态
checkUserStatus(sysUser.getStatus());
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(sysUser.getEmail(),sysUser.getPassword(), super.getName() );
info.setCredentialsSalt(ByteSource.Util.bytes(sysUser.getSalt().toCharArray()));
return info;
}
/**
* 检查用户状态
* 如果用户状态异常,则抛出对应的错误
* @param status 用户状态
* @throws AuthenticationException 用户状态错误信息
*/
private void checkUserStatus(int status)throws AuthenticationException{
switch (status){
case ShiroConstants.ADMIN_STATUS_DISABLED:
throw new DisabledAccountException();
case ShiroConstants.ADMIN_STATUS_LOCKED:
throw new LockedAccountException();
case ShiroConstants.ADMIN_STATUS_DELETE:
throw new UnknownAccountException();
}
}
}
package com.lenovoedu.config;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
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.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.ServletContainerSessionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 项目名称:
* 类名称:ShiroConfig
* 类描述:shiro配置类
* 创建人:YuanGL
* 创建时间:2019年4月1日14:12:36
* version 2.0
*/
@Configuration
public class ShiroConfig {
/**
* ServletContainerSessionManager 类中有一个方法是isServletContainerSessions(),返回的是true.
* DefaultWebSessionManager类中有一个方法是isServletContainerSessions(),返回是false。
* 因为实现了Spring Session,代理了所有Servlet里的session,所以这里的session一定是Servlet能控制的,否则无法实现Spring session共享。
*
* @return
*/
@Bean
public SessionManager sessionManager(){
ServletContainerSessionManager sessionManager = new
ServletContainerSessionManager();
return sessionManager;
}
@Bean("authenticator")
public SecurityManager securityManager(ShiroRealm shiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm);
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new
DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/css/**", "anon");
chainDefinition.addPathDefinition("/fonts/**", "anon");
chainDefinition.addPathDefinition("/images/**", "anon");
chainDefinition.addPathDefinition("/js/**", "anon");
chainDefinition.addPathDefinition("/themes/**", "anon");
chainDefinition.addPathDefinition("/vendor/**", "anon");
chainDefinition.addPathDefinition("/favicon.ico", "anon");
chainDefinition.addPathDefinition("/login.html", "anon");
chainDefinition.addPathDefinition("/swagger-ui.html", "anon");
chainDefinition.addPathDefinition("/webjars/**", "anon");
chainDefinition.addPathDefinition("/v2/**", "anon");
chainDefinition.addPathDefinition("/swagger-resources/**", "anon");
chainDefinition.addPathDefinition("/v2.0/lls/sys/login", "anon");
chainDefinition.addPathDefinition("/v2.0/lls/sys/logout", "anon");
chainDefinition.addPathDefinition("/v2.0/lls/sys/isLogin", "anon");
chainDefinition.addPathDefinition("/verifyCode", "anon");
// chainDefinition.addPathDefinition("/**", "anon");
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
/**
* LifecycleBeanPostProcessor将Initializable和Destroyable的实现类统一在其内部
* 自动分别调用了Initializable.init()和Destroyable.destroy()方法,从而达到管理shiro bean生命周期的目的。
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor
authorizationAttributeSourceAdvisor(ShiroRealm shiroRealm) {
AuthorizationAttributeSourceAdvisor advisor = new
AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager(shiroRealm));
return advisor;
}
}
此时,shiro的就已经配置完毕了!
接下来针对session-redis工程,要配置redis和spring-session
maven如下:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-redis
1.4.7.RELEASE
org.springframework.session
spring-session-data-redis
配置好redis,配置类代码如下:
package com.lenovoedu.base.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 项目名称:
* 类名称:RedisCache
* 类描述:redis缓存实现
* 创建人:YuanGL
* 创建时间:2019年3月25日11:02:21
* version 2.0
*/
@Component
public class RedisCache extends AbstractCache {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
private void init() {
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
}
public void setMap(String key, Map obj, final long timeout, final TimeUnit unit) {
init();
redisTemplate.opsForHash().putAll(this.getPrefixKey(key), obj);
redisTemplate.expire(this.getPrefixKey(key), timeout, unit);
}
public Map getMap(String key) {
init();
return redisTemplate.opsForHash().entries(this.getPrefixKey(key));
}
public void updateMap(String key, Map obj, final long timeout, final TimeUnit unit) {
setMap(key, obj, timeout, unit);
}
@Override
public void set(String key, String value) {
stringRedisTemplate.opsForValue().set(this.getPrefixKey(key), value);
}
@Override
public void set(String key, String value, long timeout) {
stringRedisTemplate.opsForValue().set(this.getPrefixKey(key), value, timeout);
}
@Override
public void set(String key, String value, long timeout, TimeUnit unit) {
stringRedisTemplate.opsForValue().set(this.getPrefixKey(key), value, timeout, unit);
}
@Override
public boolean setIfAbsent(String key, String value) {
return stringRedisTemplate.opsForValue().setIfAbsent(this.getPrefixKey(key), value);
}
@Override
public String get(String key) {
return stringRedisTemplate.opsForValue().get(this.getPrefixKey(key));
}
@Override
public String getSub(String key, long start, long end) {
return stringRedisTemplate.opsForValue().get(this.getPrefixKey(key), start, end);
}
@Override
public String getAndSet(String key, String value) {
return stringRedisTemplate.opsForValue().getAndSet(this.getPrefixKey(key), value);
}
@Override
public boolean exists(String key) {
return stringRedisTemplate.hasKey(this.getPrefixKey(key));
}
@Override
public void delete(String key) {
stringRedisTemplate.delete(this.getPrefixKey(key));
}
@Override
public void delete(List keys) {
if(CollectionUtils.isEmpty(keys)){
return;
}
for (String key:
keys) {
stringRedisTemplate.delete(this.getPrefixKey(key));
}
}
@Override
public Long incrementForInteger(String key) {
return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), 1L);
}
@Override
public Double incrementForDouble(String key) {
return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), 1.0);
}
@Override
public Long increment(String key, Long value) {
return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), value);
}
@Override
public Double increment(String key, Double value) {
return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), value);
}
@Override
public Boolean expire(String key, Date endTime) {
return stringRedisTemplate.expireAt(this.getPrefixKey(key), endTime);
}
@Override
public Boolean expire(String key, Long timeout, TimeUnit unit) {
return stringRedisTemplate.expire(this.getPrefixKey(key), timeout, unit);
}
@Override
public Set keys(String keyPrefix) {
return stringRedisTemplate.keys(this.getPrefixKey(keyPrefix))
.parallelStream()
.map(key->key.replace(getPrefix()+":", ""))
.collect(Collectors.toSet());
}
@Override
public Long decrement(String key) {
return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), -1L);
}
@Override
public Long decrement(String key, Long value) {
return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), -value);
}
@Override
public Double decrementForDouble(String key) {
return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), -1.0);
}
@Override
public Double decrementForDouble(String key, Double value) {
return stringRedisTemplate.opsForValue().increment(this.getPrefixKey(key), -value);
}
public Long leftPushAll(String key, Set values){
return stringRedisTemplate.opsForList().leftPushAll(this.getPrefixKey(key), values);
}
public Long leftPush(String key, String value){
return stringRedisTemplate.opsForList().leftPush(this.getPrefixKey(key), value);
}
public String leftPop(String key){
return stringRedisTemplate.opsForList().leftPop(this.getPrefixKey(key));
}
public Long rightPushAll(String key, Set values){
return stringRedisTemplate.opsForList().rightPushAll(this.getPrefixKey(key), values);
}
public Long rightPush(String key, String value){
return stringRedisTemplate.opsForList().rightPush(this.getPrefixKey(key), value);
}
public String rightPop(String key){
return stringRedisTemplate.opsForList().rightPop(this.getPrefixKey(key));
}
}
package com.lenovoedu.base.redis;
import org.springframework.beans.factory.annotation.Value;
/**
* 项目名称:lenovolls2.0
* 类名称:AbstractCache
* 类描述:
* 创建人:YuanGL
* 创建时间:2019年3月25日11:03:00
* version 2.0
*/
public abstract class AbstractCache implements Cache {
@Value("${spring.redis.prefix}")
private String prefix;
public String getPrefixKey(String key){
return prefix + ":" + key;
}
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
}
package com.lenovoedu.base.redis;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 项目名称:lenovolls2.0
* 类名称:Cache
* 类描述:缓存接口类
* 创建人:YuanGL
* 创建时间:2019-3-25 11:03:16
* version 2.0
*/
public interface Cache {
/**
* 设置cache
* @param key 缓存key
* @param value 值
*/
void set(String key, String value);
/**
* 设置cache
* 指定时间后过期
* @param key 缓存key
* @param value 值
* @param timeout 过期时间,单位毫秒
*/
void set(String key, String value, long timeout);
/**
* 设置cache
* @param key 缓存key
* @param value 值
* @param timeout 过期时间
* @param unit 时间单位
*/
void set(String key, String value, long timeout, TimeUnit unit);
/**
* 设置cache,如果该cache不存在的话
* @param key 缓存key
* @param value 值
*/
boolean setIfAbsent(String key, String value);
/**
* 获取cache
* @param key 缓存key
* @return String
*/
String get(String key);
/**
* 获取cache并截取
* key: a value: abcd start: 0 end: 2 result: abc
* @param key 缓存key
* @param start 开始
* @param end 结束
* @return 被截取后的字符串
*/
String getSub(String key, long start, long end);
/**
* 获取cache并设置新的值
* @param key 缓存key
* @param value 设置的值
* @return 设置前的值
*/
String getAndSet(String key, String value);
/**
* 判断cache是否存在
* @param key 缓存key
* @return true:存在 false:不存在
*/
boolean exists(String key);
/**
* 删除cache
* @param key 缓存key
*/
void delete(String key);
/**
* 删除cache集合
* @param keys 缓存key
*/
void delete(List keys);
/**
* 递增
* 默认步长 1
* @param key 缓存key
* @return 递增后的值
*/
Long incrementForInteger(String key);
/**
* 递增
* 默认步长 1
* @param key 缓存key
* @return 递增后的值
*/
Double incrementForDouble(String key);
/**
* 递增
* @param key 缓存key
* @param value 步长
* @return 递增后的值
*/
Long increment(String key, Long value);
/**
* 递增
* @param key 缓存key
* @param value 步长
* @return 递增后的值
*/
Double increment(String key, Double value);
/**
* 设置失效时间
* @param key 缓存key
* @param endTime 失效时间点
* @return
*/
Boolean expire(String key, Date endTime);
/**
* 设置失效时间
* @param key 缓存key
* @param timeout 时间长度
* @param unit 时间单位
* @return
*/
Boolean expire(String key, Long timeout, TimeUnit unit);
/**
* 获得类似的key集合
* @param keyPrefix *key*,*:通配符
* @return 所有的类似的key
*/
Set keys(String keyPrefix);
/**
* 递减
* 默认步长 -1
* @param key 缓存key
* @return 递减后的值
*/
Long decrement(String key);
/**
* 递减
* @param key 缓存key
* @param value 递减步长
* @return 递减后的值
*/
Long decrement(String key, Long value);
/**
* 递减
* 默认步长 -1.0
* @param key 缓存key
* @return 递减后的值
*/
Double decrementForDouble(String key);
/**
* 递减
* @param key 缓存key
* @param value 递减步长
* @return 递减后的值
*/
Double decrementForDouble(String key, Double value);
}
针对spring-session只需一下两个类:
配置比较简单,主要是添加@EnableRedisHttpSession注解即可,该注解会创建一个名字叫springSessionRepositoryFilter的Spring Bean,其实就是一个Filter,这个Filter负责用Spring Session来替换原先的默认HttpSession实现,同时实现是采用redis管理session的方式。在这个例子中,Spring Session是用Redis来实现的。
package com.lenovoedu.base.session;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* 启用RedisHttpSession功能,并向Spring容器中注册一个RedisConnectionFactory
*
* 对象10分钟(maxInactiveIntervalInSeconds=600)后失效。
*/
@EnableRedisHttpSession(maxInactiveIntervalInSeconds=600)
public class SessionConfig {
@Value("${spring.redis.host:localhost}")
private String host;
@Value("${spring.redis.password:}")
private String password;
@Value("${spring.redis.port:6379}")
private int port;
@Bean
public JedisConnectionFactory connectionFactory() {
JedisConnectionFactory connection = new JedisConnectionFactory();
connection.setHostName(host);
connection.setPassword(password);
connection.setPort(port);
return connection;
}
}
package com.lenovoedu.base.session;
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
public class SessionInitializer extends AbstractHttpSessionApplicationInitializer {
public SessionInitializer () {
super(SessionConfig.class);
}
}
然后在application.yml中配置好redis的信息,就OK了
spring:
redis:
prefix: lls
open: true # 是否开启redis缓存 true开启 false关闭
database: 0
host: 127.0.0.1
port: 6379
password: # 密码(默认为空)
timeout: 6000 # 连接超时时长(毫秒)
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
至此,spring-session和redis、shiro、springboot就已经完全集成完毕。快去测试一下吧!
shiro中的几种session类型,以及之间的区别是什么:https://blog.csdn.net/qq_32347977/article/details/51084480
spring-session具体是怎么实现的,可以参见这篇博客:https://www.cnblogs.com/lxyit/p/9672097.html
spring-session解决session共享好文:https://blog.csdn.net/patrickyoung6625/article/details/45694157
因为项目中也使用了zuul作为网关:
在实际的使用中,我们发现每次请求经过zuul的时候,session就会发生改变,导致后面的请求获取不到session中的内容,该问题疑似是cookie丢失导致的,目前的解决方案是每次请求的时候,前端都将cookie信息带过来,从而获取session里面的信息,如有更好的解决方案,还请不吝赐教。