本篇博客主要是学习shiro权限管理系统的一篇入门博客,已代码为主要内容。
shiro介绍
Shiro是Apache下的一个开源项目,我们称之为Apache Shiro。它是一个很易用与Java项目的的安全框架,提供了认证、授权、加密、会话管理,与Spring Security 一样都是做一个权限的安全框架,但是与Spring Security 相比,在于 Shiro 使用了比较简单易懂易于使用的授权方式。shiro属于轻量级框架,相对于security简单的多,也没有security那么复杂。所以我这里也是简单介绍一下shiro的使用。
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。
Authentication(认证):用户身份识别,通常被称为用户“登录”
Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
Session Management(会话管理):特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
shiro的三大组建
Subject:可以看成是用户,虽然这样理解不一定正确,但是这样却好理解,可以由subject来对用户进行一个封装,通过subject来判断这个用户的权限等信息。
SecurityManager:安全管理器,Shiro的核心,用于管理所有的Subject ,它主要用于协调Shiro内部各种安全组件,例如Realm,Session,Cache等,不过我们一般不用太关心SecurityManager,对于应用程序开发者来说,主要还是使用Subject的API来处理各种安全验证逻辑。
Realm:这是用于连接Shiro和客户系统的用户数据的桥梁。一旦Shiro真正需要访问各种安全相关的数据(比如使用用户账户来做用户身份验证以及权限验证)时,他总是通过调用系统配置的各种Realm来读取数据。
正因为如此,Realm往往被看做是安全领域的DAO,他封装了数据源连接相关的细节,将数据以Shiro需要的格式提供给Shiro。
Subject (org.apache.shiro.subject.Subject),如上所述.
SecurityManager (org.apache.shiro.mgt.SecurityManager),如上所述.
Authenticator(用户认证管理器), (org.apache.shiro.authc.Authenticator) 这个组件主要用于处理用户登录逻辑,他通过调用Realm的接口来判断当前登录的用户的身份。
用户认证策略,(org.apache.shiro.authc.pam.AuthenticationStrategy) 如果系统配置了多个Realm,则需要使用AuthenticationStrategy 来协调这些Realm以便决定一个用户登录的认证是成功还是失败。(比如,如果一个Realm验证成功了,但是其他的都失败了,怎么算?还是说都成功才算成功).
Authorizer(权限管理器)(org.apache.shiro.authz.Authorizer)这个组件主要是用来做用户的访问控制。通俗来说就是决定用户能做什么、不能做什么。和Authenticator类似,Authorizer也知道怎么协调多个Realm数据源的数据,他有自己的一套策略。
SessionManager(会话管理器) (org.apache.shiro.session.mgt.SessionManager) SessionManager知道如何创建会话、管理用户回话的声明周期以便在所有运行环境下都可以给用户提供一个健壮的回话管理体验。Shiro在任何运行环境下都可以在本地管理用户会话(即便没有Web或者EJB容器也可以)——这在安全管理的框架中算是独门绝技了。当然,如果当前环境中有会话管理机制(比如Servlet容器),则Shiro默认会使用该环境的会话管理机制。而如果像控制台程序这种独立的应用程序,本身没有会话管理机制,此时Shiro就会使用内部的会话管理器来给应用的开发提供一直的编程体验。SessionDAO允许用户使用任何类型的数据源来存储Session数据。
SessionDAO (org.apache.shiro.session.mgt.eis.SessionDAO) 用于代替SessionManager执行Session相关的增删改查。这个接口允许我们将任意种类的数据存储方式引入到Session管理的基础框架中。
CacheManager (org.apache.shiro.cache.CacheManager) CacheManager用于创建和维护一些在其他的Shiro组件中用到的Cache实例,维护这些Cache实例的生命周期。缓存用于存储那些从后端获取到的用户验证与权限控制方面的数据以提高性能,缓存是一等公民,在获取数据时,总是先从缓存中查找,如果没有再调用后端接口从其他数据源获取。Shiro允许用户使用其他更加现代的、企业级的数据源来替代内部的默认实现,以提供更高的性能和更好的用户体验。
Cryptography 加密技术,(org.apache.shiro.crypto.*) 对于一个企业级的安全框架来说,加密算是其固有的一种特性。Shiro的crypto包中包含了一系列的易于理解和使用的加密、哈希(aka摘要)辅助类。这个包内的所有类都是经过精心设计,相比于java本身提供的那一套反人类的加密组件,Shiro提供的这套加密组件简直不要好用太多。
Realm (org.apache.shiro.realm.Realm) 就如上文所提到的,Realm是连接Shiro和你的安全数据的桥梁。任何时候当Shiro需要执行登录或者访问控制的时候,都需要调用已经配置的Realm的接口去获取数据。一个应用程序可以配置一个或者多个Realm(最少配置一个)。
下面是一个用户通过shiro登录的流程图
1.使用用户的登录信息创建令牌
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password,rememberMe);
2.执行登陆动作
//获取user封装对象,可以将它看成是一个操作用户的入口
Subject subject = SecurityUtils.getSubject();
subject.login(usernamePasswordToken);//执行登录操作
Session session = subject.getSession();获取登录后的session
SysUser user=(SysUser) subject.getPrincipal();
//更新用户登录时间,也可以在ShiroRealm里面做
session.setAttribute("user", user);//将用户信息存入到session中
session.setTimeout(360000);
3.判断用户
Shiro本身无法知道所持有令牌的用户是否合法,因为除了项目的设计人员恐怕谁都无法得知。因此Realm是整个框架中为数不多的必须由设计者自行实现的模块,当然Shiro提供了多种实现的途径,本文只介绍最常见也最重要的一种实现方式——数据库查询。
会调用自己写的Realm里面的doGetAuthenticationInfo和doGetAuthorizationInfo进行用户验证和权限授权。
下面是这个项目的代码,本项目是springboot+shiro+mybatis+redis。其中springboot的视图解析器用的是freemark,实现了用户的登录验证,相关权限授权,记住我,将用户权限存入redis,避免每次验证权限的时候都去数据库中查询。本来也想着实现将session存入到redis中的,但是最后这个功能没有实现。
下面首先是数据库的设计:总共五张表:用户表,角色表,权限表,用户角色表,角色权限表
CREATE TABLE `sys_user` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT '' COMMENT '用户名',
`password` varchar(256) DEFAULT NULL COMMENT '登录密码',
`name` varchar(256) DEFAULT NULL COMMENT '用户真实姓名',
`id_card_num` varchar(256) DEFAULT NULL COMMENT '用户身份证号',
`state` char(3) DEFAULT '0' COMMENT '用户状态:0:正常状态,1:用户被锁定',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`uid`),
UNIQUE KEY `username` (`username`) USING BTREE,
UNIQUE KEY `id_card_num` (`id_card_num`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`state` char(1) DEFAULT '0' COMMENT '是否可用0可用 1不可用',
`role` varchar(20) DEFAULT NULL COMMENT '角色标识程序中判断使用,如"admin"',
`description` varchar(100) DEFAULT NULL COMMENT '角色描述,UI界面显示使用',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `role` (`role`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`parent_id` int(11) DEFAULT NULL COMMENT '父编号,本权限可能是该父编号权限的子权限',
`parent_ids` varchar(20) DEFAULT NULL COMMENT '父编号列表',
`permission` varchar(100) DEFAULT NULL COMMENT '权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view',
`resource_type` varchar(20) DEFAULT NULL COMMENT '资源类型,[menu|button]',
`url` varchar(200) DEFAULT NULL COMMENT '资源路径 如:/userinfo/list',
`name` varchar(50) DEFAULT NULL COMMENT '权限名称',
`available` char(1) DEFAULT '0' COMMENT '是否可用0可用 1不可用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
CREATE TABLE `sys_user_role` (
`uid` int(11) DEFAULT NULL COMMENT '用户id',
`role_id` int(11) DEFAULT NULL COMMENT '角色id',
KEY `uid` (`uid`) USING BTREE,
KEY `role_id` (`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `sys_role_permission` (
`role_id` int(11) DEFAULT NULL COMMENT '角色id',
`permission_id` int(11) DEFAULT NULL COMMENT '权限id',
KEY `role_id` (`role_id`) USING BTREE,
KEY `permission_id` (`permission_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `sys_user` (`uid`, `username`, `password`, `name`, `id_card_num`, `state`, `create_time`)
VALUES
(1, 'zhangsan', '123456', '张三', '422201199107280987', '1', '2019-08-07 11:21:00'),
(2, 'lisi', '123456', '李四', '322213199509082345', '-1', '2019-08-07 11:21:00'),
(3, 'wangwu', '123456', '王五', '12345678908', '-1', '2019-08-14 09:52:04'),
(4, 'chenliu', '123456', '陈六', '243545342', '-1', '2019-08-14 10:11:12'),
(5, 'qianqi', '666666', '钱七', '38478483993', '1', '2019-08-14 10:12:24');
INSERT INTO `sys_role` (`id`, `state`, `role`, `description`, `create_time`)
VALUES
(1, '1', '超级管理员', '所有权限都有', '2019-08-07 11:24:00'),
(2, '1', '普通操作员', '查看权限,新增权限', '2019-08-07 11:24:00'),
(3, '1', '游客', '查看权限', '2019-08-14 22:56:00');
INSERT INTO `sys_permission` (`id`, `parent_id`, `parent_ids`, `permission`, `resource_type`, `url`, `name`, `available`)
VALUES
(1, NULL, NULL, 'user:selectSysUser', 'menu', NULL, '用户列表', '1'),
(2, 1, NULL, 'user:addSysUser', 'button', NULL, '添加用户', '1'),
(3, NULL, NULL, 'user:updateSysUser', 'button', NULL, '更改状态', '0');
INSERT INTO `sys_user_role` (`uid`, `role_id`)
VALUES
(1, 1),
(2, 2),
(3, 3);
INSERT INTO `sys_role_permission` (`role_id`, `permission_id`)
VALUES
(1, 1),
(1, 2),
(2, 1),
(1, 3),
(2, 2),
(3, 1);
下面是pom.xml
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.6.RELEASE
study.springboot
springbootShiro
0.0.1-SNAPSHOT
demo
Demo project for Spring Boot
1.8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-web
RELEASE
compile
org.springframework.boot
spring-boot-starter-freemarker
2.1.6.RELEASE
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
mysql
mysql-connector-java
runtime
com.alibaba
druid
1.1.10
log4j
log4j
1.2.17
com.baomidou
mybatis-plus-boot-starter
2.3
org.apache.velocity
velocity-engine-core
2.0
org.springframework.boot
spring-boot-starter-thymeleaf
org.apache.shiro
shiro-spring
1.4.0
com.github.theborakompanioni
thymeleaf-extras-shiro
2.0.0
net.mingsoft
shiro-freemarker-tags
0.1
javax.servlet
javax.servlet-api
org.springframework.boot
spring-boot
2.1.6.RELEASE
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
2.9.0
cn.hutool
hutool-all
4.6.1
org.springframework.boot
spring-boot-maven-plugin
配置文件
############################################################
#
# freemarker配置
#
############################################################
#模板文件路径(不推荐使用)
spring.freemarker.template-loader-path=classpath:/templates
#关闭缓存即时刷新,生产环境需要改成true;
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.check-template-location=true
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.freemarker.request-context-attribute=request
spring.freemarker.suffix=.html
############################################################
#
# 数据库 druid配置
#
############################################################
# 数据库访问配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=gbk&zeroDateTimeBehavior=convertToNull&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
# 下面为连接池的补充设置,应用到上面所有数据源中
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=SELECT 1 FROM DUAL
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.filters=stat,wall,log4j
spring.datasource.logSlowSql=true
# 注意:对应实体类的路径,多个package之间可以用逗号
#mybatis.type-aliases-package=study.springboot.demo.entity
#注意:一定要对应mapper映射xml文件的所在路径
#mybatis.mapper-locations=classpath*:mapper/*.xml
mybatis-plus.mapper-locations=classpath:/mapper/*.xml
mybatis-plus.typeAliasesPackage=study.springboot.demo.entity
mybatis-plus.configuration.mapUnderscoreToCamelCase=true
#sql日志输出
logging.level.study.springboot.demo.dao=debug
############################################################
#
# 缓存 redis配置
#
############################################################
# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0
ShiroConfig的配置,可以看成是shiro的核心
package study.springboot.demo.config;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
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.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.Cookie;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import study.springboot.demo.shiro.RedisSessionDAO;
import study.springboot.demo.shiro.ShiroRedisCacheManager;
import study.springboot.demo.shiro.ShiroRealm;
import java.util.*;
/**
* 参考博客:https://blog.csdn.net/qq_34021712/article/details/80294417
*
* @Author wangbiao
* @Date 2019-07-29 20:43
* @Decripition TODO
**/
@Configuration
public class ShiroConfig {
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
* Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截
* @param securityManager
* @return
*/
@Bean(name = "shirFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//必须设置 SecurityManager,Shiro的核心安全接口
shiroFilterFactoryBean.setSecurityManager(securityManager);
//这里的/login是后台的接口名,非页面,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
//这里的/index是后台的接口名,非页面,登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授权界面,该配置无效,并不会进行页面跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
//自定义拦截器限制并发人数,参考博客:
//LinkedHashMap filtersMap = new LinkedHashMap<>();
//限制同一帐号同时在线的个数
//filtersMap.put("kickout", kickoutSessionControlFilter());
//shiroFilterFactoryBean.setFilters(filtersMap);
// 配置访问权限 必须是LinkedHashMap,因为它必须保证有序
// 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 一定要注意顺序,否则就不好使了
LinkedHashMap filterChainDefinitionMap = new LinkedHashMap<>();
//配置不登录可以访问的资源,anon 表示资源都可以匿名访问
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/toLogin", "anon");
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/druid/**", "anon");
//logout是shiro提供的过滤器
filterChainDefinitionMap.put("/logout", "anon");
filterChainDefinitionMap.put("/novelMain/*", "anon");
//此时访问/userInfo/del需要del权限,在自定义Realm中为用户授权。
//filterChainDefinitionMap.put("/userInfo/del", "perms[\"userInfo:del\"]");
//其他资源都需要认证 authc 表示需要认证才能进行访问 user表示配置记住我或认证通过可以访问的地址
filterChainDefinitionMap.put("/**", "user");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
*
* 配置核心安全事务管理器,用来协调shiro的各个组建,她相当于是个大管家
*
* @param shiroRealm
* @return
*/
@Bean(name="securityManager")
public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置自定义realm.用于权限的验证和授权
securityManager.setRealm(shiroRealm);
//配置记住我 ,利用cookie记住用户的登录信息,即使关掉浏览器,下次打开网页也是登录状态
securityManager.setRememberMeManager(rememberMeManager());
//配置 redis缓存管理器 参考博客:https://blog.csdn.net/qq_31897023/article/details/89082541
//这样做的目的是将权限信息存入到数据库中,避免了每次权限验证的时候都需要去从数据库里面查找这个用户对应的权限
securityManager.setCacheManager(cacheManager());
//配置自定义session管理,使用redis 如果在集群的基础上实现shiro session共享
//securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 起到授权和验证的作用,在登录时验证用户名和密码
* 在访问的时候起到权限的作用
* 身份认证realm; (这个需要自己写,账号密码校验;权限等)
* @return
*/
@Bean
public ShiroRealm shiroRealm(){
ShiroRealm shiroRealm = new ShiroRealm();
return shiroRealm;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/** * Shiro生命周期处理器 * @return */
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){ return new LifecycleBeanPostProcessor(); }
/**
* 解决: 无权限页面不跳转 shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized") 无效
* shiro的源代码ShiroFilterFactoryBean.Java定义的filter必须满足filter instanceof AuthorizationFilter,
* 只有perms,roles,ssl,rest,port才是属于AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,
* 所以unauthorizedUrl设置后页面不跳转 Shiro注解模式下,登录失败与没有权限都是通过抛出异常。
* 并且默认并没有去处理或者捕获这些异常。在SpringMVC下需要配置捕获相应异常来通知用户信息
* @return
*/
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
SimpleMappingExceptionResolver simpleMappingExceptionResolver=new SimpleMappingExceptionResolver();
Properties properties=new Properties();
//这里的 /unauthorized 是页面,不是访问的路径
properties.setProperty("org.apache.shiro.authz.UnauthorizedException","/unauthorized");
properties.setProperty("org.apache.shiro.authz.UnauthenticatedException","/unauthorized");
simpleMappingExceptionResolver.setExceptionMappings(properties);
return simpleMappingExceptionResolver;
}
/**
* 记住我功能在各各网站是比较常见的,实现起来也都差不多,主要就是利用cookie来实现,而shiro对记住我功能的实现也是比较简单的,只需要几步即可。
* Shiro提供了记住我(RememberMe)的功能,比如访问一些网站时,关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:
* (1)、首先在登录页面选中RememberMe然后登录成功;如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并保存下来;
* (2)、关闭浏览器再重新打开;会发现浏览器还是记住你的;
* (3)、访问一般的网页服务器端还是知道你是谁,且能正常访问;
*
*
*
* cookie对象;会话Cookie模板 ,默认为: JSESSIONID 问题: 与SERVLET容器名冲突,重新定义为sid或rememberMe,自定义
* @return
*/
@Bean
public SimpleCookie rememberMeCookie(){
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//setcookie的httponly属性如果设为true的话,会增加对xss防护的安全系数。它有以下特点:
//setcookie()的第七个参数
//设为true后,只能通过http访问,javascript无法访问
//防止xss读取cookie
simpleCookie.setHttpOnly(true);
simpleCookie.setPath("/");
//
simpleCookie.setMaxAge(2592000);
return simpleCookie;
}
/**
* cookie管理对象;记住我功能,rememberMe管理器
* @return
*/
@Bean
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
/**
* FormAuthenticationFilter 过滤器 过滤记住我
* @return
*/
@Bean
public FormAuthenticationFilter formAuthenticationFilter(){
FormAuthenticationFilter formAuthenticationFilter = new FormAuthenticationFilter();
//对应前端的checkbox的name = rememberMe
formAuthenticationFilter.setRememberMeParam("rememberMe");
return formAuthenticationFilter;
}
/*******start************下面是使用redis进行缓存管理********start*********/
@Bean
public ShiroRedisCacheManager cacheManager(){
return new ShiroRedisCacheManager();
}
/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO(){
return new RedisSessionDAO();
}
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
sessionManager.setGlobalSessionTimeout(60*60*24);
sessionManager.setCacheManager(cacheManager());
sessionManager.setDeleteInvalidSessions(true);//删除过期的session
sessionManager.setSessionIdCookieEnabled(true);
sessionManager.setSessionIdCookie(sessionIdCookie());
return sessionManager;
}
//设置cookie
@Bean
public Cookie sessionIdCookie(){
Cookie sessionIdCookie=new SimpleCookie("STID");
sessionIdCookie.setMaxAge(-1);
sessionIdCookie.setHttpOnly(true);
return sessionIdCookie;
}
}
shiroRealm,用户用户验证和权限授权的,链接数据库里面的用户信息
package study.springboot.demo.shiro;
import org.apache.shiro.SecurityUtils;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import study.springboot.demo.dao.SysPermissionMapper;
import study.springboot.demo.dao.SysRoleMapper;
import study.springboot.demo.dao.SysUserMapper;
import study.springboot.demo.entity.SysPermission;
import study.springboot.demo.entity.SysRole;
import study.springboot.demo.entity.SysUser;
import java.util.List;
/**
* @Author wangbiao
* @Date 2019-07-29 21:17
* @Decripition TODO 在Shiro中,最终是通过Realm来获取应用程序中的用户、角色及权限信息的
* 在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。可以说,Realm是专用于安全框架的DAO.
**/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
@Autowired
private SysPermissionMapper sysPermissionMapper;
@Override
public AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)authenticationToken;
String name = usernamePasswordToken.getUsername();
String password = new String(usernamePasswordToken.getPassword());
SysUser sysUserQuery = new SysUser();
sysUserQuery.setUsername(name);
SysUser sysUser = sysUserMapper.selectOne(sysUserQuery);
if(sysUser==null){
throw new UnknownAccountException("用户名或密码错误!");
}
if(!password.equals(sysUser.getPassword())){
throw new IncorrectCredentialsException("用户名或密码错误!");
}
if ("0".equals(sysUser.getState())) {
throw new LockedAccountException("账号已被锁定,请联系管理员!");
}
//盐值加密
ByteSource byteSource = ByteSource.Util.bytes(sysUser.getUsername());
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(sysUser,sysUser.getPassword(),byteSource,getName());
return info;
}
/**
* 授权用户权限
* 授权的方法是在碰到 标签的时候调用的
* 它会去检测shiro框架中的权限(这里的permissions)是否包含有该标签的name值,如果有,里面的内容显示
* 如果没有,里面的内容不予显示(这就完成了对于权限的认证.)
*
* shiro的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();
* 当访问到页面的时候,链接配置了相应的权限或者shiro标签才会执行此方法否则不会执行
* 所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回null即可。
*
* 在这个方法中主要是使用类:SimpleAuthorizationInfo 进行角色的添加和权限的添加。
* authorizationInfo.addRole(role.getRole()); authorizationInfo.addStringPermission(p.getPermission());
*
* 当然也可以添加set集合:roles是从数据库查询的当前用户的角色,stringPermissions是从数据库查询的当前用户对应的权限
* authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(stringPermissions);
*
* 就是说如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "perms[权限添加]");
* 就说明访问/add这个链接必须要有“权限添加”这个权限才可以访问
*
* 如果在shiro配置文件中添加了filterChainDefinitionMap.put("/add", "roles[100002],perms[权限添加]");
* 就说明访问/add这个链接必须要有 "权限添加" 这个权限和具有 "100002" 这个角色才可以访问
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取用户
SysUser user = (SysUser) SecurityUtils.getSubject().getPrincipal();
//获取用户角色
List roles =this.sysRoleMapper.findRolesByUserId(user.getUid());
//添加角色
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
for (SysRole role : roles) {
authorizationInfo.addRole(role.getRole());
}
//获取用户权限
List permissions = this.sysPermissionMapper.findPermissionsByRoleId(roles);
//添加权限
for (SysPermission permission:permissions) {
authorizationInfo.addStringPermission(permission.getPermission());
}
return authorizationInfo;
}
}
登录控制类
package study.springboot.demo.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap;
import study.springboot.demo.entity.SysUser;
/**
* @Author wangbiao
* @Date 2019-08-06 09:14
* @Decripition TODO
**/
@Controller
@RequestMapping("/")
public class LoginController {
@RequestMapping("/login")
public String toLogin(){
return "login";
}
/**
* 登出 这个方法没用到,用的是shiro默认的logout
* @param model
* @return
*/
@RequestMapping("/logout")
public String logout(Model model) {
Subject subject = SecurityUtils.getSubject();
subject.logout();
model.addAttribute("msg","安全退出!");
return "login";
}
@RequestMapping("/toLogin")
public String login(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam(value = "rememberMe",required = false)String rememberMe,RedirectAttributesModelMap model){
boolean rememberMeB = false;
if("true".equals(rememberMe)){
rememberMeB = true;
}
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
try{
usernamePasswordToken.setRememberMe(rememberMeB);//记住我
subject.login(usernamePasswordToken);
Session session = subject.getSession();
SysUser user=(SysUser) subject.getPrincipal();
//更新用户登录时间,也可以在ShiroRealm里面做
session.setAttribute("user", user);
session.setTimeout(360000);
}catch (UnknownAccountException uae) {
model.addFlashAttribute("error", "未知用户");
return redirectTo("/login");
} catch (IncorrectCredentialsException ice) {
model.addFlashAttribute("error", "密码错误");
return redirectTo("/login");
} catch (LockedAccountException lae) {
model.addFlashAttribute("error", "账号已锁定");
return redirectTo("/login");
}
catch (AuthenticationException ae) {
//unexpected condition? error?
model.addFlashAttribute("error", "服务器繁忙");
return redirectTo("/login");
}
subject.login(usernamePasswordToken);
return "index";
}
/**
* 重定向至地址 url
*
* @param url
* 请求地址
* @return
*/
protected String redirectTo( String url ) {
StringBuffer rto = new StringBuffer("redirect:");
rto.append(url);
return rto.toString();
}
}
/**
* 集成html页面的Shiro标签 ,freemark为解析器的时候一定要用到这个
* Created by Administrator on 2016/3/15.
*/
@Component
public class ShiroTagFreeMarkerConfigurer implements InitializingBean {
@Autowired
private Configuration configuration;
@Autowired
private FreeMarkerViewResolver resolver;
@Override
public void afterPropertiesSet() {
// 加上这句后,可以在页面上使用shiro标签
configuration.setSharedVariable("shiro", new ShiroTags());
// 加上这句后,可以在页面上用${context.contextPath}获取contextPath
resolver.setRequestContextAttribute("context");
}
}
package study.springboot.demo.shiro;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import study.springboot.demo.redis.JedisUtil;
import study.springboot.demo.redis.SerializableUtil;
import java.util.*;
/**
* 重写Shiro的Cache保存读取
* @author Wang926454
* @date 2018/9/4 17:31
*/
public class ShiroRedisCache implements Cache {
/**
* redis-key-前缀-shiro:cache:
*/
public final static String PREFIX_SHIRO_CACHE = "shiro:cache:";
/**
* 过期时间-5分钟
*/
private static final Integer EXPIRE_TIME = 5 * 60 * 1000;
/**
* 缓存的key名称获取为shiro:cache:account
* @param key
* @return java.lang.String
* @author Wang926454
* @date 2018/9/4 18:33
*/
private String getKey(Object key){
return PREFIX_SHIRO_CACHE + key.toString();
}
/**
* 获取缓存
*/
@Override
public Object get(Object key) throws CacheException {
if(!JedisUtil.exists(this.getKey(key))){
return null;
}
return JedisUtil.getObject(this.getKey(key));
}
/**
* 保存缓存
*/
@Override
public Object put(Object key, Object value) throws CacheException {
// 设置Redis的Shiro缓存
return JedisUtil.setObject(this.getKey(key), value, EXPIRE_TIME);
}
/**
* 移除缓存
*/
@Override
public Object remove(Object key) throws CacheException {
if(!JedisUtil.exists(this.getKey(key))){
return null;
}
JedisUtil.delKey(this.getKey(key));
return null;
}
/**
* 清空所有缓存
*/
@Override
public void clear() throws CacheException {
JedisUtil.getJedis().flushDB();
}
/**
* 缓存的个数
*/
@Override
public int size() {
Long size = JedisUtil.getJedis().dbSize();
return size.intValue();
}
/**
* 获取所有的key
*/
@Override
public Set keys() {
Set keys = JedisUtil.getJedis().keys(new String("*").getBytes());
Set
package study.springboot.demo.shiro;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
/**
* @Author wangbiao
* @Date 2019-08-16 18:13
* @Decripition TODO
**/
public class ShiroRedisCacheManager implements CacheManager {
@Override
public Cache getCache(String s) throws CacheException {
return new ShiroRedisCache<>();
}
}
package study.springboot.demo.shiro;
import java.io.Serializable;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import study.springboot.demo.redis.JedisUtil;
/**
* @Author wangbiao
* @Date 2019-08-17 23:48
* @Decripition TODO
**/
public class RedisSessionDAO extends EnterpriseCacheSessionDAO {
@Override
public void delete(Session session) {
if(session == null || session.getId() == null){
System.out.println("Session is null");
return;
}
JedisUtil.delKey(session.getId().toString());
}
@Override
public void update(Session session) throws UnknownSessionException {
if(session == null || session.getId() == null){
System.out.println("Session is null");
return;
}
Serializable sessionId = session.getId();
JedisUtil.setObject(sessionId.toString(), session);
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
//添加进redis
JedisUtil.setObject(sessionId.toString(), session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
return (Session)JedisUtil.getObject(sessionId.toString());
}
}
package study.springboot.demo.redis;
import study.springboot.demo.common.CustomException;
import java.io.*;
/**
* Serializable工具(JDK)(也可以使用Protobuf自行百度)
* @author Wang926454
* @date 2018/9/4 15:13
*/
public class SerializableUtil {
/**
* 序列化
* @param object
* @return byte[]
* @author Wang926454
* @date 2018/9/4 15:14
*/
public static byte[] serializable(Object object) {
ByteArrayOutputStream baos = null;
ObjectOutputStream oos = null;
try {
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
oos.writeObject(object);
byte[] bytes = baos.toByteArray();
return bytes;
} catch (IOException e) {
e.printStackTrace();
throw new CustomException("SerializableUtil工具类序列化出现IOException异常");
} finally {
try {
if(oos != null) {
oos.close();
}
if(baos != null) {
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 反序列化
* @param bytes
* @return java.lang.Object
* @author Wang926454
* @date 2018/9/4 15:14
*/
public static Object unserializable(byte[] bytes) {
ByteArrayInputStream bais = null;
ObjectInputStream ois = null;
try {
bais = new ByteArrayInputStream(bytes);
ois = new ObjectInputStream(bais);
return ois.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
throw new CustomException("SerializableUtil工具类反序列化出现ClassNotFoundException异常");
} catch (IOException e) {
e.printStackTrace();
throw new CustomException("SerializableUtil工具类反序列化出现IOException异常");
} finally {
try {
if(ois != null) {
ois.close();
}
if(bais != null) {
bais.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package study.springboot.demo.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import study.springboot.demo.common.CustomException;
import java.util.Set;
/**
* JedisUtil(推荐存Byte数组,存Json字符串效率更慢)
* @author Wang926454
* @date 2018/9/4 15:45
*/
@Component
public class JedisUtil {
/**
* Logger
*/
private static Logger logger = LoggerFactory.getLogger(JedisUtil.class);
/**
* Status-OK
*/
public final static String OK = "OK";
/**
* 静态注入JedisPool连接池
* 本来是正常注入JedisUtil,可以在Controller和Service层使用,但是重写Shiro的CustomCache无法注入JedisUtil
* 现在改为静态注入JedisPool连接池,JedisUtil直接调用静态方法即可
* https://blog.csdn.net/W_Z_W_888/article/details/79979103
*/
private static JedisPool jedisPool;
@Autowired
public void setJedisPool(JedisPool jedisPool) {
JedisUtil.jedisPool = jedisPool;
}
/**
* 获取Jedis实例
* @param
* @return redis.clients.jedis.Jedis
* @author Wang926454
* @date 2018/9/4 15:47
*/
public static synchronized Jedis getJedis() {
try {
if (jedisPool != null) {
Jedis resource = jedisPool.getResource();
return resource;
} else {
return null;
}
} catch (Exception e) {
throw new CustomException("释放Jedis资源异常:" + e.getMessage());
}
}
/**
* 释放Jedis资源
* @param
* @return void
* @author Wang926454
* @date 2018/9/5 9:16
*/
public static void closePool() {
try {
jedisPool.close();
}catch (Exception e){
throw new CustomException("释放Jedis资源异常:" + e.getMessage());
}
}
/**
* 获取redis键值-object
* @param key
* @return java.lang.Object
* @author Wang926454
* @date 2018/9/4 15:47
*/
public static Object getObject(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
byte[] bytes = jedis.get(key.getBytes());
if(StringUtil.isNotNull(bytes)) {
return SerializableUtil.unserializable(bytes);
}
} catch (Exception e) {
throw new CustomException("获取Redis键值getObject方法异常:key=" + key + " cause=" + e.getMessage());
} finally {
if(jedis != null) {
jedis.close();
}
}
return null;
}
/**
* 设置redis键值-object
* @param key
* @param value
* @return java.lang.String
* @author Wang926454
* @date 2018/9/4 15:49
*/
public static String setObject(String key, Object value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.set(key.getBytes(), SerializableUtil.serializable(value));
} catch (Exception e) {
throw new CustomException("设置Redis键值setObject方法异常:key=" + key + " value=" + value + " cause=" + e.getMessage());
} finally {
if(jedis != null) {
jedis.close();
}
}
}
/**
* 设置redis键值-object-expiretime
* @param key
* @param value
* @param expiretime
* @return java.lang.String
* @author Wang926454
* @date 2018/9/4 15:50
*/
public static String setObject(String key, Object value, int expiretime) {
String result = "";
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
result = jedis.set(key.getBytes(), SerializableUtil.serializable(value));
if(OK.equals(result)) {
jedis.expire(key.getBytes(), expiretime);
}
return result;
} catch (Exception e) {
throw new CustomException("设置Redis键值setObject方法异常:key=" + key + " value=" + value + " cause=" + e.getMessage());
} finally {
if(jedis != null) {
jedis.close();
}
}
}
/**
* 获取redis键值-Json
* @param key
* @return java.lang.Object
* @author Wang926454
* @date 2018/9/4 15:47
*/
public static String getJson(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.get(key);
} catch (Exception e) {
throw new CustomException("获取Redis键值getJson方法异常:key=" + key + " cause=" + e.getMessage());
} finally {
if(jedis != null) {
jedis.close();
}
}
}
/**
* 设置redis键值-Json
* @param key
* @param value
* @return java.lang.String
* @author Wang926454
* @date 2018/9/4 15:49
*/
public static String setJson(String key, String value) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.set(key, value);
} catch (Exception e) {
throw new CustomException("设置Redis键值setJson方法异常:key=" + key + " value=" + value + " cause=" + e.getMessage());
} finally {
if(jedis != null) {
jedis.close();
}
}
}
/**
* 设置redis键值-Json-expiretime
* @param key
* @param value
* @param expiretime
* @return java.lang.String
* @author Wang926454
* @date 2018/9/4 15:50
*/
public static String setJson(String key, String value, int expiretime) {
String result = "";
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
result = jedis.set(key, value);
if(OK.equals(result)) {
jedis.expire(key, expiretime);
}
return result;
} catch (Exception e) {
throw new CustomException("设置Redis键值setJson方法异常:key=" + key + " value=" + value + " cause=" + e.getMessage());
} finally {
if(jedis != null) {
jedis.close();
}
}
}
/**
* 删除key
* @param key
* @return java.lang.Long
* @author Wang926454
* @date 2018/9/4 15:50
*/
public static Long delKey(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.del(key.getBytes());
}catch(Exception e) {
throw new CustomException("删除Redis的键delKey方法异常:key=" + key + " cause=" + e.getMessage());
}finally{
if(jedis != null) {
jedis.close();
}
}
}
/**
* key是否存在
* @param key
* @return java.lang.Boolean
* @author Wang926454
* @date 2018/9/4 15:51
*/
public static Boolean exists(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.exists(key.getBytes());
}catch(Exception e) {
throw new CustomException("查询Redis的键是否存在exists方法异常:key=" + key + " cause=" + e.getMessage());
}finally{
if(jedis != null) {
jedis.close();
}
}
}
/**
* 模糊查询获取key集合
* @param key
* @return java.util.Set
* @author Wang926454
* @date 2018/9/6 9:43
*/
public static Set keysS(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.keys(key);
}catch(Exception e) {
throw new CustomException("模糊查询Redis的键集合keysS方法异常:key=" + key + " cause=" + e.getMessage());
}finally{
if(jedis != null) {
jedis.close();
}
}
}
/**
* 模糊查询获取key集合
* @param key
* @return java.util.Set
* @author Wang926454
* @date 2018/9/6 9:43
*/
public static Set keysB(String key) {
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.keys(key.getBytes());
}catch(Exception e) {
throw new CustomException("模糊查询Redis的键集合keysB方法异常:key=" + key + " cause=" + e.getMessage());
}finally{
if(jedis != null) {
jedis.close();
}
}
}
/**
* 获取过期剩余时间
* @param key
* @return java.lang.String
* @author Wang926454
* @date 2018/9/11 16:26
*/
public static Long getExpireTime(String key) {
Long result = -2L;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
result = jedis.ttl(key);
return result;
} catch (Exception e) {
throw new CustomException("获取Redis键过期剩余时间getExpireTime方法异常:key=" + key + " cause=" + e.getMessage());
} finally {
if(jedis != null) {
jedis.close();
}
}
}
}
package study.springboot.demo.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* Jedis配置,项目启动注入JedisPool
* http://www.cnblogs.com/GodHeng/p/9301330.html
* @author Wang926454
* @date 2018/9/5 10:35
*/
@Component
@Configuration
@EnableAutoConfiguration
@PropertySource("classpath:application.properties")
@ConfigurationProperties(prefix = "redis")
public class JedisConfig {
private static Logger logger = LoggerFactory.getLogger(JedisConfig.class);
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.jedis.pool.max-wait}")
private int maxWait;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Bean
public JedisPool redisPoolFactory(){
try {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWait);
jedisPoolConfig.setMaxTotal(maxActive);
jedisPoolConfig.setMinIdle(minIdle);
// JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, null);
logger.info("初始化Redis连接池JedisPool成功!" + " Redis地址: " + host + ":" + port);
return jedisPool;
} catch (Exception e) {
logger.error("初始化Redis连接池JedisPool异常:" + e.getMessage());
}
return null;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
public int getMaxActive() {
return maxActive;
}
public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}
public int getMaxWait() {
return maxWait;
}
public void setMaxWait(int maxWait) {
this.maxWait = maxWait;
}
public int getMaxIdle() {
return maxIdle;
}
public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}
public int getMinIdle() {
return minIdle;
}
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
}
package study.springboot.demo.common;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
/**
* 让错误跳转到同一页面
* 解决spring-boot Whitelabel Error Page
* @Author wangbiao
* @Date 2019-08-15 00:07
* @Decripition TODO
**/
@Controller
class ErrorPageConfig implements ErrorController {
@RequestMapping("/error")
public String handleError(HttpServletRequest request){
//获取statusCode:401,404,500
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
if(statusCode == 401){
return "/401";
}else if(statusCode == 404){
return "/404";
}else if(statusCode == 403){
return "/403";
}else{
return "/500";
}
}
@Override
public String getErrorPath() {
return "/error";
}
}
package study.springboot.demo.common;
/**
* @Author wangbiao
* @Date 2019-08-17 19:34
* @Decripition TODO
**/
public class CustomException extends RuntimeException{
public CustomException(String msg){
super(msg);
}
public CustomException() {
super();
}
}
package study.springboot.demo.controller;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestParam;
import study.springboot.demo.entity.SysUser;
import study.springboot.demo.service.SysUserService;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.List;
/**
*
* 前端控制器
*
*
* @author wangbiao
* @since 2019-07-29
*/
@Controller
@RequestMapping("/sysUser")
public class SysUserController {
@Autowired
private SysUserService sysUserService;
@RequiresPermissions("user:selectSysUser")
@RequestMapping("/selectSysUser")
public String selectSysUser(@RequestParam(value = "username", required = false) String username, Model model){
Wrapper sysUserWrapper = new EntityWrapper<>();
if(StringUtils.isNotBlank(username)){
sysUserWrapper.eq("username",username);
}
List sysUserList = sysUserService.selectList(sysUserWrapper);
model.addAttribute("sysUserList",sysUserList);
return "userList";
}
@RequiresPermissions("user:addSysUser")
@RequestMapping("/toAddSysUser")
public String toAddSysUser(){
return "addUser";
}
@RequiresPermissions("user:addSysUser")
@RequestMapping("/addSysUser")
public String addSysUser(SysUser sysUser, Model model) throws Exception{
sysUser.setCreateTime(new Date());
boolean result = sysUserService.insert(sysUser);
model.addAttribute("result",result);
return "redirect:/sysUser/selectSysUser";
}
@RequiresPermissions("user:updateSysUser")
@RequestMapping("/updateSysUser/{id}")
public String addSysUser(@PathVariable("id") Integer id) throws Exception{
SysUser sysUser = sysUserService.selectById(id);
if(sysUser!=null){
if("1".equals(sysUser.getState())){
sysUser.setState("-1");
}else{
sysUser.setState("1");
}
sysUserService.updateById(sysUser);
}
return "redirect:/sysUser/selectSysUser";
}
}
package study.springboot.demo.service;
import study.springboot.demo.entity.SysUser;
import com.baomidou.mybatisplus.service.IService;
/**
*
* 服务类
*
*
* @author wangbiao
* @since 2019-07-29
*/
public interface SysUserService extends IService {
}
package study.springboot.demo.service.impl;
import study.springboot.demo.entity.SysUser;
import study.springboot.demo.dao.SysUserMapper;
import study.springboot.demo.service.SysUserService;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
*
* 服务实现类
*
*
* @author wangbiao
* @since 2019-07-29
*/
@Service
public class SysUserServiceImpl extends ServiceImpl implements SysUserService {
}
dao和map.xml文件我就不贴出来了
下面是html文件
登录页面 login.html
Title
主页面:index.html
Title
<@shiro.guest>游客显示的信息@shiro.guest>
<@shiro.user>用户已经登录过了@shiro.user>
<@shiro.authenticated>不是记住我登录@shiro.authenticated>
<@shiro.principal>@shiro.principal>
<@shiro.notAuthenticated>已记住用户@shiro.notAuthenticated>
<@shiro.principal type="java.lang.String"/>
<@shiro.principal property="username"/>
<@shiro.hasRole name="admin">这是admin角色@shiro.hasRole>
<@shiro.hasAnyRoles name="admin,vip">用户拥有admin角色 或者 vip角色@shiro.hasAnyRoles>
<@shiro.lacksRole name="admin">如果不是admin角色,显示内容@shiro.lacksRole>
<@shiro.hasPermission name="userInfo:add">用户拥有添加权限@shiro.hasPermission>
<@shiro.lacksPermission name="userInfo:add">如果用户没有添加权限,显示的内容@shiro.lacksPermission>
用户列表页面:userList.html
Title
<@shiro.hasPermission name="user:addSysUser">
新增用户
@shiro.hasPermission>
退出登录
用户列表
用户名 真实姓名 状态 创建时间 操作
<#list sysUserList as user>
${user.username}
${user.name}
<#if user.state=='1' >
禁用
<#else>
开启
#if>
${user.createTime?string('yyyy-MM-dd HH:mm:ss')}
<@shiro.hasPermission name="user:updateSysUser">
<#if user.state=='1' >
开启
<#else>
禁用
#if>
@shiro.hasPermission>
#list>
添加用户页面:addUser.html
新增用户
404错误统一展示页面:404.html
Insert title here
对不起,404了,你迷路了
500错误统一展示页面 500.html
Insert title here
对不起,500了,伍佰来了,唱首挪威森林吧
无权限统一展示页面:
Insert title here
对不起,您没有权限
下面是操作界面:
输入http://localhost:8080/login ,显示:
登录完成后跳转到主页面: 张三的账户点击查看用户列表按钮跳转到下面的页面
shiro其实最直观的表现就是根据账户信息的不同控制着这个账户可以访问那些url,可以看到页面上的那些菜单和按钮。
这个工程参考了网上很多其他人的代码,但大体框架是自己搭建的,中间踩过很多坑,过程不是一帆风顺,但总算完成了基本功能。后面开启springboot的学习。
源代码地址:https://pan.baidu.com/s/1HmVPdGURrzJM64KalN92ug
参考博客:https://blog.csdn.net/qq_34021712/article/details/80294096
https://blog.csdn.net/qq_34021712/article/details/80294417