栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

spring boot shiro+session+redis 实现登录会话、会话保持、分布式session共享

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

spring boot shiro+session+redis 实现登录会话、会话保持、分布式session共享

参考: Shiro Springboot 集群共享Session (Redis)+单用户登录

https://zhuanlan.zhihu.com/p/54176956

框架搭建 1.基础环境

jdk8
maven
lombok
spring boot 2.5.7

2.导入shiro maven坐标

   org.apache.shiro
    shiro-spring-boot-web-starter
    1.8.0

3.新建自定义Realm类,实现认证与鉴权核心逻辑

创建UserInfo.java:

@Setter
@Getter
public class UserInfo implements Serializable {
    private String username;
    private String password;
    private Set roles;
    private Set perms;
}

创建CustomRealm .java:

import org.apache.shiro.authc.*;
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 java.util.HashSet;

public class CustomRealm extends AuthorizingRealm {
    
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //登录TOKEN,包含了用户账号密码
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        String username = upToken.getUsername();
        //下列多个判断可根据业务自行增删
        // 判断用户名是否不存在,如果不存在抛出异常
        if (username == null) {
            throw new AccountException("Null usernames are not allowed by this realm.");
        }
		
		//模拟数据,可通自行通过查找数据库获取当前用户信息
        UserInfo user = new UserInfo();
        user.setUsername("aesop");
        user.setPassword("123");

        //查询用户的角色和权限存到SimpleAuthenticationInfo中,这样在其它地方
        //SecurityUtils.getSubject().getPrincipal() 就能拿出用户的所有信息,包括角色和权限

        
        HashSet roles = new HashSet<>();
        roles.add("admin");
        roles.add("teacher");
        user.setRoles(roles);

        HashSet perms = new HashSet<>();
        perms.add("blog:read");
        perms.add("blog:search");
        user.setPerms(perms);

        //也可存入额外的信息到Session
     	//SecurityUtils.getSubject().getSession().setAttribute(Constants.SESSION_USER_INFO, userInfo);


        //构造验证信息返回
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
        return info;
    }

    
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //null usernames are invalid
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }
        //获取当前用户对应的User对象
        UserInfo user = (UserInfo) getAvailablePrincipal(principals);
        //创建权限对象
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //设置用户角色(user.getRoles()是一个Set,【admin,student。。。】)
        info.setRoles(user.getRoles());
        //设置用户许可(user.getPerms()是一个Set,【blog:read,blog:search。。。】)
        info.setStringPermissions(user.getPerms());
        return info;
    }
}

3. 添加Shiro拦截配置

创建ShiroConfig.java:

package com.example.springshirodemo.config.shiro;

import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;


@Configuration
public class ShiroConfig {
    
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager(myRealm()));

        Map filters = new HashMap<>();
        filters.put("authc", new LoginFormFilter());
        shiroFilterFactoryBean.setFilters(filters);
        Map map = new HashMap<>();
        // 登入登出
        map.put("/doLogin", "anon");
        map.put("/logout", "logout");
        // swagger
        map.put("/swagger**
    @Bean
    public CustomRealm myRealm() {
        return new CustomRealm();
    }

    
    @Bean
    public DefaultWebSecurityManager securityManager(CustomRealm customRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 自定义Realm
        securityManager.setRealm(customRealm);
        return securityManager;
    }
}

自定义登录失败、或没有登陆时返回json格式,而不是重定向到login.jsp页面。注意:配置了这个之后,重定向路径配置setLoginUrl将失效
创建ShiroLoginFilter类:

import cn.aesop.common.restful.ResultBean;
import cn.aesop.common.restful.ResultCode;
import com.alibaba.fastjson.JSON;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;


public class ShiroLoginFilter extends FormAuthenticationFilter {

	@Override
	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
		PrintWriter out = null;
		HttpServletResponse res = (HttpServletResponse) response;
		try {
			res.setCharacterEncoding("UTF-8");
			res.setContentType("application/json");
			out = response.getWriter();
			out.println(JSON.toJSONString(ResultBean.FAIL(ResultCode.E_201)));
		} catch (Exception e) {
		} finally {
			if (null != out) {
				out.flush();
				out.close();
			}
		}
		return false;
	}
}

4.注解权限

在Controller接口上加上如下注解,即可拦截没有权限的请求

 @RequiresRoles(value={"admin","user"},logical = Logical.OR)
 @RequiresPermissions(value={"add","update"},logical = Logical.AND)

如果有多个权限/角色验证的时候中间用“,”隔开,默认是所有列出的权限/角色必须同时满足才生效。但是在注解中有logical = Logical.OR这块。这里可以让权限控制更灵活些。

如果将这里设置成OR,表示所列出的条件只要满足其中一个就可以,如果不写或者设置成logical = Logical.AND,表示所有列出的都必须满足才能进入方法。

用subject这种通过代码控制的方法我没有深入了解,所以没有找到这种权限的控制。再加上使用注解更加简洁明了,所以个人更倾向于使用注解方式来控制。

至此一个基本的shrio + spring boot的框架已经搭建完毕

5.获取上下文信息

登录成功后可以通过以下代码获取当前登录的用户信息

Subject currentUser = SecurityUtils.getSubject();
UserInfo principal = (UserInfo)currentUser.getPrincipal();

//或者从session中获取自定义的信息
//Session session = SecurityUtils.getSubject().getSession();
//UserInfo principal = (UserInfo) session.getAttribute(Constants.SESSION_USER_INFO);

5. 密码加密

上面的例子密码是直接明文保存在数据库的,不安全,需要进行加密后才能存储,并且要与身份认证形成一个体系,下面介绍基本修改步骤:
1) 创建凭证匹配器

@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
	HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
	//散列算法:这里使用MD5算法;
	hashedCredentialsMatcher.setHashAlgorithmName("md5");
	//散列的次数,比如散列两次,相当于 md5(md5(""));
	hashedCredentialsMatcher.setHashIterations(2);
	//storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用base64编码
	hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
	return hashedCredentialsMatcher;
}

2)在注入CustomRealm处设置凭证匹配器,修改代码如下

@Bean
public CustomerRealm userRealm() {
	CustomerRealm realm = new CustomerRealm();
	realm.setCredentialsMatcher(hashedCredentialsMatcher());
	return realm;
}

3)修改CustomerRealm类的doGetAuthenticationInfo方法

...

//加入盐 salt=username+salt
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(username+"salt"), getName());
...

4)在注册用户或创建密码时,使用以下规则创建加密密码,存入数据库

// md5 + salt + hash散列次数
Md5Hash md5Hash2 = new Md5Hash(password, username+"salt", 2);
return md5Hash2.toString();

参考:shiro使用Md5加密

6. session持久化、分布式session共享

将session保存到redis ,多机部署使用同一个redis,可以保证session互相共享; 系统重启,用户也无需重新登陆
1)maven pom加入redis


    org.springframework.boot
    spring-boot-starter-data-redis

2)application.yml配置

spring:
  redis:
    host: localhost #redis服务PI
    port: 6379      #服务端

Redis 的基本操作

@Autowired
private RedisTemplate redisTemplate;
//保存
redisTemplate.opsForValue().set("key-1", "value-1"); 
//带有效期的保存
redisTemplate.opsForValue().set("key-1", "value-1", 120, TimeUnit.SECONDS);
//删除
redisTemplate.delete("key-1");

3)创建类继承CachingSessionDAO,自定义session持久化实现
需要Override的4个方法是:
doCreate: shiro创建session时,将session保存到redis
doUpdate: 当用户维持会话时,刷新session的有效时间
doDelete: 当用户注销或会话过期时,将session从redis中删除
doReadSession: shiro通过sessionId获取Session对象,从redis中获取

创建 RedisSessionDAO.java

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.springframework.data.redis.core.RedisTemplate;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;


public class RedisSessionDAO extends CachingSessionDAO {
    //存入Redis中的SessionID的前缀
    private static final String PREFIX = "SENTGON_SHOP_SHIRO_SESSION_ID";
    //有效期(后续使用时会增加时间单位,秒)
    private static final int EXPRIE = 86400; //1天
    //Redis 操作工具 
    private RedisTemplate redisTemplate;

    //构造函数
    public RedisSessionDAO(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }


    
    @Override
    protected Serializable doCreate(Session session) {
        //生成SessionID
        Serializable serializable = this.generateSessionId(session);
        assignSessionId(session, serializable);
        //将sessionid作为Key,session作为value存入redis
        redisTemplate.opsForValue().set(PREFIX+serializable, session);
        return serializable;
    }

    
    @Override
    protected void doUpdate(Session session) {
        //设置session有效期
        session.setTimeout(EXPRIE * 1000);
        //将sessionid作为Key,session作为value存入redis,并设置有效期
        redisTemplate.opsForValue().set(PREFIX+session.getId(), session, EXPRIE, TimeUnit.SECONDS);
    }

    
    @Override
    protected void doDelete(Session session) {
        //null 验证
        if (session == null) {
            return;
        }
        //从Redis中删除指定SessionId的k-v
        redisTemplate.delete(PREFIX+session.getId());
    }

    
    @Override
    protected Session doReadSession(Serializable sessionId) {
        if (sessionId == null) {
            return null;
        }
        //从Redis中读取Session对象
        Session session = redisTemplate.opsForValue().get(PREFIX+sessionId);
        return session;
    }
}

4)将RedisSessionManager注入 SecurityManager

@Autowired
private RedisTemplate redisTemplate;


@Bean
public SessionDAO redisSessionDAO(RedisTemplate redisTemplate) {
	return new RedisSessionDAO(redisTemplate);
}


@Bean
public DefaultWebSecurityManager securityManager(CustomerRealm customRealm) {
	DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
	// 自定义Realm
	securityManager.setRealm(customRealm);
	// 重写session管理器,注入自定义的SessionDao
	DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
	defaultWebSessionManager.setSessionDAO(redisSessionDAO(redisTemplate));
	securityManager.setSessionManager(defaultWebSessionManager);
	return securityManager;
}

至此,已经完成Shiro的集群共享Session

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/657526.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号