介绍:
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。(网上介绍很多,可以自行百度),本文使用springboot+mybatisplus+shiro实现数据库动态的管理用户、角色、权限管理,在本文的最后我会提供源码的下载地址,想看到效果的小伙伴可以直接下载运行就ok了(需要在配置文件中的redis配置改为你自己的)
因为shiro的功能比较多,本章只介绍如下几个功能
-
当用户没有登陆时只能访问登陆界面
-
当用户登陆成功后,只能访问该用户下仅有的权限(如果涉及到左侧树菜单展示的时候有有一个思路就是把左侧菜单当成权限,角色和权限(这里可以想成菜单)关系绑定,通过用户去找对应的角色,角色和菜单绑定,不同角色展示不同菜单)
一 数据库设计
本文的数据库表为5个,分别是权限表、角色表、角色-权限关系表、用户表、用户-角色表
表的简单介绍:一个用户可以有多个角色,一个角色可以有多个权限,都是多对多
二项目结构
common:为公共的一些文件,这个项目比较简单只有一个mybatis plus的代码生成器
dao:为数据库操作mapper,这里的service我没有使用,controller直接调的mapper,少了些重复代码
entity:数据库实体bean对象
shiro:shiro的配置和自定义ream的实现
web:登录和首页控制器
三 引入依赖
这里使用的gradle项目,maven的话同理,这里是在shiro的build.gradle引入的,也是主要的依赖位置
plugins {
id 'java'
}
group 'com.lxy'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
dependencies {
compile project(":service")
compile project(":entity")
compile project(":common")
testCompile group: 'junit', name: 'junit', version: '4.12'
// https://mvnrepository.com/artifact/org.projectlombok/lombok
compile group: 'org.projectlombok', name: 'lombok', version: '1.18.4'
// https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring ,shiro引入
compile group: 'org.apache.shiro', name: 'shiro-spring', version: '1.4.0'
// https://mvnrepository.com/artifact/org.crazycake/shiro-redis,shiro+redis缓存插件
compile group: 'org.crazycake', name: 'shiro-redis', version: '3.2.1'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis
compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis', version: '2.0.6.RELEASE'
// https://mvnrepository.com/artifact/org.springframework/spring-webmvc
compile group: 'org.springframework', name: 'spring-webmvc', version: '5.0.0.RELEASE'
// https://mvnrepository.com/artifact/net.mingsoft/shiro-freemarker-tags ,ftl页面shiro标签
compile group: 'net.mingsoft', name: 'shiro-freemarker-tags', version: '1.0.0'
// https://mvnrepository.com/artifact/org.apache.tomcat.embed/tomcat-embed-core
compile group: 'org.apache.tomcat.embed', name: 'tomcat-embed-core', version: '9.0.13'
}
四 编辑 application.properties
spring.banner.charset=UTF-8
server.tomcat.uri-encoding=UTF-8
spring.http.encoding.charset=UTF-8
spring.http.encoding.enabled=true
spring.http.encoding.force=true
spring.messages.encoding=UTF-8
spring.freemarker.charset=UTF-8
mybatis-plus.mapper-locations=classpath:mapper/*.xml
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0
logging.level.com.welsee.mapper=debug
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/dbshiro?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
#dataSource Pool configuration
spring.datasource.tomcat.initialSize=5
spring.datasource.tomcat.minIdle=5
spring.datasource.tomcat.max-active=20
spring.datasource.tomcat.maxWait=60000
spring.datasource.tomcat.timeBetweenEvictionRunsMillis=60000
spring.datasource.tomcat.minEvictableIdleTimeMillis=300000
spring.datasource.tomcat.validationQuery=SELECT 1 FROM DUAL
spring.datasource.tomcat.testWhileIdle=true
spring.datasource.tomcat.testOnBorrow=false
spring.datasource.tomcat.testOnReturn=false
spring.datasource.tomcat.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
#redis设置-宿舍
#spring.redis.host=192.168.0.102
#redis设置-公司
spring.redis.host=192.168.10.215
#redis设置-端口号
spring.redis.port=6379
#redis设置-密码
spring.redis.redisPassword=
#redis连接超时时间(毫秒)
spring.redis.timeout=10000
# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
spring.redis.database=0
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
#项目端口号
server.port=8082
五 创建ShiroConfig配置
package com.lxy.shiro;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.util.StringUtils;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration //@Configuration标注在类上,相当于把该类作为spring的xml配置文件中的<beans>,作用为:配置spring容器(应用上下文)
@Slf4j
public class ShiroConfig {
//获取application.properties参数,此处不能加static关键字
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.redisPassword}")
private String redisPassword;
// 下面两个方法对 注解权限起作用有很大的关系,请把这两个方法,放在配置的最上面,
/**
* Shiro生命周期处理器
*
* @return
*/
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
log.info("===============(1)Shiro生命周期周期处理器设置");
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)
* 和AuthorizationAttributeSourceAdvisor)即可实现此功能
*
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
log.info("===============(6)开启Shiro后台注解");
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Shiro的Web过滤器
*
* @param securityManager 项目
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//Shiro的核心安全接口,这个属性是必须的
shiroFilterFactoryBean.setSecurityManager(securityManager);
//要求登录时的链接(可根据项目的URL进行替换),非必须的属性,默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/");
//用户访问未对其授权的资源时,所显示的连接
shiroFilterFactoryBean.setUnauthorizedUrl("/notlogin");
//设置拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//开放登录接口
filterChainDefinitionMap.put("/login", "anon");
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
//游客,开发权限
filterChainDefinitionMap.put("/guest/**", "anon");
// //用户,需要角色权限 “user”
// filterChainDefinitionMap.put("/user/**", "roles[user]");
// //管理员,需要角色权限 “admin”
// filterChainDefinitionMap.put("/admin/**", "roles[admin]");
//<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
log.info("===============(5)Shiro拦截器工厂类注入成功");
return shiroFilterFactoryBean;
}
/**
* 注入SecurityManager,SecurityManager 是 Shiro 架构的核心,
* 通过它来链接Realm和用户(文档中称之为Subject.)
*
* @return
*/
@Bean
public SecurityManager securityManager() {
log.info("===============(2)注入securityManager开始");
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置realm
securityManager.setRealm(realm());
//自定义缓存实现,使用redis(暂时不知道有什么用)
securityManager.setCacheManager(redisCacheManager());
return securityManager;
}
/**
* 自定义身份认证 realm
* 需要将自定义的密码匹配器注入到Realm中
* realm就是一个安全数据源。可以将其看作为数据库的另一层封装,连接了应用和db
*
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
@ConditionalOnMissingBean
public Realm realm() {
AuthRealm authRealm = new AuthRealm();
//根据情况使用缓存器
authRealm.setCacheManager(redisCacheManager());
return authRealm;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisCacheManager redisCacheManager() {
log.info("===============(3)创建缓存管理器RedisCacheManager");
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
//redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
redisCacheManager.setPrincipalIdFieldName("id");
//用户权限信息缓存时间
redisCacheManager.setExpire(200000);
return redisCacheManager;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*
* @return
*/
@Bean
public RedisManager redisManager() {
log.info("===============(4)创建RedisManager,连接Redis..URL= " + host + ":" + port);
RedisManager redisManager = new RedisManager();
redisManager.setHost(host + ":" + port);//老版本是分别setHost和setPort,新版本只需要setHost就可以了
if (!StringUtils.isEmpty(redisPassword)) {
redisManager.setPassword(redisPassword);
}
return redisManager;
}
}
六 自字义 Realm
package com.lxy.shiro;
import com.lxy.entity.User;
import com.lxy.mapper.PermissionMapper;
import com.lxy.mapper.RoleMapper;
import com.lxy.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
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.springframework.beans.factory.annotation.Autowired;
import java.util.Set;
@Slf4j
public class AuthRealm extends AuthorizingRealm {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private PermissionMapper permissionMapper;
/**
* 用户信息认证是在用户进行登录的时候进行验证(不存redis)
*
* @param token 用户登录的账号密码信息
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("===============Shiro用户认证开始");
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
//获取登录用户名
String username = String.valueOf(usernamePasswordToken.getUsername());
User user = userMapper.getUserByUserName(username);
AuthenticationInfo authenticationInfo = null;
//如果有此用户名的用户,则判断输入的密码和数据库中的密码是否一致
if (user != null) {
String password = new String(usernamePasswordToken.getPassword());
if (password.equals(user.getPassword())) {
authenticationInfo = new SimpleAuthenticationInfo(
user, //用户实体对象,不能只是用户名,因为redis中针对不同用户缓存使用的是id,这里赋值用户名的话则会找不到id
user.getPassword(), //密码
getName() //realm name
);
log.info("===============Shiro用户认证成功");
}
}
return authenticationInfo;
}
/**
* 权限信息认证是用户访问controller的时候才进行验证(redis存储的也是权限信息)
*
* @param principals 身份信息
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("===============Shiro权限认证开始");
//获取身份认证时设置的用户名(SimpleAuthenticationInfo)
User user = (User) principals.getPrimaryPrincipal();
String username = user.getUsername();
if (username != null && !("".equals(username))) {
//通过用户名获取该用户所属角色名称
Set<String> roleName = roleMapper.getRoleByUserName(username);
//通过用户名获取该用户所拥有权限的名称
Set<String> permName = permissionMapper.getPermissionNameByUserName(username);
//设置用户角色和权限
SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();
authenticationInfo.setRoles(roleName);
authenticationInfo.setStringPermissions(permName);
log.info("===============Shiro权限认证成功");
return authenticationInfo;
}
return null;
}
/**
* 清除当前用户的权限认证缓存
*
* @param principals 权限信息
*/
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
}
七 异常处理类
package com.lxy.shiro;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Shiro异常处理类,(@ControllerAdvice用来处理异常)
*/
@ControllerAdvice
public class GlobalDefaultExceptionHandler {
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public String defaultAuthorizedExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception e) {
return defaultException(request, response);
}
@ExceptionHandler(UnauthenticatedException.class)
@ResponseBody
public String defaultAuthenticatedExceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception e) {
return defaultException(request, response);
}
private String defaultException(HttpServletRequest request, HttpServletResponse response) {
// try {
// if (isAjax(request)) {
// onLoginFail(response);
// return "";
// }
// else {
// response.sendRedirect("/notlogin");
// }
// }
// catch (Exception ex){
//
// }
return "您没有访问权限";
}
/**
这两个方法可用于以后权限异常后返回给前台的信息,前台可以根据返回的信息可以进行用户退登操作
public boolean isAjax(ServletRequest request){
String header = ((HttpServletRequest) request).getHeader("X-Requested-With");
if("XMLHttpRequest".equalsIgnoreCase(header)){
return Boolean.TRUE;
}
return Boolean.FALSE;
}
public void onLoginFail(HttpServletResponse response) throws IOException {
response.setHeader("sessionstatus", "timeout");
response.setHeader("basePath", "/notlogin");
}
**/
}
八 HomeController的代码,调用"/"就可以跳到登录页面
package com.lxy.controller;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.session.Session;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
@Controller
@Slf4j
public class HomeController {
/**
* 登录时的链接
*
* @param mv
* @return
*/
@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView index(ModelAndView mv) {
if (mv == null) {
new ModelAndView();
}
//获取shiro中的session
Session session = SecurityUtils.getSubject().getSession();
if (session.getAttribute("MEMBER_USER_KEY") == null) {
log.info("用户没有登录,即将跳转登录页面");
mv.setViewName("views/login");
} else {
log.info("用户已经登录,即将跳转登录页面");
mv.setViewName("views/index");
}
return mv;
}
/**
* 用户登录后的首页
*
* @return
*/
@RequestMapping(value = "index", method = RequestMethod.GET)
public ModelAndView index() {
ModelAndView mv = new ModelAndView();
mv.setViewName("views/index");
return mv;
}
/**
* 错误页面
*
* @return
*/
@RequestMapping(value = "guest/error", method = RequestMethod.GET)
public ModelAndView error() {
ModelAndView mv = new ModelAndView();
mv.setViewName("views/error");
return mv;
}
/**
* 创建学校页面
*
* @return
*/
@RequiresRoles("ROLE_ADMIN")
@RequiresPermissions("school:create")
@RequestMapping(value = "createSchool", method = RequestMethod.GET)
public ModelAndView createSchool() {
ModelAndView mv = new ModelAndView();
mv.setViewName("views/createSchool");
return mv;
}
/**
* 创建学生页面
*
* @return
*/
@RequiresRoles("ROLE_SCHOOLADMIN")
@RequiresPermissions("student:create")
@RequestMapping(value = "createStudent")
public ModelAndView createStudent() {
ModelAndView mv = new ModelAndView();
mv.setViewName("views/createStudent");
return mv;
}
}
九 Index.ftl页面
<html>
<head>
<title></title>
</head>
<body>
<@shiro.hasAnyRoles name="ROLE_ADMIN,ROLE_SCHOOLADMIN">
您好,欢迎
</@shiro.hasAnyRoles>
<@shiro.hasRole name="ROLE_ADMIN">
超级管理员,您可以<a href="/createSchool">创建校管理员用户</a>
</@shiro.hasRole>
<@shiro.hasRole name="ROLE_SCHOOLADMIN">
校管理员,您可以<a href="/createStudent">创建学生用户</a>
</@shiro.hasRole>
<br>
<a href="/logout">退出登录</a>
</body>
</html>
十 项目测试
-
用户admin对应的角色名称为ROLE_ADMIN,角色ROLE_ADMIN对应的权限是school:create,
所以用admin进入页面后拥有“创建校管理员用户”的按钮,并且可以进入该按钮下的方法。
-
用户lixinyu对应的角色名称为ROLE_SCHOOLADMIN,角色ROLE_SCHOOLADMIN对应的权限是student:create,所以用lixinyu进入页面后拥有“创建学生用户”俺就,并且可以进入该按钮下的方法。
-
如果admin调用lixinyu的穿件学生用户接口会抛出没有权限异常,然后被shiro捕获到执行Shiro异常处理类的方法
-
redis的作用就是用户第一次登录的时候回去验证用户的角色和权限,然后将权限存入redis,当第二次登录的时候只会去realm中执行doGetAuthenticationInfo验证身份,并不会去doGetAuthorizationInfo验证权限
项目git地址: https://github.com/lyz8jj0/shiro-demo