java|springboot+redis+shiro实现前后端分离的权限管理(自定义session管理和redis缓存)
之前介绍了springboot+shrio的入门教程,项目结构比较简单,最近想自己做一个前后端分离项目,权限框架依然想用流行的shiro框架,参照网上的众多资料,踩了不少坑之后,终于是实现了后端的相关配置和操作,话不多说,马上进入正题。
温馨提示 本篇博文的代码并不是项目所用到的技术的全部代码,所以万万不可上来就只看博客就开始敲代码,一定要先下载下来我的项目,配合项目进行学习,博客里没有的类或接口在项目里都可以找到。
项目连接:https://gitee.com/qizhongxiao/candy-demo.git
SVN:svn://gitee.com/qizhongxiao/candy-demo
前期准备 项目使用的是springboot2.x+mysql5+redis+mybatis-plus+shiro
在开始我们的项目之前,我们需要先搭建一套springboot2.x的项目,所以你需要对springboot项目有一定的了解,同时我们需要在本地搭建redis服务器,没有搭建的话快去搭建吧,先来看看我们的项目结构:
文章图片
然后是我们的数据库表结构:
/*
Navicat MySQL Data TransferSource Server: 本地mysql5版本
Source Server Version : 50727
Source Host: localhost:3307
Source Database: activitiTarget Server Type: MYSQL
Target Server Version : 50727
File Encoding: 65001Date: 2019-09-18 17:54:58
*/SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`menu_name` varchar(45) NOT NULL,
`menu_path` varchar(45) NOT NULL,
`permission_code` varchar(45) DEFAULT NULL COMMENT '权限名',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES ('1', '例-table', '/example/table', 'sys:user:view', '2018-10-26 19:38:30', '2018-10-30 11:42:15');
INSERT INTO `sys_menu` VALUES ('2', '表单', '/form', 'sys:user:view', '2018-10-30 14:43:19', '2018-10-30 15:35:35');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`rolename` varchar(45) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'admin', '2018-10-25 16:33:34');
INSERT INTO `sys_role` VALUES ('2', '总经理', '2019-09-12 15:24:29');
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_id` int(11) NOT NULL,
`menu_id` int(11) NOT NULL,
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES ('1', '1', '1', '2018-10-27 20:16:50');
INSERT INTO `sys_role_menu` VALUES ('2', '1', '2', '2018-10-30 14:44:48');
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(45) NOT NULL,
`password` varchar(45) NOT NULL,
`salt` varchar(10) DEFAULT NULL,
`nickname` varchar(45) NOT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`state` int(2) NOT NULL DEFAULT '1' COMMENT '1:有效\n2:冻结\n',
`avatar` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', 'c34af346c89b8b03438e27a32863c9b5', 'admin', '王大锤', '2019-09-18 10:35:00', '2019-09-18 10:35:00', '1', 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif');
INSERT INTO `sys_user` VALUES ('2', 'wuyanzu', '6bb50ce0c9e42923e443af29a33b8fb8', 'wuyanzu', '吴彦祖', '2019-09-12 17:58:58', '2019-09-12 17:58:58', '1', 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif');
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('1', '1', '1', '2018-10-25 16:33:47');
INSERT INTO `sys_user_role` VALUES ('2', '7', '1', '2019-09-16 15:25:10');
代码奉上,直接运行就好
然后是我们的pom文件了,为了方便我这里也直接贴出来,因为我之后要集成其它技术,所以并不是所有的maven依赖我们都需要,我比懒,这里就直接全粘贴出来了(我只粘贴了properties和dependencies,注意哦)
1.8
5.1.47
2.1.7.RELEASE
3.1.0
org.springframework.boot
spring-boot-starter-activemq
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
io.lettuce
lettuce-core
redis.clients
jedis
org.apache.commons
commons-pool2
2.5.0
com.alibaba
fastjson
1.2.47
org.springframework.boot
spring-boot-configuration-processor
true
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-websocket
org.springframework.session
spring-session-data-redis
org.springframework.session
spring-session-jdbc
org.springframework.boot
spring-boot-devtools
runtime
true
com.github.pagehelper
pagehelper
5.1.8
org.springframework.boot
spring-boot-starter-log4j2
mysql
mysql-connector-java
${mysql.version}
com.fasterxml.jackson.core
jackson-databind
com.baomidou
mybatis-plus-boot-starter
3.1.0
com.baomidou
mybatis-plus-generator
3.1.0
org.freemarker
freemarker
org.projectlombok
lombok
true
io.springfox
springfox-swagger2
2.9.2
io.springfox
springfox-swagger-ui
2.9.2
org.springframework.boot
spring-boot-starter-thymeleaf
org.apache.shiro
shiro-spring
1.4.0
org.springframework.boot
spring-boot-starter-aop
com.alibaba
druid-spring-boot-starter
1.1.10
org.apache.commons
commons-lang3
org.springframework.boot
spring-boot-configuration-processor
true
org.apache.shiro
shiro-spring
1.4.0
org.activiti
activiti-spring-boot-starter-basic
6.0.0
org.apache.xmlgraphics
batik-transcoder
1.7
org.apache.xmlgraphics
batik-codec
1.7
org.activiti
activiti-json-converter
6.0.0
org.apache.shiro
shiro-cache
1.4.0
org.springframework.boot
spring-boot-starter-cache
net.sf.ehcache
ehcache
org.apache.shiro
shiro-ehcache
1.4.0
org.crazycake
shiro-redis
${shiro-redis.version}
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-test
test
添加完依赖后,我们需要编写配置文件application.yml,注意改jdbc的url哦
server:
port: 8080spring:
datasource:
url: jdbc:mysql://127.0.0.1:3307/activiti?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
druid:
#配置监控统计拦截的filters,去掉后监控界面SQL无法进行统计,'wall'用于防火墙
filters: stat,wall,log4j
#初始化大小
initial-size: 10
#最小连接数
min-idle: 10
#最大连接数
max-active: 20
#获取连接等待超时时间
max-wait: 12000
#间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
time-between-eviction-runs-millis: 60000
#一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 30000
#测试语句是否执行正确
validation-query: SELECT 1
validation-query-timeout: 2000
#指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除.
test-while-idle: true
#借出连接时不要测试,否则很影响性能
test-on-borrow: false
test-on-return: false
#打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false
pool-prepared-statements: false
#与Oracle数据库PSCache有关,再druid下可以设置的比较高
max-pool-prepared-statement-per-connection-size: 20
redis:
host: 127.0.0.1
port: 6379
password : root
timeout: 10000
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
session:
store-type: nonemybatis-plus:
global-config:
db-config:
id-type: auto
field-strategy: not_empty
table-underline: true
db-type: mysql
logic-delete-value: 1
logic-not-delete-value: 0
mapper-locations: classpath:/mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
至此前期的准备工作基本上就完成了,下面开始进入正题。
项目搭建 对于项目结构我这里是用mybatis-plus代码生成器直接生成的,下面贴出来
package com.candy.candydemo.util;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableFill;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import java.util.ArrayList;
import java.util.List;
public class MysqlGenerator {/**
* RUN THIS
*/
public static void main(String[] args) {
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
// TODO 设置用户名
gc.setAuthor("yuan");
gc.setOpen(true);
// service 命名方式
gc.setServiceName("%sService");
// service impl 命名方式
gc.setServiceImplName("%sServiceImpl");
// 自定义文件命名,注意 %s 会自动填充表实体属性!
gc.setMapperName("%sMapper");
gc.setXmlName("%sMapper");
gc.setFileOverride(true);
gc.setActiveRecord(true);
// XML 二级缓存
gc.setEnableCache(false);
// XML ResultMap
gc.setBaseResultMap(true);
// XML columList
gc.setBaseColumnList(false);
mpg.setGlobalConfig(gc);
// TODO 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://127.0.0.1:3307/activiti?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false");
dsc.setDriverName("com.mysql.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
mpg.setDataSource(dsc);
// TODO 包配置
PackageConfig pc = new PackageConfig();
//pc.setModuleName(scanner("模块名"));
pc.setParent("com.candy.candydemo");
pc.setEntity("entity");
pc.setService("service");
pc.setServiceImpl("service.impl");
mpg.setPackageInfo(pc);
// 自定义需要填充的字段
List tableFillList = new ArrayList<>();
//如 每张表都有一个创建时间、修改时间
//而且这基本上就是通用的了,新增时,创建时间和修改时间同时修改
//修改时,修改时间会修改,
//虽然像Mysql数据库有自动更新几只,但像ORACLE的数据库就没有了,
//使用公共字段填充功能,就可以实现,自动按场景更新了。
//如下是配置
//TableFill createField = new TableFill("gmt_create", FieldFill.INSERT);
//TableFill modifiedField = new TableFill("gmt_modified", FieldFill.INSERT_UPDATE);
//tableFillList.add(createField);
//tableFillList.add(modifiedField);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
List focList = new ArrayList<>();
focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输入文件名称
return projectPath + "/src/main/resources/mapper/"
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
mpg.setTemplate(new TemplateConfig().setXml(null));
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
// 设置逻辑删除键
strategy.setLogicDeleteFieldName("deleted");
// TODO 指定生成的bean的数据库表名
strategy.setInclude("sys_user","sys_user_role","sys_role","sys_role_menu","sys_role_permission","sys_menu","sys_permission");
//strategy.setSuperEntityColumns("id");
// 驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
mpg.setStrategy(strategy);
// 选择 freemarker 引擎需要指定如下加,注意 pom 依赖必须有!
mpg.setTemplateEngine(new FreemarkerTemplateEngine());
mpg.execute();
}}
注意里面需要改动的地方,数据源以及模块名等,这个很简单,我就不详细赘述了。
然后是我们的redis配置文件:
package com.candy.candydemo.conf.redis;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
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.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author :qzx
* @ClassName :RedisConfig
* @date : 2019/9/6 10:04
* @description : TODO redis配置类
*/
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate
如果你进行到这里了,说明你的项目结构已经搭建完成了,接下来就是最最重要的shiro配置环节了
shrio配置类的编写 从这里开始就非常重要了,稍不留神就会踩坑(我也是踩了很多坑才走到这一步的?)
ShiroRealm配置类的编写 这个配置类,主要是处理用户登录的操作和获取用户权限的操作,先贴上代码
package com.candy.candydemo.conf.shiro;
import com.candy.candydemo.entity.SysMenu;
import com.candy.candydemo.entity.SysRole;
import com.candy.candydemo.entity.SysUser;
import com.candy.candydemo.entity.vo.Result;
import com.candy.candydemo.service.SysUserService;
import com.candy.candydemo.util.StringUtil;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import java.util.List;
public class ShiroRealm extends AuthorizingRealm {
private static final Logger LOGGER = LoggerFactory.getLogger(ShiroRealm.class);
@Autowired//自己定义的接口,用于查询用户信息(用户名,角色,权限等)
private SysUserService sysUserService;
/**
* @return : org.apache.shiro.authz.AuthorizationInfo
* @Author : qzx
* @Description : //TODO 授权
* @Date : 10:34 2019/9/16
* @Param : [principals]
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
LOGGER.info("---------------------执行shiro权限获取开始----------------------");
Object principal = principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (principal instanceof SysUser) {
SysUser sysUser = (SysUser) principal;
if (sysUser != null) {
List roles = sysUser.getRoles();
if (!CollectionUtils.isEmpty(roles)) {
for (SysRole sysRole : roles) {
info.addRole(sysRole.getRolename());
List permissions = sysRole.getMenus();
if (!CollectionUtils.isEmpty(permissions)) {
for (SysMenu sysPermission : permissions) {
if (StringUtil.isNotEmpty(sysPermission.getPermissionCode())) {
info.addStringPermission(sysPermission.getPermissionCode());
}
}
}
}
}
}
LOGGER.info("---------------------执行shiro权限获取成功----------------------");
return info;
}
return null;
}
/**
* @return : org.apache.shiro.authc.AuthenticationInfo
* @Author : qzx
* @Description : //TODO 登录认证
* @Date : 10:34 2019/9/16
* @Param : [authcToken]
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
LOGGER.info("--------------------执行shiro凭证认证开始----------------------");
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
String name = token.getUsername();
SysUser sysuser = new SysUser();
sysuser.setUsername(name);
Result result = sysUserService.getUserByName(sysuser);
SysUser user = (SysUser) result.getData();
if (user != null) {
if (!(user.getState() == 1)) {
LOGGER.info("---------------------用户已被冻结----------------------");
throw new DisabledAccountException();
}
LOGGER.info("---------------------执行shiro凭证认证成功----------------------");
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
return authenticationInfo;
}
throw new UnknownAccountException();
}
}
这里主要是重写了AuthorizingRealm类的两个方法,doGetAuthenticationInfo方法主要是处理用户登录的相关操作,将用户登录信息存放到SimpleAuthenticationInfo中以供之后shiro调用,doGetAuthorizationInfo方法主要是获取用户信息,包括用户角色和权限。这里需要提一下,doGetAuthenticationInfo是在用户执行登录操作时就会调用,而doGetAuthorizationInfo是在某接口需要进行权限验证的时候才会调用。
自定义session管理类 shiro本身获取sessionid的方法是从cookie中获取的,如果cookie中没有就从url或参数中获取。在前后端分离中,我们推荐将sessionid放在请求头中,每次都从请求头中获取用户的sessionid。所以我们要重写shiro获取sessionid的放啊,建立SessionManager类,继成shiro的DefaultWebSessionManager,重写getSessionId方法,使我们从每次请求的请求头获取sessionId,代码如下
package com.candy.candydemo.conf.shiro;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
public class SessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public SessionManager() {
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//获取请求头,或者请求参数中的Token
String id = StringUtils.isEmpty(WebUtils.toHttp(request).getHeader(AUTHORIZATION))
? request.getParameter(AUTHORIZATION) : WebUtils.toHttp(request).getHeader(AUTHORIZATION);
// 如果请求头中有 Token 则其值为sessionId
if (StringUtils.isNotEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
// 否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
/**
* 获取session 优化单次请求需要多次访问redis的问题
*
* @param sessionKey
* @return
* @throws UnknownSessionException
*/
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
if (request != null && null != sessionId) {
Object sessionObj = request.getAttribute(sessionId.toString());
if (sessionObj != null) {
return (Session) sessionObj;
}
}
Session session = super.retrieveSession(sessionKey);
if (request != null && null != sessionId) {
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}
这里的作用主要是在每次请求的时候,获取请求头中的sessionId,用来验证当前用户是否登录,如果当前的redis中存放有sessionid,那么就获取该用户信息。这个配置类先放在这里,等会我们再来看。
用户密码校验类(MD5加盐加密处理) 由于我们的密码进行了加盐加密处理,而shiro在获取用户信息后会进行密码比对,doCredentialsMatch就是其进行密码校对的方法,若想让其与我们的加密算法进行想匹配的话,我们不如重写这个方法,加入我们自己的逻辑。
package com.candy.candydemo.conf.shiro;
import com.candy.candydemo.util.Md5Util;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
public class CredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken utoken = (UsernamePasswordToken) token;
// 获得用户输入的密码:(可以采用加盐(salt)的方式去检验)
String inPassword = new String(utoken.getPassword());
String inUsername = new String (utoken.getUsername());
String password = Md5Util.md5(inPassword, inUsername);
// 获得数据库中的密码
String dbPassword = (String) info.getCredentials();
// 进行密码的比对
boolean flag = password.equals(dbPassword)? true:false;
return flag;
}
}
Md5Util的代码如下:
package com.candy.candydemo.util;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;
/** 用于生成MD5 密码的工具类
* */
public class Md5Util {public static final String md5(String password, String salt){
//加密方式
String hashAlgorithmName = "MD5";
//盐:为了即使相同的密码不同的盐加密后的结果也不同
ByteSource byteSalt = ByteSource.Util.bytes(salt);
//密码
Object source = password;
//加密次数
int hashIterations = 1024;
SimpleHash result = new SimpleHash(hashAlgorithmName, source, byteSalt, hashIterations);
return result.toString();
}
}
我这里为了演示方便只做了简单的加密处理,slat默认获取的是用户的用户名。
编写shiroConfig配置类 shiro里的核心配置类就是这个ShiroConfig配置类了,这个配置类主要是规定我们shiro需要拦截的接口,以及我们自定义session,redis相关配置注入等操作。下面我贴下完整的代码。
package com.candy.candydemo.conf.shiro;
import com.candy.candydemo.conf.filter.ShiroFormAuthenticationFilter;
import org.apache.shiro.mgt.SecurityManager;
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.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 没有登陆的用户只能访问登陆页面,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
shiroFilterFactoryBean.setLoginUrl("/common/unauth");
// 登录成功后要跳转的链接
//shiroFilterFactoryBean.setSuccessUrl("/auth/index");
// 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("common/unauth");
//自定义拦截器
Map filtersMap = new LinkedHashMap();
//限制同一帐号同时在线的个数。
filtersMap.put("kickout", kickoutSessionControlFilter());
filtersMap.put("authc", new ShiroFormAuthenticationFilter());
//将自定义 的FormAuthenticationFilter注入shiroFilter中
shiroFilterFactoryBean.setFilters(filtersMap);
// 权限控制map.
Map filterChainDefinitionMap = new LinkedHashMap();
// 公共请求
filterChainDefinitionMap.put("/common/**", "anon");
// 静态资源
filterChainDefinitionMap.put("/static/**", "anon");
// 登录方法
filterChainDefinitionMap.put("/admin/login*", "anon");
// 表示可以匿名访问
//此处需要添加一个kickout,上面添加的自定义拦截器才能生效
filterChainDefinitionMap.put("/admin/**", "authc,kickout");
// 表示需要认证才可以访问
//filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 设置realm.
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* 身份认证realm
*
* @return
*/
@Bean
public ShiroRealm myShiroRealm() {
ShiroRealm myShiroRealm = new ShiroRealm();
myShiroRealm.setCredentialsMatcher(credentialsMatcher());
return myShiroRealm;
}
@Bean
public CredentialsMatcher credentialsMatcher() {
return new CredentialsMatcher();
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
redisCacheManager.setKeyPrefix("SPRINGBOOT_CACHE:");
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
redisSessionDAO.setKeyPrefix("SPRINGBOOT_SESSION:");
return redisSessionDAO;
}
/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public SessionManager sessionManager() {
SimpleCookie simpleCookie = new SimpleCookie("Token");
simpleCookie.setPath("/");
simpleCookie.setHttpOnly(false);
SessionManager sessionManager = new SessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
sessionManager.setSessionIdCookieEnabled(false);
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionIdCookie(simpleCookie);
return sessionManager;
}/**AuthorizationAttributeSourceAdvisor
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(redisHost);
redisManager.setPort(redisPort);
redisManager.setTimeout(1800);
//设置过期时间
redisManager.setPassword(redisPassword);
return redisManager;
}
/**
* 限制同一账号登录同时登录人数控制
*这个方法不需要的话可以注释掉。注意注释掉这里需要同时注释掉shiroFilter中关于此方法的拦截器
* @return
*/
@Bean
public SessionControlFilter kickoutSessionControlFilter() {
SessionControlFilter kickoutSessionControlFilter = new SessionControlFilter();
kickoutSessionControlFilter.setCache(cacheManager());
kickoutSessionControlFilter.setSessionManager(sessionManager());
kickoutSessionControlFilter.setKickoutAfter(false);
kickoutSessionControlFilter.setMaxSession(1);
kickoutSessionControlFilter.setKickoutUrl("/common/kickout");
return kickoutSessionControlFilter;
}
/***
* 授权所用配置
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/***
* 使授权注解起作用不如不想配置可以在pom文件中加入
*
*org.springframework.boot
*spring-boot-starter-aop
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* Shiro生命周期处理器
* 此方法需要用static作为修饰词,否则无法通过@Value()注解的方式获取配置文件的值
*
*/
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
至此为止我们的配置就差不多完成了,中间有其它的一些类我们有贴出来,如果你编写到这里的话找不到那些类 ,可以去我的项目地址将项目下载下来看看吧。
编写相关类进行测试 进行到这里我们就可以进行相关的测试了,首先编写一个用户登录的Controller
package com.candy.candydemo.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.candy.candydemo.entity.SysUser;
import com.candy.candydemo.entity.vo.Result;
import com.candy.candydemo.exception.ResultStatusCode;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/admin")
public class LoginController {
@RequestMapping("/login")
public Result login(String loginName, String pwd){
try {
UsernamePasswordToken token = new UsernamePasswordToken(loginName, pwd);
//登录不在该处处理,交由shiro处理
Subject subject = SecurityUtils.getSubject();
subject.login(token);
if (subject.isAuthenticated()) {
JSON json = new JSONObject();
((JSONObject) json).put("Token", subject.getSession().getId());
return new Result(ResultStatusCode.OK, json);
}else{
return new Result(ResultStatusCode.SHIRO_ERROR);
}
}catch (IncorrectCredentialsException | UnknownAccountException e){
return new Result(ResultStatusCode.NOT_EXIST_USER_OR_ERROR_PWD);
}catch (LockedAccountException e){
return new Result(ResultStatusCode.USER_FROZEN);
}catch (Exception e){
return new Result(ResultStatusCode.SYSTEM_ERR);
}
}
/**
* 退出登录
* @return
*/
@RequestMapping("/logout")
public Result logout(){
SecurityUtils.getSubject().logout();
return new Result(ResultStatusCode.OK);
}
}
接下来是我们没有权限或者没登陆时的跳转的Controller
package com.candy.candydemo.controller;
import com.candy.candydemo.entity.SysUser;
import com.candy.candydemo.entity.vo.Result;
import com.candy.candydemo.exception.ResultStatusCode;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.web.session.mgt.WebSessionKey;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RequestMapping("/common")
@RestController
public class CommonController {
/**
* 未授权跳转方法
* @return
*/
@RequestMapping("/unauth")
public Result unauth(){
//SysUser principal = (SysUser) SecurityUtils.getSubject().getPrincipal();
SecurityUtils.getSubject().logout();
return new Result(ResultStatusCode.UNAUTHO_ERROR);
}
/**
* 被踢出后跳转方法
* @return
*/
@RequestMapping("/kickout")
public Result kickout(){
return new Result(ResultStatusCode.INVALID_TOKEN);
}
}
最后是我们测试权限是否启用了的类
package com.candy.candydemo.controller;
import com.candy.candydemo.entity.SysUser;
import com.candy.candydemo.service.SysUserService;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("test")
public class TestController {
@RequiresRoles("admin") //表示只有admin这个角色才能访问这个方法
@RequestMapping("test")
public String test(){
return "欢迎你,admin" ;
}
}
测试结果展示 首先我们来访问一下我们的测试类,看看是什么结果,使用postman访问接口http://localhost:8080/test/test,结果应该是下图那样
文章图片
这是因为我们还未登录,所以获取不到我们的权限,接下来我们来执行登录操作,访问接口http://localhost:8080/admin/login
文章图片
如果是上图显示,就说明我们登录成功了,注意,这里的token需要我们复制下来,等一会访问其它接口时作为请求头传到后台。还记得我们之前编写的sessionManager那个配置类吗?我们向后端的请求首先要经过sessionManager的验证,验证请求头中是否存在token,如果存在就获取,否则就在cookie中寻找我们的sessionid.
然后我们再来测试一下刚才的接口
文章图片
注意在访问的时候选择Headers并将刚才Token保存的sessionId传进去,再次访问发现接口访问成功了,至此,我们的shiro就全部完成了。
这篇博文我并没有花很多时间编写,所以内容上可能会有一些不全面,如果看到这里的话项目依然没有跑起来的话,赶紧去我的码云上把代码下载下来,配合项目和博文来进行学习吧。
项目地址:https://gitee.com/qizhongxiao/candy-demo.git 参考博文:https://blog.csdn.net/zhourenfei17/article/details/83543002 【java|springboot+redis+shiro实现前后端分离的权限管理(自定义session管理和redis缓存)】看完之后如果还有什么不懂得地方,欢迎评论哦!
如果有更好得建议,可以留言,我也会进行改进得!
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 关于QueryWrapper|关于QueryWrapper,实现MybatisPlus多表关联查询方式
- MybatisPlus使用queryWrapper如何实现复杂查询
- python学习之|python学习之 实现QQ自动发送消息
- 事件代理
- 孩子不是实现父母欲望的工具——林哈夫
- opencv|opencv C++模板匹配的简单实现
- Java|Java OpenCV图像处理之SIFT角点检测详解
- Node.js中readline模块实现终端输入
- java中如何实现重建二叉树