参考: Shiro Springboot 集群共享Session (Redis)+单用户登录
https://zhuanlan.zhihu.com/p/54176956
框架搭建 1.基础环境jdk8
maven
lombok
spring boot 2.5.7
3.新建自定义Realm类,实现认证与鉴权核心逻辑org.apache.shiro shiro-spring-boot-web-starter 1.8.0
创建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这种通过代码控制的方法我没有深入了解,所以没有找到这种权限的控制。再加上使用注解更加简洁明了,所以个人更倾向于使用注解方式来控制。
5.获取上下文信息至此一个基本的shrio + spring boot的框架已经搭建完毕
登录成功后可以通过以下代码获取当前登录的用户信息
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();
6. session持久化、分布式session共享参考:shiro使用Md5加密
将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 RedisTemplateredisTemplate; //保存 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



