为了减轻用户访问量增大带来的服务器压力,可以把多个相同的应用部署到多个服务器,然后通过nginx的负载均衡能力将用户请求根据各个应用实际运行情况来决定分发给哪个服务器,这样多个应用就形成了一个集群;不过这样又产生了一个新问题,多个应用之间如何实现session共享呢?
可以通过shiro框架中的会话管理功能配合redis缓存来实现,下面是我做的一个小Demo;
Demo使用的是SpringBoot,pom.xml文件中的依赖如下:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-data-redis
2.1.2.RELEASE
io.lettuce
lettuce-core
redis.clients
jedis
org.apache.shiro
shiro-all
1.3.2
org.crazycake
shiro-redis
3.1.0
Shiro的Realm如下,这里我都简写了授权方法没有,身边验证方法都是代码写死的:
public class MyRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
、 return null; //授权无
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal(); //得到用户名
if(!username.equals("admin")){
throw new UnknownAccountException(); //如果用户名错误
}
String encryptPassword=new Md5Hash("root",username,1).toString(); //加密后密码,此处模拟从数据库取得,加密规则md5 盐是登录名 散列次数1次
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo("admin",encryptPassword,getName());
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return simpleAuthenticationInfo;
}
}
用到的RedisInfo 如下:
public class RedisInfo {
private String host;
private int timeout;
private int port;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
Shiro配置类如下:
@Configuration
public class ShiroConfig {
@Bean //验证密码
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher=new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(1); //此处加密规则要和MyRealm中一致
return hashedCredentialsMatcher;
}
@Bean
public MyRealm myRealm(){
MyRealm myRealm=new MyRealm();
myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myRealm;
}
@Bean
public SimpleCookie simpleCookie(){
SimpleCookie simpleCookie=new SimpleCookie("SHAREJSESSIONID"); //SHARE
simpleCookie.setHttpOnly(true);
simpleCookie.setMaxAge(180000);
simpleCookie.setPath("/");
return simpleCookie;
}
@Bean //会话监听器 不是必需的
public SessionListener sessionListener(){
return new SessionListener();
}
@Bean //Session工厂 不是必需的
public SessionFactory sessionFactory(){
return new MySessionFactory();
}
@Bean(name = "sessionIdGenerator") //会话ID生成器
public SessionIdGenerator sessionIdGenerator(){
return new JavaUuidSessionIdGenerator();
}
@Bean
@ConfigurationProperties(prefix = "spring.redis")
public RedisInfo redisInfo(){
return new RedisInfo();
}
@Bean
public RedisManager redisManager(){
RedisManager redisManager=new RedisManager();
redisManager.setHost(redisInfo().getHost());
redisManager.setPort(redisInfo().getPort());
redisManager.setTimeout(redisInfo().getTimeout());
return redisManager;
}
@Bean
public RedisSessionDAO sessionDAO(){
RedisSessionDAO sessionDAO = new RedisSessionDAO(); // crazycake 实现
sessionDAO.setRedisManager(redisManager());
sessionDAO.setSessionIdGenerator(sessionIdGenerator()); // Session ID 生成器
return sessionDAO;
}
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(redisInfo().getTimeout()*1000);
sessionManager.setSessionListeners(Arrays.asList(sessionListener()));
sessionManager.setSessionFactory(sessionFactory());
sessionManager.setSessionDAO(sessionDAO());
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionValidationSchedulerEnabled(true); //是否开启会话验证器,默认是开启的
sessionManager.setSessionIdCookieEnabled(true);
sessionManager.setSessionIdCookie(simpleCookie());
return sessionManager;
}
@Bean
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setSessionManager(sessionManager());
securityManager.setRealm(myRealm());
return securityManager;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
LinkedHashMap map = new LinkedHashMap<>();
map.put("/testLogin","anon");
map.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
// 配置登录url
shiroFilterFactoryBean.setLoginUrl("/goLogin");
// 配置无权限路径
shiroFilterFactoryBean.setUnauthorizedUrl("/goLogin");
return shiroFilterFactoryBean;
}
@Bean
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new DelegatingFilterProxy("shiroFilter"));
registration.addInitParameter("targetFilterLifecycle", "true");
registration.setEnabled(true);
registration.addUrlPatterns("/*");
return registration;
}
}
Controller如下:
@RestController
public class MyController{
@RequestMapping("/testLogin")
public String testLogin(String username,String password){
String result="模拟登陆成功";
Subject subject= SecurityUtils.getSubject();
UsernamePasswordToken token=new UsernamePasswordToken(username,password);
try {
subject.login(token);
Session session=subject.getSession();
//由于会话通过shiro转给redis管理,此处相当于将用户信息存入redis缓存中
session.setAttribute("username",username);
} catch (UnknownAccountException e) {
e.printStackTrace();
result="用户名不存在";
} catch (IncorrectCredentialsException e){
e.printStackTrace();
result="密码错误";
}
return result;
}
@RequestMapping("/goLogin")
public String goLogin(){
return "请先登录!";
}
@RequestMapping("/welcome")
public String welcome(){
Subject subject=SecurityUtils.getSubject();
Session session=subject.getSession();
return session.getAttribute("username")+",欢迎访问!";
}
}
SpringBoot配置文件如下:
#server
server:
port: 8081
# port:8082
#spring
spring:
application:
name: demo
redis:
jedis:
pool:
min-idle: 5
max-active: 10
max-idle: 10
max-wait: 2000
host: 127.0.0.1
port: 6379
timeout: 1800
分别打成两个jar包,两个jar包唯一区别就是配置文件中的端口不一样,分别为8081和8082;
通过java -jar命令运行两个jar包;
运行访问两个端口的/welcome,由于没有登录都会跳到/goLogin,如下:
此时8081登录,浏览器访问http://localhost:8081/testLogin?username=admin&password=root
此时查看浏览器中的cookie内容和redis的keys,如下:
redis已经保存了session信息
再次访问8082的/welcome,结果如下:
session通过redis实现共享成功!