本篇在springboot+vue项目搭建的基础上进行shiro和redis的集成,写上自己的一些小理解,如有错误,望大佬们指出,谢谢!
贴上之前搭建基础的链接,来个小总结
springboot+vue之旅(一)
springboot+vue之旅(二)
springboot+vue集成shiro和redis的小理解——reids
百度中,写项目如何集成shiro和redis的例子很多,只是想写一个demo的话,从我自己看来,基本上是先导入需要的包,然后copy一下人家的配置,基本就完成了。
所以这里仅仅大致记录一下流程,主要谢谢自己对里面一些方法的理解
导入包是必须的,国际惯例贴一下我用的包
<!-- shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.2.3</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
我在config里配置了如下图的目录
分别分为
ShiroConfig 即配置类
ShiroRealm 认证类
ShiroKit 这个是自己项目里为了方便,写的获取用户信息的方法
MySessionManager 重写了安全认证的一些我需要的方法
package com.example.demo.config.shiro;
import com.example.demo.config.filter.ShiroFilter;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
@Configuration
public class ShiroConfig {
protected static final long MILLIS_PER_SECOND = 1000;
protected static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
protected static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE;
//在shiro中注入redis使得redis能够记住登录信息
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
//shiroFilterFactoryBean.setLoginUrl("/login/index");
//shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");// 设置无权限时跳转的 url;
//也可以自行在filter中重写方法,定义无权限时的返回等,下面是定义自己的过滤器为shirofilter
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("authc", new ShiroFilter());
shiroFilterFactoryBean.setFilters(filters);
// 设置拦截器,即访问地址时候需要的权限,有anno(不需要权限),authc需要登录权限,和其他定义权限
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/user/logout", "logout");
//其余接口一律拦截
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截
filterChainDefinitionMap.put("/**", "authc");
//然后将设置好的拦截器注入shirobean里
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
System.out.println("Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
/**
* 注入 securityManager shiro的安全管理器 redis在此注入
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm.
securityManager.setRealm(myShiroRealm());
//未使用redis来保存shiro登录信息时不加本句以下两条
// 自定义session管理 使用redis缓存登录信息
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
return securityManager;
}
/**
* 自定义身份认证 realm;
*
* 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
* 否则会影响 CustomRealm类 中其他类的依赖注入
*/
@Bean
public ShiroRealm myShiroRealm() {
//加密时使用,当你在存用户名密码时候,使用的明文和MD5加密时shiro不同的比对方式
// ShiroRealm myShiroRealm = new ShiroRealm();
// myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return new ShiroRealm();
}
/**
* 加密算法 由于数据库存的明文,因此不使用
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
//hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
/**
* 重写CookieID 避免与tomcat冲突
* cookieID要保证与vue项目前端的cookie相同
*/
public SimpleCookie SimpleCookie() {
SimpleCookie simpleCookie = new SimpleCookie("xzn_demo");
simpleCookie.setPath("/");
return simpleCookie;
}
/**
* 自定义sessionManager
* 重写session管理,定义自己判断请求头中的某字段或属性来判断是否已经处于登录状态中
* @return
*/
@Bean
public SessionManager sessionManager() {
MySessionManager mySessionManager = new MySessionManager();
mySessionManager.setSessionIdUrlRewritingEnabled(false);
mySessionManager.setSessionDAO(redisSessionDAO());
mySessionManager.setGlobalSessionTimeout(24 * 7 * MILLIS_PER_HOUR); //7天过期
mySessionManager.setSessionIdCookie(SimpleCookie());
return mySessionManager;
}
/**
* 配置shiro redisManager
* 在shiro中加入redis后要加入的配置,加载了redis的端口,密码等配置信息
* @return
*/
@Bean
public RedisManager redisManager() {
JedisConnectionFactory jc = (JedisConnectionFactory) redisConnectionFactory;
RedisManager redisManager = new RedisManager();
redisManager.setHost(jc.getHostName()+":"+jc.getPort());
redisManager.setPassword(jc.getPassword());
redisManager.setTimeout(jc.getTimeout());
return redisManager;
}
/**
* shiro配置中加入cacheManger,在里面设置实现redis缓存配置
* cacheManager 缓存 redis实现
*/
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
}
每个shiro配置好之后,在登录时都会调用subject.login方法,而这个方法的最终目的地就是shiroRealm中,因每个人的系统写的都不太一致,所以我们需要重写关于如何认证信息和授权的方法,理解都写在注释里
package com.example.demo.config.shiro;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.demo.entity.Resource;
import com.example.demo.entity.User;
import com.example.demo.entity.UserRole;
import com.example.demo.service.ResourceService;
import com.example.demo.service.RoleService;
import com.example.demo.service.UserRoleService;
import com.example.demo.service.UserService;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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 java.io.UnsupportedEncodingException;
import java.util.List;
public class ShiroRealm extends AuthorizingRealm {
//先注入认证权限信息时需要注入的类
@Autowired
private RoleService roleService;
@Autowired
private ResourceService resourceService;
@Autowired
private UserService userService;
@Autowired
private UserRoleService userRoleService;
/**
* 授权 查询用户对应的角色,将其拥有的权限赋予本次登录的用户
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
try {
User user = (User) principals.getPrimaryPrincipal();
String userid = user.getId();
//查询当前用户的角色(沒有角色的控制可以不用)
UserRole userRole = userRoleService.getOne(new QueryWrapper<UserRole>().eq("user_id", userid));
authorizationInfo.addRole(userRole.getRoleId().toString());
List<Resource> resources = resourceService.list(new QueryWrapper<Resource>().eq("role_id",userRole.getRoleId()));
if (resources != null && resources.size() > 0) {
for (Resource resource : resources) {
authorizationInfo.addStringPermission(resource.getResourceName());
}
}
} catch (Exception e) {
e.printStackTrace();
}
return authorizationInfo;
}
/**
* 认证
* 这里的理解是为匹配用户的账号密码信息,确定登录的正确性
*/
@SuppressWarnings("unused")
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
//获取用户的输入的账号.
String username = (String) token.getPrincipal();
User userInfo = userService.getOne(new QueryWrapper<User>().eq("username", username));
if (userInfo == null) {
return null;
}
if (userInfo.getState() == 1) { //账户冻结
throw new LockedAccountException();
}
// 第 1 个参数可以传一个实体对象,然后在认证的环节可以取出
// 第 2 个参数应该传递在数据库中“正确”的数据,然后和 token 中的数据进行匹配
if (userInfo != null) {
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(userInfo, userInfo.getPassword(), getName());
// 设置盐值
ByteSource credentialsSalt = null;
try {
credentialsSalt = ByteSource.Util.bytes(username.getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
info.setCredentialsSalt(credentialsSalt);
return info;
}
return null;
}
}
这段我也不是特别理解,从用的角度上看,发送的请求都会带有一个请求头,请求头中会定义一个用户的信息,而判断这个请求是否过期等等,实际使用中传入的是用户的登录token,防止用户过期后还能访问接口
package com.example.demo.config.shiro;
import java.io.Serializable;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import com.example.demo.utils.StrUtil;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
/**
* 自定义SessionIds
* @author DELL
*
*/
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
setGlobalSessionTimeout(DEFAULT_GLOBAL_SESSION_TIMEOUT * 48);
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有 Authorization 则其值为sessionId
if (!StrUtil.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
//System.out.println("Authorization:"+id);
return id;
} else {
//System.out.println("getSessionId:"+super.getSessionId(request, response));
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
最后是这个过滤器,可以不用,我这里是重写了当用户登录信息过期时的返回值
package com.example.demo.config.filter;
import java.io.IOException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import com.example.demo.pojo.ResultInfo;
import org.apache.shiro.web.filter.authc.UserFilter;
import com.alibaba.fastjson.JSON;
public class ShiroFilter extends UserFilter {
@Override
protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
response.setContentType("application/json; charset=utf-8");
response.getWriter().print(JSON.toJSON(new ResultInfo(403, "请登陆")));
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginRequest(request, response)) {
return false;
}
return super.isAccessAllowed(request, response, mappedValue);
}
}