SpringBoot 整合Shiro实现动态权限加载更新+Session共享+单点登录
作者:sans_
juejin.im/post/5d087d605188256de9779e64
一.说明
shiro是一个安全框架,项目中主要用它做认证,授权,加密,以及用户的会话管理,虽然shiro没有springsecurity功能更丰富,但是它轻量,简单,在项目中通常业务需求shiro也都能胜任.
二.项目环境
-
mybatis-plus版本: 3.1.0
-
springboot版本:2.1.5
-
jdk版本:1.8
-
shiro版本:1.4
-
shiro-redis插件版本:3.1.0
数据表(sql文件在项目中):数据库中测试号的密码进行了加密,密码皆为123456
maven依赖如下:
<dependencies>
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
<dependency>
<groupid>mysql</groupid>
<artifactid>mysql-connector-java</artifactid>
<scope>runtime</scope>
</dependency>
<!-- aop依赖,一定要加,否则权限拦截验证不生效 -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-aop</artifactid>
</dependency>
<!-- lombok插件 -->
<dependency>
<groupid>org.projectlombok</groupid>
<artifactid>lombok</artifactid>
<optional>true</optional>
</dependency>
<!-- redis -->
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-data-redis-reactive</artifactid>
</dependency>
<!-- mybatisplus 核心库 -->
<dependency>
<groupid>com.baomidou</groupid>
<artifactid>mybatis-plus-boot-starter</artifactid>
<version>3.1.0</version>
</dependency>
<!-- 引入阿里数据库连接池 -->
<dependency>
<groupid>com.alibaba</groupid>
<artifactid>druid</artifactid>
<version>1.1.6</version>
</dependency>
<!-- shiro 核心依赖 -->
<dependency>
<groupid>org.apache.shiro</groupid>
<artifactid>shiro-spring</artifactid>
<version>1.4.0</version>
</dependency>
<!-- shiro-redis插件 -->
<dependency>
<groupid>org.crazycake</groupid>
<artifactid>shiro-redis</artifactid>
<version>3.1.0</version>
</dependency>
<!-- stringuitls工具 -->
<dependency>
<groupid>org.apache.commons</groupid>
<artifactid>commons-lang3</artifactid>
<version>3.5</version>
</dependency>
</dependencies>
配置如下:
# 配置端口
server:
port: 8764
spring:
# 配置数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.driver
url: jdbc:mysql://localhost:3306/my_shiro?servertimezone=utc&useunicode=true&characterencoding=utf-8&zerodatetimebehavior=converttonull&usessl=false
username: root
password: root
type: com.alibaba.druid.pool.druiddatasource
# redis数据源
redis:
host: localhost
port: 6379
timeout: 6000
password: 123456
jedis:
pool:
max-active: 1000 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 10 # 连接池中的最大空闲连接
min-idle: 5 # 连接池中的最小空闲连接
# mybatis-plus相关配置
mybatis-plus:
# xml扫描,多个目录用逗号或者分号分隔(告诉 mapper 所对应的 xml 文件位置)
mapper-locations: classpath:mapper/*.xml
# 以下配置均有默认值,可以不设置
global-config:
db-config:
#主键类型 auto:"数据库id自增" input:"用户输入id",id_worker:"全局唯一id (数字类型唯一id)", uuid:"全局唯一id uuid";
id-type: auto
#字段策略 ignored:"忽略判断" not_null:"非 null 判断") not_empty:"非空判断"
field-strategy: not_empty
#数据库类型
db-type: mysql
configuration:
# 是否开启自动驼峰命名规则映射:从数据库列名到java属性驼峰命名的类似映射
map-underscore-to-camel-case: true
# 如果查询结果中包含空值的列,则 mybatis 在映射的时候,不会映射这个字段
call-setters-on-nulls: true
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.stdoutimpl
二.编写项目基础类
用户实体,dao,service等在这里省略,请参考源码
编写exception类来处理shiro权限拦截异常
创建sha256util加密工具
创建spring工具
/**
* @description spring上下文工具类
* @author sans
* @createtime 2019/6/17 13:40
*/
@component
public class springutil implements applicationcontextaware {
private static applicationcontext context;
/**
* spring在bean初始化后会判断是不是applicationcontextaware的子类
* 如果该类是,setapplicationcontext()方法,会将容器中applicationcontext作为参数传入进去
* @author sans
* @createtime 2019/6/17 16:58
*/
@override
public void setapplicationcontext(applicationcontext applicationcontext) throws beansexception {
context = applicationcontext;
}
/**
* 通过name返回指定的bean
* @author sans
* @createtime 2019/6/17 16:03
*/
public static <t> t getbean(class<t> beanclass) {
return context.getbean(beanclass);
}
}
创建shiro工具
/**
* @description shiro工具类
* @author sans
* @createtime 2019/6/15 16:11
*/
public class shiroutils {
/** 私有构造器 **/
private shiroutils(){}
private static redissessiondao redissessiondao = springutil.getbean(redissessiondao.class);
/**
* 获取当前用户session
* @author sans
* @createtime 2019/6/17 17:03
* @return sysuserentity 用户信息
*/
public static session getsession() {
return securityutils.getsubject().getsession();
}
/**
* 用户登出
* @author sans
* @createtime 2019/6/17 17:23
*/
public static void logout() {
securityutils.getsubject().logout();
}
/**
* 获取当前用户信息
* @author sans
* @createtime 2019/6/17 17:03
* @return sysuserentity 用户信息
*/
public static sysuserentity getuserinfo() {
return (sysuserentity) securityutils.getsubject().getprincipal();
}
/**
* 删除用户缓存信息
* @author sans
* @createtime 2019/6/17 13:57
* @param username 用户名称
* @param isremovesession 是否删除session
* @return void
*/
public static void deletecache(string username, boolean isremovesession){
//从缓存中获取session
session session = null;
collection<session> sessions = redissessiondao.getactivesessions();
sysuserentity sysuserentity;
object attribute = null;
for(session sessioninfo : sessions){
//遍历session,找到该用户名称对应的session
attribute = sessioninfo.getattribute(defaultsubjectcontext.principals_session_key);
if (attribute == null) {
continue;
}
sysuserentity = (sysuserentity) ((simpleprincipalcollection) attribute).getprimaryprincipal();
if (sysuserentity == null) {
continue;
}
if (objects.equals(sysuserentity.getusername(), username)) {
session=sessioninfo;
}
}
if (session == null||attribute == null) {
return;
}
//删除session
if (isremovesession) {
redissessiondao.delete(session);
}
//删除cache,在访问受限接口时会重新授权
defaultwebsecuritymanager securitymanager = (defaultwebsecuritymanager) securityutils.getsecuritymanager();
authenticator authc = securitymanager.getauthenticator();
((logoutaware) authc).onlogout((simpleprincipalcollection) attribute);
}
}
创建shiro的sessionid生成器
三.编写shiro核心类
创建realm用于授权和认证
/**
* @description shiro权限匹配和账号密码匹配
* @author sans
* @createtime 2019/6/15 11:27
*/
public class shirorealm extends authorizingrealm {
@autowired
private sysuserservice sysuserservice;
@autowired
private sysroleservice sysroleservice;
@autowired
private sysmenuservice sysmenuservice;
/**
* 授权权限
* 用户进行权限验证时候shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
* @author sans
* @createtime 2019/6/12 11:44
*/
@override
protected authorizationinfo dogetauthorizationinfo(principalcollection principalcollection) {
simpleauthorizationinfo authorizationinfo = new simpleauthorizationinfo();
sysuserentity sysuserentity = (sysuserentity) principalcollection.getprimaryprincipal();
//获取用户id
long userid =sysuserentity.getuserid();
//这里可以进行授权和处理
set<string> rolesset = new hashset<>();
set<string> permsset = new hashset<>();
//查询角色和权限(这里根据业务自行查询)
list<sysroleentity> sysroleentitylist = sysroleservice.selectsysrolebyuserid(userid);
for (sysroleentity sysroleentity:sysroleentitylist) {
rolesset.add(sysroleentity.getrolename());
list<sysmenuentity> sysmenuentitylist = sysmenuservice.selectsysmenubyroleid(sysroleentity.getroleid());
for (sysmenuentity sysmenuentity :sysmenuentitylist) {
permsset.add(sysmenuentity.getperms());
}
}
//将查到的权限和角色分别传入authorizationinfo中
authorizationinfo.setstringpermissions(permsset);
authorizationinfo.setroles(rolesset);
return authorizationinfo;
}
/**
* 身份认证
* @author sans
* @createtime 2019/6/12 12:36
*/
@override
protected authenticationinfo dogetauthenticationinfo(authenticationtoken authenticationtoken) throws authenticationexception {
//获取用户的输入的账号.
string username = (string) authenticationtoken.getprincipal();
//通过username从数据库中查找 user对象,如果找到进行验证
//实际项目中,这里可以根据实际情况做缓存,如果不做,shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
sysuserentity user = sysuserservice.selectuserbyname(username);
//判断账号是否存在
if (user == null) {
throw new authenticationexception();
}
//判断账号是否被冻结
if (user.getstate()==null||user.getstate().equals("prohibit")){
throw new lockedaccountexception();
}
//进行验证
simpleauthenticationinfo authenticationinfo = new simpleauthenticationinfo(
user, //用户名
user.getpassword(), //密码
bytesource.util.bytes(user.getsalt()), //设置盐值
getname()
);
//验证成功开始踢人(清除缓存和session)
shiroutils.deletecache(username,true);
return authenticationinfo;
}
}
创建sessionmanager类
创建shiroconfig配置类
/**
* @description shiro配置类
* @author sans
* @createtime 2019/6/10 17:42
*/
@configuration
public class shiroconfig {
private final string cache_key = "shiro:cache:";
private final string session_key = "shiro:session:";
private final int expire = 1800;
//redis配置
@value("${spring.redis.host}")
private string host;
@value("${spring.redis.port}")
private int port;
@value("${spring.redis.timeout}")
private int timeout;
@value("${spring.redis.password}")
private string password;
/**
* 开启shiro-aop注解支持
* @attention 使用代理方式所以需要开启代码支持
* @author sans
* @createtime 2019/6/12 8:38
*/
@bean
public authorizationattributesourceadvisor authorizationattributesourceadvisor(securitymanager securitymanager) {
authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor();
authorizationattributesourceadvisor.setsecuritymanager(securitymanager);
return authorizationattributesourceadvisor;
}
/**
* shiro基础配置
* @author sans
* @createtime 2019/6/12 8:42
*/
@bean
public shirofilterfactorybean shirofilterfactory(securitymanager securitymanager){
shirofilterfactorybean shirofilterfactorybean = new shirofilterfactorybean();
shirofilterfactorybean.setsecuritymanager(securitymanager);
map<string, string> filterchaindefinitionmap = new linkedhashmap<>();
// 注意过滤器配置顺序不能颠倒
// 配置过滤:不会被拦截的链接
filterchaindefinitionmap.put("/static/**", "anon");
filterchaindefinitionmap.put("/userlogin/**", "anon");
filterchaindefinitionmap.put("/**", "authc");
// 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
shirofilterfactorybean.setloginurl("/userlogin/unauth");
shirofilterfactorybean.setfilterchaindefinitionmap(filterchaindefinitionmap);
return shirofilterfactorybean;
}
/**
* 安全管理器
* @author sans
* @createtime 2019/6/12 10:34
*/
@bean
public securitymanager securitymanager() {
defaultwebsecuritymanager securitymanager = new defaultwebsecuritymanager();
// 自定义ssession管理
securitymanager.setsessionmanager(sessionmanager());
// 自定义cache实现
securitymanager.setcachemanager(cachemanager());
// 自定义realm验证
securitymanager.setrealm(shirorealm());
return securitymanager;
}
/**
* 身份验证器
* @author sans
* @createtime 2019/6/12 10:37
*/
@bean
public shirorealm shirorealm() {
shirorealm shirorealm = new shirorealm();
shirorealm.setcredentialsmatcher(hashedcredentialsmatcher());
return shirorealm;
}
/**
* 凭证匹配器
* 将密码校验交给shiro的simpleauthenticationinfo进行处理,在这里做匹配配置
* @author sans
* @createtime 2019/6/12 10:48
*/
@bean
public hashedcredentialsmatcher hashedcredentialsmatcher() {
hashedcredentialsmatcher shacredentialsmatcher = new hashedcredentialsmatcher();
// 散列算法:这里使用sha256算法;
shacredentialsmatcher.sethashalgorithmname(sha256util.hash_algorithm_name);
// 散列的次数,比如散列两次,相当于 md5(md5(""));
shacredentialsmatcher.sethashiterations(sha256util.hash_iterations);
return shacredentialsmatcher;
}
/**
* 配置redis管理器
* @attention 使用的是shiro-redis开源插件
* @author sans
* @createtime 2019/6/12 11:06
*/
@bean
public redismanager redismanager() {
redismanager redismanager = new redismanager();
redismanager.sethost(host);
redismanager.setport(port);
redismanager.settimeout(timeout);
redismanager.setpassword(password);
return redismanager;
}
/**
* 配置cache管理器
* 用于往redis存储权限和角色标识
* @attention 使用的是shiro-redis开源插件
* @author sans
* @createtime 2019/6/12 12:37
*/
@bean
public rediscachemanager cachemanager() {
rediscachemanager rediscachemanager = new rediscachemanager();
rediscachemanager.setredismanager(redismanager());
rediscachemanager.setkeyprefix(cache_key);
// 配置缓存的话要求放在session里面的实体类必须有个id标识
rediscachemanager.setprincipalidfieldname("userid");
return rediscachemanager;
}
/**
* sessionid生成器
* @author sans
* @createtime 2019/6/12 13:12
*/
@bean
public shirosessionidgenerator sessionidgenerator(){
return new shirosessionidgenerator();
}
/**
* 配置redissessiondao
* @attention 使用的是shiro-redis开源插件
* @author sans
* @createtime 2019/6/12 13:44
*/
@bean
public redissessiondao redissessiondao() {
redissessiondao redissessiondao = new redissessiondao();
redissessiondao.setredismanager(redismanager());
redissessiondao.setsessionidgenerator(sessionidgenerator());
redissessiondao.setkeyprefix(session_key);
redissessiondao.setexpire(expire);
return redissessiondao;
}
/**
* 配置session管理器
* @author sans
* @createtime 2019/6/12 14:25
*/
@bean
public sessionmanager sessionmanager() {
shirosessionmanager shirosessionmanager = new shirosessionmanager();
shirosessionmanager.setsessiondao(redissessiondao());
return shirosessionmanager;
}
}
四.实现权限控制
shiro可以用代码或者注解来控制权限,通常我们使用注解控制,不仅简单方便,而且更加灵活.shiro注解一共有五个:
一般情况下我们在项目中做权限控制,使用最多的是requirespermissions和requiresroles,允许存在多个角色和权限,默认逻辑是and,也就是同时拥有这些才可以访问方法,可以在注解中以参数的形式设置成or
示例
使用顺序:shiro注解是存在顺序的,当多个注解在一个方法上的时候,会逐个检查,知道全部通过为止,默认拦截顺序是:requiresroles->requirespermissions->requiresauthentication->
requiresuser->requiresguest
示例
创建userrolecontroller角色拦截测试类
/**
* @description 角色测试
* @author sans
* @createtime 2019/6/19 11:38
*/
@restcontroller
@requestmapping("/role")
public class userrolecontroller {
@autowired
private sysuserservice sysuserservice;
@autowired
private sysroleservice sysroleservice;
@autowired
private sysmenuservice sysmenuservice;
@autowired
private sysrolemenuservice sysrolemenuservice;
/**
* 管理员角色测试接口
* @author sans
* @createtime 2019/6/19 10:38
* @return map<string,object> 返回结果
*/
@requestmapping("/getadmininfo")
@requiresroles("admin")
public map<string,object> getadmininfo(){
map<string,object> map = new hashmap<>();
map.put("code",200);
map.put("msg","这里是只有管理员角色能访问的接口");
return map;
}
/**
* 用户角色测试接口
* @author sans
* @createtime 2019/6/19 10:38
* @return map<string,object> 返回结果
*/
@requestmapping("/getuserinfo")
@requiresroles("user")
public map<string,object> getuserinfo(){
map<string,object> map = new hashmap<>();
map.put("code",200);
map.put("msg","这里是只有用户角色能访问的接口");
return map;
}
/**
* 角色测试接口
* @author sans
* @createtime 2019/6/19 10:38
* @return map<string,object> 返回结果
*/
@requestmapping("/getroleinfo")
@requiresroles(value={"admin","user"},logical = logical.or)
@requiresuser
public map<string,object> getroleinfo(){
map<string,object> map = new hashmap<>();
map.put("code",200);
map.put("msg","这里是只要有admin或者user角色能访问的接口");
return map;
}
/**
* 登出(测试登出)
* @author sans
* @createtime 2019/6/19 10:38
* @return map<string,object> 返回结果
*/
@requestmapping("/getlogout")
@requiresuser
public map<string,object> getlogout(){
shiroutils.logout();
map<string,object> map = new hashmap<>();
map.put("code",200);
map.put("msg","登出");
return map;
}
}
创建usermenucontroller权限拦截测试类
/**
* @description 权限测试
* @author sans
* @createtime 2019/6/19 11:38
*/
@restcontroller
@requestmapping("/menu")
public class usermenucontroller {
@autowired
private sysuserservice sysuserservice;
@autowired
private sysroleservice sysroleservice;
@autowired
private sysmenuservice sysmenuservice;
@autowired
private sysrolemenuservice sysrolemenuservice;
/**
* 获取用户信息集合
* @author sans
* @createtime 2019/6/19 10:36
* @return map<string,object> 返回结果
*/
@requestmapping("/getuserinfolist")
@requirespermissions("sys:user:info")
public map<string,object> getuserinfolist(){
map<string,object> map = new hashmap<>();
list<sysuserentity> sysuserentitylist = sysuserservice.list();
map.put("sysuserentitylist",sysuserentitylist);
return map;
}
/**
* 获取角色信息集合
* @author sans
* @createtime 2019/6/19 10:37
* @return map<string,object> 返回结果
*/
@requestmapping("/getroleinfolist")
@requirespermissions("sys:role:info")
public map<string,object> getroleinfolist(){
map<string,object> map = new hashmap<>();
list<sysroleentity> sysroleentitylist = sysroleservice.list();
map.put("sysroleentitylist",sysroleentitylist);
return map;
}
/**
* 获取权限信息集合
* @author sans
* @createtime 2019/6/19 10:38
* @return map<string,object> 返回结果
*/
@requestmapping("/getmenuinfolist")
@requirespermissions("sys:menu:info")
public map<string,object> getmenuinfolist(){
map<string,object> map = new hashmap<>();
list<sysmenuentity> sysmenuentitylist = sysmenuservice.list();
map.put("sysmenuentitylist",sysmenuentitylist);
return map;
}
/**
* 获取所有数据
* @author sans
* @createtime 2019/6/19 10:38
* @return map<string,object> 返回结果
*/
@requestmapping("/getinfoall")
@requirespermissions("sys:info:all")
public map<string,object> getinfoall(){
map<string,object> map = new hashmap<>();
list<sysuserentity> sysuserentitylist = sysuserservice.list();
map.put("sysuserentitylist",sysuserentitylist);
list<sysroleentity> sysroleentitylist = sysroleservice.list();
map.put("sysroleentitylist",sysroleentitylist);
list<sysmenuentity> sysmenuentitylist = sysmenuservice.list();
map.put("sysmenuentitylist",sysmenuentitylist);
return map;
}
/**
* 添加管理员角色权限(测试动态权限更新)
* @author sans
* @createtime 2019/6/19 10:39
* @param username 用户id
* @return map<string,object> 返回结果
*/
@requestmapping("/addmenu")
public map<string,object> addmenu(){
//添加管理员角色权限
sysrolemenuentity sysrolemenuentity = new sysrolemenuentity();
sysrolemenuentity.setmenuid(4l);
sysrolemenuentity.setroleid(1l);
sysrolemenuservice.save(sysrolemenuentity);
//清除缓存
string username = "admin";
shiroutils.deletecache(username,false);
map<string,object> map = new hashmap<>();
map.put("code",200);
map.put("msg","权限添加成功");
return map;
}
}
创建userlogincontroller登录类
/**
* @description 用户登录
* @author sans
* @createtime 2019/6/17 15:21
*/
@restcontroller
@requestmapping("/userlogin")
public class userlogincontroller {
@autowired
private sysuserservice sysuserservice;
/**
* 登录
* @author sans
* @createtime 2019/6/20 9:21
*/
@requestmapping("/login")
public map<string,object> login(@requestbody sysuserentity sysuserentity){
map<string,object> map = new hashmap<>();
//进行身份验证
try{
//验证身份和登陆
subject subject = securityutils.getsubject();
usernamepasswordtoken token = new usernamepasswordtoken(sysuserentity.getusername(), sysuserentity.getpassword());
//验证成功进行登录操作
subject.login(token);
}catch (incorrectcredentialsexception e) {
map.put("code",500);
map.put("msg","用户不存在或者密码错误");
return map;
} catch (lockedaccountexception e) {
map.put("code",500);
map.put("msg","登录失败,该用户已被冻结");
return map;
} catch (authenticationexception e) {
map.put("code",500);
map.put("msg","该用户不存在");
return map;
} catch (exception e) {
map.put("code",500);
map.put("msg","未知异常");
return map;
}
map.put("code",0);
map.put("msg","登录成功");
map.put("token",shiroutils.getsession().getid().tostring());
return map;
}
/**
* 未登录
* @author sans
* @createtime 2019/6/20 9:22
*/
@requestmapping("/unauth")
public map<string,object> unauth(){
map<string,object> map = new hashmap<>();
map.put("code",500);
map.put("msg","未登录");
return map;
}
}
五.postman测试
登录成功后会返回token,因为是单点登录,再次登陆的话会返回新的token,之前redis的token就会失效了
当第一次访问接口后我们可以看到缓存中已经有权限数据了,在次访问接口的时候,shiro会直接去缓存中拿取权限,注意访问接口时候要设置请求头.
admin这个号现在没有sys:info:all这个权限的,所以无法访问getinfoall接口,我们要动态分配权限后,要清掉缓存,在访问接口时候,shiro会去重新执行授权方法,之后再次把权限和角色数据放入缓存中
访问添加权限测试接口,因为是测试,我把增加权限的用户admin写死在里面了,权限添加后,调用工具类清掉缓存,我们可以发现,redis中已经没有缓存了
再次访问getinfoall接口,因为缓存中没有数据,shiro会重新授权查询权限,拦截通过
六.项目源码
https://gitee.com/liselotte/spring-boot-shiro-demo
https://github.com/xuyulong2017/my-java-demo
推荐阅读(点击即可跳转阅读)
2. 面试题内容聚合
3. 设计模式内容聚合
4. mybatis内容聚合
5. 多线程内容聚合
上一篇: Java Servlet的配置
下一篇: 记录微信jsapi公众号支付全程