SpringBoot整合Redis实现Shiro分布式Session共享

前言:

我们知道shiro有一套自身的session管理机制,默认的session是存储在运行jvm内存中的,在单应用服务器中可共享session,但系统若为分布式架构,则不同应用服务器之间无法共享session,要实现不同应用服务器之间共享session,则需要重写SessionManager中的SessionDao,把session存储在缓存中,这里我们采用redis来存储,引用shiro-redis插件已为我们实现了SessionDao的重写,闲话少说上代码:

1.pom.xml配置依赖包



    org.apache.shiro
    shiro-all
    1.4.0
    pom



    com.github.theborakompanioni
    thymeleaf-extras-shiro
    2.0.0



    org.crazycake
    shiro-redis
    2.4.2.1-RELEASE

2.创建系统账户实体类

import java.io.Serializable;

/**
 * 系统帐户
 */
public class Account implements Serializable{
	
    private static final long serialVersionUID = 1L;

    private String pk;//主键
    private String xm;//姓名
    private String username;//用户名
    private String password;//密码
    private String salt;//加密盐
    
    public String getPk() {
	    return pk;
    }
    public void setPk(String pk) {
	    this.pk = pk;
    }
    public String getXm() {
	    return xm;
    }
    public void setXm(String xm) {
	    this.xm = xm;
    }
    public String getUsername() {
	    return username;
    }
    public void setUsername(String username) {
	    this.username = username;
    }
    public String getPassword() {
	    return password;
    }
    public void setPassword(String password) {
	    this.password = password;
    }
    public String getSalt() {
	    return salt;
    }
    public void setSalt(String salt) {
	    this.salt = salt;
    }
    public static long getSerialversionuid() {
	    return serialVersionUID;
    }	

}

3.创建验证帐号密码实体类

import org.apache.shiro.authc.UsernamePasswordToken;

import cn.com.app.bean.Account;

/**
 * 扩展具有Account对象 Token
 */
public class AccountLoginToken extends UsernamePasswordToken {
    private static final long serialVersionUID = 1L;
    private Account account;

    public AccountLoginToken(String username, char[] password, boolean rememberMe, String host, Account account) {
        super(username, password, rememberMe, host);
        this.account = account;
    }

    public AccountLoginToken(String username, String password, Account account) {
        super(username, password);
        this.account = account;
    }

    public Account getAccount() {
        return account;
    }

    public void setAccount(Account account) {
        this.account = account;
    }

}

4.创建ShiroRealm实现用户认证

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

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.AuthorizationException;
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.beans.factory.annotation.Value;

import cn.com.app.bean.Account;
import cn.com.app.service.UserInfoService;

public class MyShiroRealm extends AuthorizingRealm {
   
    @Autowired
    private UserInfoService userInfoService;
    
    /*
     * Authorization 授权(权限操作验证,只有请求设置特定权限时调用此方法)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    	if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }       
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Set roles = new HashSet();
        Set permissions = new HashSet();  
        Account userInfo = (Account) principals.fromRealm(getName()).iterator().next();
        Map paramMap = new HashMap();
        paramMap.put("userid", userInfo.getPk());
        try {
            //根据用户id从数据库中查询用户角色
            List>userRoles = userInfoService.queryRoleByUserid(paramMap);
	    for (Map mapRole : userRoles) {
	        roles.add((String) mapRole.get("code"));
		Map parMap = new HashMap();
		parMap.put("roleid", (String) mapRole.get("roleid"));
		//根据用户id从数据库中查询用户权限
		List>userPermissions = userInfoService.queryPermissionByRoleid(parMap);
		for (Map mapPermission : userPermissions) {
		    permissions.add((String) mapPermission.get("code"));
		}
	    }
	} catch (Exception e) {
	    e.printStackTrace();
	}
        //添加角色
        info.addRoles(roles);
        //添加权限
        info.addStringPermissions(permissions);
        return info;
    }

    /*
     * Authentication 验证(登录认证)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
    	AccountLoginToken userInfoToken = (AccountLoginToken) authcToken;
        String username = userInfoToken.getUsername();     
        SimpleAuthenticationInfo info = null;
        try {
            Map m = new HashMap();
            m.put("username", username);
            Map p = (Map) userInfoService.queryInfoByUsername(m);
            Account userinfo = new Account();
            userinfo.setPk(p.get("pk")==null?"":p.get("pk").toString());
            userinfo.setXm(p.get("xm")==null?"":p.get("xm").toString());                    
            userinfo.setUsername(p.get("username")==null?"":p.get("username").toString());       	       	
            userinfo.setPassword(p.get("password")==null?"":p.get("password").toString());
            userinfo.setSalt(p.get("salt")==null?"":p.get("salt").toString());					
            String password = userinfo.getPassword();
            info = new SimpleAuthenticationInfo(userinfo, password.toCharArray(), getName());
            if(userinfo.getSalt() != null && !"".equals(userinfo.getSalt())){
            	info.setCredentialsSalt(ByteSource.Util.bytes(userinfo.getSalt()));
            }
        } catch (Exception e) {
        	throw new AuthenticationException(e.getMessage(), e);
        }
        return info;
    }

}

5.application.properties配置redis连接参数

# Redis 配置
# Redis数据库索引(默认为0)
spring.redis.database=1
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码
spring.redis.password=123456
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=100
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=5000
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=10
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=5000

6.创建shiro配置类

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.SessionListener;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
 
@Configuration
public class ShiroConfiguration {
	
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.password}")
    private String password;
	
    //将自己的验证方式加入容器
    @Bean
    public MyShiroRealm myShiroRealm(CredentialsMatcher credentialsMatcher) {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        //将自定义的令牌set到了Realm
        myShiroRealm.setCredentialsMatcher(credentialsMatcher);
        return myShiroRealm;
    }
 
    //Filter工厂,设置对应的过滤条件和跳转条件
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
	ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
	// 必须设置SecuritManager
	shiroFilterFactoryBean.setSecurityManager(securityManager);
	// 拦截器
	Map filterChainDefinitionMap = new LinkedHashMap();
	// 配置退出过滤器,其中的具体代码Shiro已经替我们实现了
	//filterChainDefinitionMap.put("/logout", "logout");
 
	//过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 
	//authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
 
	filterChainDefinitionMap.put("/login", "anon"); 	   //不拦截登录请求
	filterChainDefinitionMap.put("/script/**", "anon");    //不拦截js
	filterChainDefinitionMap.put("/css/**", "anon");       //不拦截css
	filterChainDefinitionMap.put("/js/**", "anon");        //不拦截js
	filterChainDefinitionMap.put("/img/**", "anon");       //不拦截图片
	filterChainDefinitionMap.put("/img2/**", "anon");      //不拦截图片
	filterChainDefinitionMap.put("/public/**", "anon");    //不拦截公共资源
	//filterChainDefinitionMap.put("/index", "roles[ADMIN]"); //设置特定角色访问
	filterChainDefinitionMap.put("/**", "authc");

	// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
	shiroFilterFactoryBean.setLoginUrl("/index");
	// 登录成功后要跳转的链接
	//shiroFilterFactoryBean.setSuccessUrl("/index");
	// 未授权界面;
	shiroFilterFactoryBean.setUnauthorizedUrl("/403");
 
	shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
	return shiroFilterFactoryBean;
 
    }
 
    //权限管理,配置主要是Realm的管理认证
    @Bean
    public SecurityManager securityManager() {
	DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();	
	//securityManager.setRealm(myShiroRealm());
	securityManager.setRealm(myShiroRealm(hashedCredentialsMatcher()));
	securityManager.setSessionManager(sessionManager());
	//配置自定义缓存redis
        securityManager.setCacheManager(cacheManager());
	return securityManager;
    }
	
    //session管理
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        Collection listeners = new ArrayList();
        listeners.add(new MySessionListener());
        sessionManager.setSessionListeners(listeners);
        //设置session超时时间为1小时(单位毫秒)
        //sessionManager.setGlobalSessionTimeout(3600000);
        sessionManager.setGlobalSessionTimeout(-1);//永不超时
        //设置redisSessionDao
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }
    
    //配置cacheManager
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }
    
    //配置redisManager
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        redisManager.setExpire(3600);//配置缓存过期时间(秒)
        return redisManager;
    }
    
    //配置redisSessionDAO
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }
	
    //配置html页面支持shiro权限标签控制
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }
	
    //密码匹配凭证管理器
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        //采用SHA-512方式加密
        hashedCredentialsMatcher.setHashAlgorithmName("SHA-512");
        //设置加密次数
        hashedCredentialsMatcher.setHashIterations(1024);
        //true加密用的hex编码,false用的base64编码
        hashedCredentialsMatcher.setStoredCredentialsHexEncoded(false);
        return hashedCredentialsMatcher;
    }
 
}

7.在子系统1中登录用户

@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(Account account,RedirectAttributes redirectAttributes, HttpSession httpSession,HttpServletRequest httpRequest) {
    String forword = "redirect:/login";
    AccountLoginToken token = new AccountLoginToken(account.getUsername(), account.getPassword(), account);
    try {
	SecurityUtils.getSubject().login(token);
        Subject subject = SecurityUtils.getSubject();
	Account currentUser = (Account) subject.getPrincipal();
	forword = "redirect:/index";
	subject.getSession().setAttribute("jh", currentUser.getJh());
	subject.getSession().setAttribute("xm", currentUser.getXm());
	subject.getSession().setAttribute("yhbh", currentUser.getPk());
	subject.getSession().setAttribute("sessionid", subject.getSession().getId());
	System.out.println("登录成功");
    }catch (UnknownAccountException uae) {
	redirectAttributes.addFlashAttribute("errorMsg", "用户不存在!");
    } catch (IncorrectCredentialsException ice) {
	redirectAttributes.addFlashAttribute("errorMsg", "密码不正确!");
    } catch (LockedAccountException lae) {
        redirectAttributes.addFlashAttribute("errorMsg", "账户被锁定!");
    } catch (ExcessiveAttemptsException eae) {
	redirectAttributes.addFlashAttribute("errorMsg", "密码尝试限制!");
    } catch (AuthenticationException e) {
	redirectAttributes.addFlashAttribute("errorMsg", "该帐号未注册!");
    }
    return forword;
}

8.子系统2中获取用户信息

@RequestMapping("/getAccount") 
@ResponseBody
public String getAccount() {
    Subject subject = SecurityUtils.getSubject();
    Account currentUser = (Account) subject.getPrincipal();
    System.out.println("登录用户姓名:"+currentUser.getXm());
    return currentUser.getXm();
}

9.退出登录,子系统2获取信息失败,需子系统1重新登录后才能获取

@RequestMapping("/logout") 
@ResponseBody
public String logout(){
    SecurityUtils.getSubject().logout();
    return "退出登录";
}

MySessionListener监听类:

import java.util.concurrent.atomic.AtomicInteger;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;
import org.springframework.stereotype.Component;

@Component
public class MySessionListener implements SessionListener {

    private final AtomicInteger sessionCount = new AtomicInteger(0);
    
    @Override
    public void onStart(Session session) {   	 
        sessionCount.incrementAndGet();
        //System.out.println("登录+1=="+sessionCount.get());     
    }

    @Override
    public void onStop(Session session) {
    	sessionCount.decrementAndGet();
        //System.out.println("登录退出-1=="+sessionCount.get());
    }

    @Override
    public void onExpiration(Session session) {
        sessionCount.decrementAndGet();
        //System.out.println("登录过期-1=="+sessionCount.get());
    }

    public int getSessionCount() {
        return sessionCount.get();
    }
    
}

备注:需提前开启Redis服务,分布式系统中的每个子系统都需要配置Shiro框架

你可能感兴趣的:(Redis)