基于Shiro框架的权限系统,包括登录权限、角色权限、菜单权限
GitHub地址:https://github.com/HappyWjl/auth-shiro/tree/master
如果该项目对您有帮助,您可以点右上角 “Star” 支持一下 谢谢!
或者您可以 “follow” 一下,该项目将持续更新,不断完善功能。
转载还请注明出处,谢谢了
博主QQ:820155406
一、简介
- Shiro 这个框架,先甩一张网上流行的架构图,看起来很高大上,然后呢,就不用细看了,知道哪个里面嵌套哪个,了解下大体结构,方便理解后面的代码结构:
- 博主对权限系统理解有限,此入门项目涵盖三部分权限场景,包括:登录、接口权限、菜单权限
二、环境搭建
- IDEA:http://www.demxy.com/#/tool?id=1&type=tool
- JDK:http://www.demxy.com/#/tool?id=4&type=tool
- MySQL:http://www.demxy.com/#/tool?id=2&type=tool
- Redis:http://www.demxy.com/#/tool?id=3&type=tool
- Postman:http://www.demxy.com/#/tool?id=9&type=tool
三、项目讲解
项目太抽象的话,不方便理解,所以我们假设一个场景,每个曾经上学很怀念的场景:
用户账号:zhuren(教务处王主任,可以叫他老王)shuxue(数学张老师,曾经拿过全国奥数奖的老张)
用户角色:zhuren(主任角色,权限很大,各种阴谋诡计)teacher(老师角色,备备课,讲讲题,顺便拖拖堂)
用户接口权限:shoubanfei(收班费,没准还能收点贿赂)jiaoxuesheng(教学生,假期还能办个补习班赚外快)
用户菜单权限:教务管理 课程安排
- 根据上方假设的场景,我们来建立权限系统,并通过Shiro框架实现权限控制
- 源码中还包含 强制T人、禁用账号、多端互T 等功能,博客中就不一一介绍了,详见源码
- 登录权限
- 账号密码登录
- 账号密码登录成功后,返回权限对应的菜单
- 账号密码带验证码登录
- 接口权限
- 单一角色限制
- 多个角色限制
- 单一权限限制
- 多个权限限制
四、项目搭建
- 环境搭建好后,我们先把表关系理清楚,大致分为下面几个表:
tb_user:用户表
tb_role:角色表
tb_permission:权限表
tb_menu:菜单表
tb_user_role:用户-角色关联表
tb_role_permission:角色-权限关联表
tb_role_menu:角色-菜单关联表
- 把表分别拆开关联的好处是可扩展,并且可实现多对多的关联关系,应对后续复杂的业务场景很适用
- 数据库的SQL文件会在文末源码中,带初始数据
- 接下来贴上项目完成后的目录结构
- 新建项目,在pom.xml文件中,引入下方的maven代码进行构建,每一个都有注释,使用时,可以思考为什么要使用这个:
<dependencies>
<!-- Apache Shiro依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 连接mysql数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JDBC连接数据库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- 添加web支持 包含SpringMVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--@ConfigurationProperties注解-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis依赖commons-pool 这个依赖一定要添加 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- google kaptcha验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
<exclusions>
<exclusion>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
- 必要的包构建完成后,打开配置文件(这里使用的是yml的配置文件语法),配置好参数,比如端口号、数据库链接、redis链接、shiro的一些自定义缓存参数等
# 端口号 port
server:
port: 8080
# 数据库配置 jdbc
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull
username: root
password: root
# 缓存配置 redis
redis:
database: 0
host: 127.0.0.1
port: 6379
password:
mybatis:
mapper-locations: classpath:/mappers/*.xml
type-aliases-package: com.example.shiro.model
# session cache config
shiro:
redis:
sessionLive: 30
sessionPrefix: shiro_redis_session_
cacheLive: 30
cachePrefix: shiro_redis_cache_
kickoutPrefix: shiro_redis_kickout_
# 验证码缓存时间
verificationCodeTime: 5
# 踢出缓存key
kickOutKey: out
- 接下来,我们可以先实现前面7张表的增删改查操作(可以适当少写点代码,不一定增删改查都会用到的),也就是项目目录中的:service、serviceImpl、dao、model、mapper 此处省略2000行代码,不细讲增删改查了,练就一身CV大法的我已经疲惫
- 直接讲重点,核心文件就一个,没有之一:ShiroConfig
- 它里面创建了各种Bean,根据最开始贴的shiro架构图,可以看出每一个Bean所在的位置,分别将redis、自定义的配置,填充到Shiro框架中了,代码中有每个Bean的注释,可以看到每一个Bean的作用
package com.shiro.api.config;
import com.shiro.api.core.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import javax.servlet.Filter;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.filter.DelegatingFilterProxy;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro配置文件
*
* Created by Happy王子乐 on 2019/8/02.
*/
@Configuration
public class ShiroConfig {
// session缓存时间
@Value("${shiro.redis.sessionLive}")
private long sessionLive;
// session前缀
@Value("${shiro.redis.sessionPrefix}")
private String sessionPrefix;
// redis缓存时间
@Value("${shiro.redis.cacheLive}")
private long cacheLive;
// redis缓存前缀
@Value("${shiro.redis.cachePrefix}")
private String cachePrefix;
// 验证码缓存前缀
@Value("${shiro.redis.kickoutPrefix}")
private String kickoutPrefix;
/**
* 自定义redis缓存管理器
*
* @param redisTemplate redis模版
* @return redis缓存管理器对象
*/
@Bean(name = "redisCacheManager")
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
RedisCacheManager redisCacheManager = new RedisCacheManager();
// 将redis缓存时间及前缀,放到配置中
redisCacheManager.setCacheLive(cacheLive);
redisCacheManager.setCacheKeyPrefix(cachePrefix);
redisCacheManager.setRedisTemplate(redisTemplate);
return redisCacheManager;
}
/**
* 凭证匹配器(密码加密)
*
* @return 凭证匹配器对象
*/
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 加密算法,指定MD5加密
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 加密的次数为2次,相当于 MD5(MD5())
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* Session ID生成管理器
*
* @return sessionId 生成器对象
*/
@Bean(name = "sessionIdGenerator")
public JavaUuidSessionIdGenerator sessionIdGenerator() {
return new JavaUuidSessionIdGenerator();
}
/**
* 自定义RedisSessionDAO
*
* @param sessionIdGenerator sessionId 生成器
* @param redisTemplate redis模版
* @return RedisSessionDAO 对象
*/
@Bean(name = "redisSessionDAO")
public RedisSessionDAO redisSessionDAO(JavaUuidSessionIdGenerator sessionIdGenerator, RedisTemplate redisTemplate) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
// 配置sessionId生成器
redisSessionDAO.setSessionIdGenerator(sessionIdGenerator);
// 配置session缓存时间及缓存前缀
redisSessionDAO.setSessionLive(sessionLive);
redisSessionDAO.setSessionKeyPrefix(sessionPrefix);
// 配置redis模版
redisSessionDAO.setRedisTemplate(redisTemplate);
return redisSessionDAO;
}
/**
* 自定义session管理器
*
* @param redisSessionDAO redisSessionDAO
* @return session管理器 对象
*/
@Bean(name = "sessionManager")
public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
MySessionManager mySessionManager = new MySessionManager();
// 配置 自定义的RedisSessionDAO 对象
mySessionManager.setSessionDAO(redisSessionDAO);
return mySessionManager;
}
/**
* 自定义域
*
* @return 自定义域 对象
*/
@Bean(name = "myRealm")
public MyRealm myRealm() {
MyRealm myRealm = new MyRealm();
// 启用缓存,默认为false
myRealm.setCachingEnabled(true);
return myRealm;
}
/**
* 安全管理器
*
* @param sessionManager session管理器
* @param redisCacheManager redis缓存管理器
* @return 安全管理器
*/
@Bean(name = "securityManager")
public SecurityManager securityManager(SessionManager sessionManager, RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 配置自定义域
securityManager.setRealm(myRealm());
// 配置session管理器
securityManager.setSessionManager(sessionManager);
// 配置redis缓存管理器
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
/**
* 多端互T控制过滤器
*
* @param sessionManager session管理器
* @param redisTemplate redis模版
* @return 互T控制过滤器
*/
@Bean(name = "outSessionControlFilter")
public OutSessionControlFilter outSessionControlFilter(SessionManager sessionManager, RedisTemplate redisTemplate) {
OutSessionControlFilter outSessionControlFilter = new OutSessionControlFilter();
outSessionControlFilter.setSessionManager(sessionManager);
outSessionControlFilter.setRedisTemplate(redisTemplate);
outSessionControlFilter.setKickOutPrefix(kickoutPrefix);
return outSessionControlFilter;
}
/**
* shiro过滤器
*
* @param securityManager 安全管理器
* @param outSessionControlFilter 多端互T控制过滤器
* @return shiro过滤工厂Bean
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, OutSessionControlFilter outSessionControlFilter) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap(2);
filters.put("out", outSessionControlFilter);
shiroFilterFactoryBean.setFilters(filters);
// 注意拦截链配置顺序,不能颠倒
Map<String, String> filterChainDefinitionMap = new LinkedHashMap();
// 退出
filterChainDefinitionMap.put("/logout", "logout");
// 可匿名访问,无需登录,即可访问的路径
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/loginByMenu", "anon");
filterChainDefinitionMap.put("/loginByCaptcha", "anon");
filterChainDefinitionMap.put("/captcha", "anon");
// 拦截所有请求
filterChainDefinitionMap.put("/**", "out,authc");
// 未认证 跳转未认证页面
shiroFilterFactoryBean.setLoginUrl("/unAuthen");
// 未授权 跳转未权限页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unAuthor");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 将springboot中的过滤器替换成shiro过滤器,不替换会报错
*
* @return 过滤器注册Bean
*/
@Bean
public FilterRegistrationBean delegatingFilterProxy() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
// 设置servlet容器来管理其生命周期,默认false,spring来管理其生命周期
proxy.setTargetFilterLifecycle(true);
// 配置改为shiro过滤器
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
/**
* 默认创建代理类Bean
* 在这里将代理改为cglib代理的方式
*
* @return 默认创建代理类Bean
*/
@Bean(name = "advisorAutoProxyCreator")
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 设置成true,是以cglib动态代理生成代理类;设置成false,就是默认用JDK动态代理生成代理类
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* shiro中的权限拦截器
*
* @param securityManager 权限管理器
* @return shiro中的权限拦截器
*/
@Bean(name = "authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
// 配置权限管理器
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
- Redis配置文件,RedisConfig:
package com.shiro.api.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis配置文件
*
* Created by Happy王子乐 on 2019/8/02.
*/
@Configuration
@EnableCaching
public class RedisConfig {
/**
* 配置自定义redis模版
*
* @return redis模版
*/
@Bean(name = "redisTemplate")
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new StringRedisTemplate(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
return redisTemplate;
}
}
- 验证码配置文件,KaptchaConfig:
package com.shiro.api.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* google验证码
*
* Created by Happy王子乐 on 2019/8/02.
*/
@Configuration
public class KaptchaConfig {
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "10");
properties.put("kaptcha.textproducer.char.length","4");
properties.put("kaptcha.image.height","34");
properties.put("kaptcha.textproducer.font.size","25");
properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
- 自定义域,MyRealm:
package com.shiro.api.core;
import com.shiro.api.model.TbUser;
import com.shiro.api.service.LoginService;
import com.shiro.api.service.TbUserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Objects;
/**
* 自定义域
*
* Created by Happy王子乐 on 2019/8/02.
*/
public class MyRealm extends AuthorizingRealm {
@Autowired
private TbUserService userService;
@Autowired
private LoginService loginService;
/**
* 认证,出现异常会被ControllerExceptionHandler捕获
*
* @param token 用户token
* @return 认证信息
* @throws AuthenticationException 认证异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = (String) token.getPrincipal();
TbUser user = userService.getByUserName(userName);
if (Objects.isNull(user)) {
throw new UnknownAccountException("该用户名称不存在!");
} else if (Objects.isNull(user.getForbidden()) || user.getForbidden().equals(1)) {
throw new UnknownAccountException("该用户已经被锁定!");
} else {
String password = new String((char[]) token.getCredentials());
// 校验传入的密码,是否等于数据库中的密码
if (user.getPassword().equals(password)) {
AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user.getUserName(), user.getPassword(), user.getUserName());
// 将user对象放到session属性中
SecurityUtils.getSubject().getSession().setAttribute("currentUser", user);
return authenticationInfo;
} else {
throw new IncorrectCredentialsException("密码错误!");
}
}
}
/**
* 授权,出现异常会被ControllerExceptionHandler捕获
*
* @param principals shiro框架中用户信息对象
* @return 授权信息
* @throws AuthorizationException 授权异常
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String userName = (String) principals.getPrimaryPrincipal();
// 根据用户名授予相应的权限,用户名需唯一
return loginService.getRolesAndPermissionsByUserName(userName);
}
}
- 自定义session管理,MySessionManager:
package com.shiro.api.core;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* 自定义session管理
*
* Created by Happy王佳乐 on 2019/8/02.
*/
public class MySessionManager extends DefaultWebSessionManager {
// 前端请求头传这个参数,用于获取SessionId
private static final String AUTHORIZATION = "Authorization";
public MySessionManager() {
super();
}
/**
* 获取sessionId
*
* @param request 请求request
* @param response 返回response
* @return SessionId
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
// 从请求头中获取SessionId
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
// 如果请求头中有 Authorization 则其值为sessionId
if (!StringUtils.isEmpty(id)) {
// 参考DefaultWebSessionManager源码中getSessionId方法
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
// 否则按默认规则(DefaultWebSessionManager)从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
- 多端互T过滤器,OutSessionControlFilter:
package com.shiro.api.core;
import com.shiro.api.enums.ErrorCode;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 多端互T过滤器
*
* Created by Happy王佳乐 on 2019/8/02.
*/
public class OutSessionControlFilter extends AccessControlFilter {
@Value("${shiro.redis.kickOutKey}")
private String kickOutKey;
private String kickOutPrefix;
private RedisTemplate redisTemplate;
private SessionManager sessionManager;
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);
// 如果没有登录,不进行多出登录判断
if (!subject.isAuthenticated() && !subject.isRemembered()) {
return true;
}
Session session = subject.getSession();
String username = (String) subject.getPrincipal();
Serializable sessionId = session.getId();
// 获取redis中数据
List<Serializable> sessionIdList = redisTemplate.opsForList().range(kickOutPrefix + username, 0, -1);
if (sessionIdList == null || sessionIdList.size() == 0) {
sessionIdList = new ArrayList<>();
}
// 如果队列里没有此sessionId,且用户没有被踢出,当前session放入队列
if (!sessionIdList.contains(sessionId) && Objects.isNull(session.getAttribute(kickOutKey))) {
sessionIdList.add(sessionId);
redisTemplate.opsForList().leftPush(kickOutPrefix + username, sessionId);
}
// 如果队列里的sessionId数大于1,开始踢人
while (sessionIdList.size() > 1) {
// 获取第一个sessionId(限转成LinkedList,保证顺序)
Serializable outSessionId = sessionIdList.get(0);
sessionIdList.remove(outSessionId);
System.out.println("移除---sessionId: " + outSessionId);
System.out.println("剩余---sessionId: " + sessionIdList.get(0));
redisTemplate.opsForList().remove(kickOutPrefix + username, 1, outSessionId);
try {
DefaultSessionKey defaultSessionKey = new DefaultSessionKey(outSessionId);
Session outSession = sessionManager.getSession(defaultSessionKey);
// 设置会话的out属性表示踢出了
if (outSession != null) {
outSession.setAttribute(kickOutKey, true);
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
// session包含out属性,T出
if (session.getAttribute(kickOutKey) != null) {
try {
subject.logout();
} catch (Exception e) {
System.out.println(e.getMessage());
}
saveRequest(servletRequest);
// 返回错误码,以及错误文案
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
httpResponse.setStatus(HttpStatus.OK.value());
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.getWriter().write("{\"code\":" + ErrorCode.UNAUTHENTIC.getCode() + ", \"msg\":\"" + "您已被强制下线!" + "\"}");
return false;
}
return true;
}
public void setKickOutPrefix(String kickOutPrefix) {
this.kickOutPrefix = kickOutPrefix;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
- 自定义redisCache,RedisCache:
package com.shiro.api.core;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 自定义redisCache
*
* Created by Happy王佳乐 on 2019/8/02.
*/
public class RedisCache<K, V> implements Cache<K, V> {
private long cacheLive;
private String cacheKeyPrefix;
private RedisTemplate redisTemplate;
@Override
public V get(K k) throws CacheException {
return (V) this.redisTemplate.opsForValue().get(this.getRedisCacheKey(k));
}
@Override
public V put(K k, V v) throws CacheException {
redisTemplate.opsForValue().set(this.getRedisCacheKey(k), v, cacheLive, TimeUnit.MINUTES);
return v;
}
@Override
public V remove(K k) throws CacheException {
V obj = (V) this.redisTemplate.opsForValue().get(this.getRedisCacheKey(k));
redisTemplate.delete(this.getRedisCacheKey(k));
return obj;
}
@Override
public void clear() throws CacheException {
Set keys = this.redisTemplate.keys(this.cacheKeyPrefix + "*");
if (null != keys && keys.size() > 0) {
Iterator iterator = keys.iterator();
this.redisTemplate.delete(iterator.next());
}
}
@Override
public int size() {
Set<K> keys = this.redisTemplate.keys(this.cacheKeyPrefix + "*");
return keys.size();
}
@Override
public Set<K> keys() {
return this.redisTemplate.keys(this.cacheKeyPrefix + "*");
}
@Override
public Collection<V> values() {
Set<K> keys = this.redisTemplate.keys(this.cacheKeyPrefix + "*");
Set<V> values = new HashSet<V>(keys.size());
for (K key : keys) {
values.add((V) this.redisTemplate.opsForValue().get(this.getRedisCacheKey(key)));
}
return values;
}
private String getRedisCacheKey(K key) {
Object redisKey = this.getStringRedisKey(key);
if (redisKey instanceof String) {
return this.cacheKeyPrefix + redisKey;
} else {
return String.valueOf(redisKey);
}
}
private Object getStringRedisKey(K key) {
Object redisKey;
if (key instanceof PrincipalCollection) {
redisKey = this.getRedisKeyFromPrincipalCollection((PrincipalCollection) key);
} else {
redisKey = key.toString();
}
return redisKey;
}
private Object getRedisKeyFromPrincipalCollection(PrincipalCollection key) {
List realmNames = this.getRealmNames(key);
Collections.sort(realmNames);
Object redisKey = this.joinRealmNames(realmNames);
return redisKey;
}
private List<String> getRealmNames(PrincipalCollection key) {
ArrayList realmArr = new ArrayList();
Set realmNames = key.getRealmNames();
Iterator i$ = realmNames.iterator();
while (i$.hasNext()) {
String realmName = (String) i$.next();
realmArr.add(realmName);
}
return realmArr;
}
private Object joinRealmNames(List<String> realmArr) {
StringBuilder redisKeyBuilder = new StringBuilder();
for (int i = 0; i < realmArr.size(); ++i) {
String s = realmArr.get(i);
redisKeyBuilder.append(s);
}
String redisKey = redisKeyBuilder.toString();
return redisKey;
}
public RedisCache(RedisTemplate redisTemplate, long cacheLive, String cachePrefix) {
this.redisTemplate = redisTemplate;
this.cacheLive = cacheLive;
this.cacheKeyPrefix = cachePrefix;
}
}
- 自定义cache管理器,RedisCacheManager:
package com.shiro.api.core;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 自定义cache管理器
*
* Created by Happy王佳乐 on 2019/8/02.
*/
public class RedisCacheManager implements CacheManager {
private long cacheLive;
private String cacheKeyPrefix;
private RedisTemplate redisTemplate;
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<>();
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
Cache cache = this.caches.get(name);
if (cache == null) {
// 自定义shiro缓存
cache = new RedisCache<K, V>(redisTemplate, cacheLive, cacheKeyPrefix);
this.caches.put(name, cache);
}
return cache;
}
public void setCacheLive(long cacheLive) {
this.cacheLive = cacheLive;
}
public void setCacheKeyPrefix(String cacheKeyPrefix) {
this.cacheKeyPrefix = cacheKeyPrefix;
}
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
- 自定义sessionDAO,RedisSessionDAO:
package com.shiro.api.core;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.data.redis.core.RedisTemplate;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 自定义sessionDAO
*
* Created by Happy王佳乐 on 2019/8/02.
*/
public class RedisSessionDAO extends AbstractSessionDAO {
private long sessionLive;
private String sessionKeyPrefix;
private RedisTemplate redisTemplate;
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
redisTemplate.opsForValue().set(sessionKeyPrefix + sessionId, session, sessionLive, TimeUnit.MINUTES);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
return (Session) redisTemplate.opsForValue().get(sessionKeyPrefix + sessionId);
}
@Override
public void update(Session session) {
this.redisTemplate.opsForValue().set(sessionKeyPrefix + session.getId(), session, sessionLive, TimeUnit.MINUTES);
}
@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
return;
}
this.redisTemplate.delete(sessionKeyPrefix + session.getId());
}
@Override
public Collection<Session> getActiveSessions() {
Set<Session> sessions = new HashSet<Session>();
Set<Serializable> keys = redisTemplate.keys(sessionKeyPrefix + "*");
if (keys != null && keys.size() > 0) {
for (Serializable key : keys) {
Session s = (Session) redisTemplate.opsForValue().get(key);
sessions.add(s);
}
}
return sessions;
}
public void setSessionLive(long sessionLive) {
this.sessionLive = sessionLive;
}
public void setSessionKeyPrefix(String sessionKeyPrefix) {
this.sessionKeyPrefix = sessionKeyPrefix;
}
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
- 自定义返回结果集,Response:
package com.shiro.api.util;
import java.io.Serializable;
/**
* 自定义返回结果集
*
* @param <T>
*/
public class Response<T> implements Serializable {
private static final long serialVersionUID = 1998307887673028548L;
private int code;
private String msg;
private T data;
public Response() {
}
public Response(int code, String msg) {
this.code = code;
this.msg = msg;
}
public Response(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public boolean equals(Object obj) {
if (!(obj instanceof Response)) {
return false;
}
return this.getCode() == ((Response) obj).getCode();
}
public int hashCode() {
return this.code;
}
}
- 结果集工具类,ResponseUtil:
package com.shiro.api.util;
import com.shiro.api.enums.ErrorCode;
public class ResponseUtil {
public ResponseUtil() {
}
public static Response makeFail(String message) {
return makeResponse(1, message, (Object)null);
}
public static Response makeSuccess(Object obj) {
return makeResponse(0, "", obj);
}
public static Response makeSuccess(Object obj, String msg) {
return makeResponse(0, msg, obj);
}
public static Response makeFail(Object obj) {
return makeResponse(1, "", obj);
}
public static Response makeError(ErrorCode errorCode) {
return makeResponse(errorCode.getCode(), errorCode.getMsg(), (Object)null);
}
public static Response makeError(ErrorCode errorCode, Object obj) {
return makeResponse(errorCode.getCode(), errorCode.getMsg(), obj);
}
public static Response makeAdminError(ErrorCode errorCode) {
return makeResponse(errorCode.getCode(), errorCode.getMsg(), (Object)null);
}
public static Response makeAdminError(ErrorCode errorCode, Object obj) {
return makeResponse(errorCode.getCode(), errorCode.getMsg(), obj);
}
public static Response makeResponse(int code, String msg, Object obj) {
Response result = new Response();
result.setCode(code);
result.setMsg(msg);
result.setData(obj);
return result;
}
public static boolean isOk(Response response) {
return response != null && response.getCode() == 0;
}
}
- 接下来,项目controller包中,建立两个controller,分别是LoginController(用于验证登录权限、菜单权限)、HelloController(用于验证访问接口权限)
package com.shiro.api.controller;
import com.shiro.api.enums.ErrorCode;
import com.shiro.api.model.*;
import com.shiro.api.service.*;
import com.shiro.api.util.Response;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.shiro.api.util.ResponseUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 业务接口Controller
*
* Created by Happy王子乐 on 2019/8/02.
*/
@RestController
public class LoginController {
@Autowired
DefaultKaptcha producer;
@Autowired
private LoginService loginService;
/**
* 账号、密码登录
*
* @param tbUser 用户对象
* @return Response
*/
@PostMapping(value = "/login")
public Response login(TbUser tbUser) {
// 校验账号密码是否传入
if (StringUtils.isEmpty(tbUser.getUserName()) || StringUtils.isEmpty(tbUser.getPassword())) {
return ResponseUtil.makeError(ErrorCode.PARAM_ERROR);
}
Subject subject = SecurityUtils.getSubject();
// 生成token
UsernamePasswordToken token = new UsernamePasswordToken(tbUser.getUserName(), tbUser.getPassword());
try {
subject.login(token);
return ResponseUtil.makeSuccess(subject.getSession().getId());
} catch (Exception e) {
return ResponseUtil.makeError(ErrorCode.LOGIN_ERROR);
}
}
/**
* 账号、密码登录,带菜单信息
*
* @param tbUser 用户对象
* @return Response
*/
@PostMapping(value = "/loginByMenu")
public Response loginByMenu(TbUser tbUser) {
// 校验账号密码是否传入
if (StringUtils.isEmpty(tbUser.getUserName()) || StringUtils.isEmpty(tbUser.getPassword())) {
return ResponseUtil.makeError(ErrorCode.PARAM_ERROR);
}
Subject subject = SecurityUtils.getSubject();
// 生成token
UsernamePasswordToken token = new UsernamePasswordToken(tbUser.getUserName(), tbUser.getPassword());
try {
subject.login(token);
// 根据用户名称,查询菜单权限
List<TbMenu> menuList = loginService.getMenuByUserName(tbUser.getUserName());
// 封装结果
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("token", subject.getSession().getId());
resultMap.put("menuList", menuList);
return ResponseUtil.makeSuccess(resultMap);
} catch (Exception e) {
return ResponseUtil.makeError(ErrorCode.LOGIN_ERROR);
}
}
/**
* 账号、密码登录,带验证码
*
* @param tbUser 用户对象
* @param sToken 验证码对应的token
* @param textStr 用户输入的验证码
* @return Response
*/
@PostMapping(value = "/loginByCaptcha")
public Response loginByCaptcha(TbUser tbUser, String sToken, String textStr) {
// 校验账号、密码、验证码等是否传入
if (StringUtils.isEmpty(tbUser.getUserName()) || StringUtils.isEmpty(tbUser.getPassword()) ||
StringUtils.isEmpty(sToken) || StringUtils.isEmpty(textStr)) {
return ResponseUtil.makeError(ErrorCode.PARAM_ERROR);
}
Subject subject = SecurityUtils.getSubject();
// 生成token
UsernamePasswordToken token = new UsernamePasswordToken(tbUser.getUserName(), tbUser.getPassword());
// 校验验证码
boolean flag = loginService.checkCodeToken(sToken, textStr);
if(!flag) {
return ResponseUtil.makeError(ErrorCode.CAPTCHA_CHECK_ERROR);
}
try {
subject.login(token);
return ResponseUtil.makeSuccess(subject.getSession().getId());
} catch (Exception e) {
return ResponseUtil.makeError(ErrorCode.LOGIN_ERROR);
}
}
/**
* 当前用户退出登录
*
* @return Response
*/
@PostMapping("/logout")
public Response logout() {
// 从缓存中删除缓存
loginService.removeSessionBySessionId(SecurityUtils.getSubject().getSession().getId().toString());
SecurityUtils.getSubject().logout();
return ResponseUtil.makeSuccess(ErrorCode.LAY_OUT_SUCCESS);
}
/**
* 生成验证码
*
* @return Response
*/
@PostMapping("/captcha")
public Response captcha() {
try {
return ResponseUtil.makeSuccess(loginService.generateVerificationCode());
} catch (Exception e) {
return ResponseUtil.makeError(ErrorCode.CAPTCHA_ERROR);
}
}
/**
* 获取在线用户
*
* @return Response
*/
@PostMapping("/listOnLine")
public Response listOnLine() {
return ResponseUtil.makeSuccess(loginService.listOnLineUser());
}
/**
* 踢出用户
*
* @param userName
* @return
*/
@RequestMapping("/kickOutUser")
@ResponseBody
public Response kickOutUser(String userName) {
return ResponseUtil.makeSuccess(loginService.forbiddenByUserName(userName));
}
/**
* 未登录,shiro应重定向到登录界面,此处返回未登录状态信息由前端控制跳转页面
*
* @return
*/
@RequestMapping(value = "/unAuthen")
public Response unAuthen() {
return ResponseUtil.makeError(ErrorCode.UNAUTHENTIC);
}
/**
* 未授权
*
* @return
*/
@RequestMapping(value = "/unAuthor")
public Response unAuthor() {
return ResponseUtil.makeError(ErrorCode.UNAUTHORIZED);
}
}
package com.shiro.api.controller;
import com.shiro.api.util.Response;
import com.shiro.api.util.ResponseUtil;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 业务接口Controller
*
* Created by Happy王子乐 on 2019/8/02.
*/
@RestController
public class HelloController {
/**
* 没有角色、权限限制,只要登录即可访问
*
* @return Response
*/
@RequestMapping("/hello")
public Response hello() {
return ResponseUtil.makeSuccess("无角色、权限限制接口,任何登录账号都可以请求该方法");
}
// 一些角色组合用法:
// 属于user角色
// @RequiresRoles("user")
// 必须同时属于user和admin角色
// @RequiresRoles({"user","admin"})
// 属于user或者admin之一;修改logical为OR 即可
// @RequiresRoles(value={"user","admin"},logical=Logical.OR)
/**
* 后面接口按照傻瓜场景进行权限演示:
* 角色:zhuren(主任) 、 teacher(老师)
* 权限:jiaoxuesheng(教学生)、shouxuefei(收学费)
*
* 于是:
* 主任可以教学生(宝刀未老的主任是由老师升职上去的),可以收学费
* 老师可以教学生,不可以收学费
*/
/**
* 拥有“zhuren”角色,登录后可正常访问
*
* @return Response
*/
@RequiresRoles("zhuren")
@RequestMapping("/role1")
public Response role1() {
return ResponseUtil.makeSuccess("zhuren角色正确,可以请求该方法");
}
/**
* 拥有“teacher”角色,登录后可正常访问
*
* @return Response
*/
@RequiresRoles("teacher")
@RequestMapping("/role2")
public Response role2() {
return ResponseUtil.makeSuccess("teacher角色正确,可以请求该方法");
}
/**
* 拥有“zhuren”角色和“teacher”角色,登录后可正常访问
*
* @return Response
*/
@RequiresRoles({"teacher", "zhuren"})
@RequestMapping("/role3")
public Response role3() {
return ResponseUtil.makeSuccess("zhuren角色和teacher角色都有,可以请求该方法");
}
/**
* 拥有“zhuren”角色或“teacher”角色,登录后可正常访问
*
* @return Response
*/
@RequiresRoles(value={"teacher", "zhuren"}, logical=Logical.OR)
@RequestMapping("/role4")
public Response role4() {
return ResponseUtil.makeSuccess("有zhuren角色或者有teacher角色,可以请求该方法");
}
/**
* 拥有“jiaoxuesheng”权限,登录后可正常访问
*
* @return Response
*/
@RequiresPermissions("jiaoxuesheng")
@RequestMapping("/permission1")
public Response permission1() {
return ResponseUtil.makeSuccess("jiaoxuesheng权限正确,可以请求该方法");
}
/**
* 拥有“shoubanfei”权限,登录后可正常访问
*
* @return Response
*/
@RequiresPermissions("shoubanfei")
@RequestMapping("/permission2")
public Response permission2() {
return ResponseUtil.makeSuccess("shoubanfei权限正确,可以请求该方法");
}
/**
* 拥有"jiaoxuesheng"权限和"shoubanfei"权限,登录后可正常访问
*
* @return Response
*/
@RequiresPermissions({"shoubanfei", "jiaoxuesheng"})
@RequestMapping("/permission3")
public Response permission3() {
return ResponseUtil.makeSuccess("拥有jiaoxuesheng权限和shoubanfei权限,可以请求该方法");
}
/**
* 拥有"jiaoxuesheng"权限或"shoubanfei"权限,登录后可正常访问
*
* @return Response
*/
@RequiresPermissions(value={"shoubanfei", "jiaoxuesheng"}, logical=Logical.OR)
@RequestMapping("/permission4")
public Response permission4() {
return ResponseUtil.makeSuccess("拥有jiaoxuesheng权限或shoubanfei权限,可以请求该方法");
}
}
- 接下来启动项目,运行成功后,可以看到8080端口已经启动
五、登录权限验证
- 当登录时,会进行认证操作,贴出相关代码:
- 启动Postman,我们先让“主任”登录:
- 可以看到,已经返回了token,登录成功
- 当账号/密码错误时(正确的密码是:zhuren),是禁止登录的,会进入拦截:
- 当账号登录管理后台时,需要带上菜单列表返回,主任的菜单权限是“教务管理”、“课程安排”,数学张老师的菜单权限是“课程安排”:
- 验证码登录,首先我们先生成验证码图片,调用接口时,在后台打印出生成的验证码数字(PS:方便后面传参):
- 调用登录接口,将生成验证码的token和验证码一起传入(PS:cToken 就是生成验证码的token):
六、接口请求权限验证
- 首先先贴出接口请求的过滤器,需要根据实际情况进行区分配置
- 接口代码1,当用户登录就可以请求:
- 当用户未登录时,请求接口:
- 反之,当用户登录后请求:
- 接口代码2,当用户符合角色,就可以请求接口:
- 当用户第一次请求角色/权限接口时,会进行初始授权
- 授权时,会进行数据库权限查询,并赋值到授权信息中(如果授权后想重新授权,需要用户退出登录):
- 主任分别请求接口(PS:第一次请求角色类型接口,会初始角色权限):
- 角色相关的接口还可以进行“与、或”组合,接口代码3:
- 主任角色分别请求接口(role3由于没有teacher角色,权限不足;role4因为角色是或的关系,请求正常):
- 角色验证完毕,我们来验证权限,接口代码4:
- 主任用户分别请求接口:
- 数学张老师,分别请求接口(PS:老师收班费没有权限,所以请求被拒绝了):
- 同理,权限也可以进行 “与、或”操作,进行组合限制,接口代码5:
- 主任、数学张老师,请求省略。。。这里不贴请求结果了,自己试试吧
- GitHub:https://github.com/HappyWjl/auth-shiro/tree/master
基于shiro的权限控制系统,包括 登录权限、接口权限、菜单权限
如果该项目对您有帮助,您可以点右上角 “Star” 支持一下 谢谢!
或者您可以 “follow” 一下,该项目将持续更新,不断完善功能。
转载还请注明出处,谢谢了
QQ:820155406