最近有个项目需要前后端分离开发并且设置权限,对比目前比较流行的三种权限架构(spring security 、shiro、jwt)后,由于本阶段需求没那么高,(听说spring security权限细粒度更高??)权限的操作粒度能控制在路径及按钮上,数据粒度通过sql实现。Shrio简单够用。下面贴上整合代码:
1、pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.oa</groupId>
<artifactId>test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>test</name>
<description>前后端分离shiro整合</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Excel -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.14</version>
</dependency>
<!--热部署,楼主配置的时候出现在shiro授权的时候,用户类型不能cast,注释掉热部署后,问题解决 -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency> -->
<!--逆向工程 -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
<!--json -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20160810</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!--数据库 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--文件上传 -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.2</version>
</dependency>
<!--字符串工具类 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!-- commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- shiro spring. -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- shiro ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro-redis做sessionid缓存管理,优点:断电保存,session共享 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.8.24</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、application.yml
server:
#服务端口号
port: 8081
tomcat:
uri-encoding: utf-8
#spring配置
spring:
#文件大小
servlet:
multipart:
max-file-size: -1
shiro:
redis:
host: localhost
port: 6379
password: 123456
timeout: 50000
#redis
redis:
# Redis服务器连接密码(默认为空)
host: localhost
port: 6379
password: 123456
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 5000
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 30
# 连接池中的最小空闲连接
min-idle: 5
# 连接超时时间(毫秒)
timeout: 50000
#数据源一配置
datasource-form:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/test1?characterEncoding=UTF-8
username: root
password: 123456
#数据源二配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/test2
username: root
password: 123456
# 初始化大小,最小,最大
initialSize: 1
minIdle: 3
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 30000
validationQuery: select 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 打开PSCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,slf4j
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
#useGlobalDataSourceStat: true
#mybatis配置
mybatis:
mapper-locations: classpath:mybatis/mapper/oa/*.xml,mybatis/mapper/form/*.xml
config-location: classpath:mybatis/mybatis-config.xml
#log4j show sql
logging:
level:
cn.oa.mapper: debug
3、mybatis双数据源配置
(1)数据源一:
datasource配置:
import javax.sql.DataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DataSourceConfig {
@Bean(name="dataSourceForm")
@ConfigurationProperties(prefix = "spring.datasource-form")
public DataSource dataSourceForm() {
return DataSourceBuilder.create().build();
}
}
配置mybatis:
package cn.oa.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.mybatis.spring.SqlSessionTemplate;
@Configuration
@MapperScan(basePackages = {"cn.oa.mapper.form"},sqlSessionFactoryRef = "sqlSessionFactory2")
public class MybatisFormConfig {
@Autowired
@Qualifier("dataSourceForm")
private DataSource ds2;
@Bean
public SqlSessionFactory sqlSessionFactory2() throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(ds2);
factory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/form/*.xml")); //必须指定位置
return factory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate2() throws Exception {
SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory2());
return template;
}
@Bean(name = "transactionManager2")
public PlatformTransactionManager transactionManager2() {
return new DataSourceTransactionManager(ds2);
}
}
(2)配置数据源二:使用druid
使用druid配置datasource:
package cn.oa.config;
import java.sql.SQLException;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import com.alibaba.druid.wall.WallConfig;
import com.alibaba.druid.wall.WallFilter;
@SuppressWarnings("all")
@Configuration
public class DruidConfig {
@Value("${spring.datasource.jdbc-url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.initialSize}")
private int initialSize;
@Value("${spring.datasource.minIdle}")
private int minIdle;
@Value("${spring.datasource.maxActive}")
private int maxActive;
@Value("${spring.datasource.maxWait}")
private int maxWait;
@Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.validationQuery}")
private String validationQuery;
@Value("${spring.datasource.testWhileIdle}")
private boolean testWhileIdle;
@Value("${spring.datasource.testOnBorrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.testOnReturn}")
private boolean testOnReturn;
@Value("${spring.datasource.poolPreparedStatements}")
private boolean poolPreparedStatements;
@Value("${spring.datasource.maxPoolPreparedStatementPerConnectionSize}")
private int maxPoolPreparedStatementPerConnectionSize;
@Value("${spring.datasource.filters}")
private String filters;
@Value("{spring.datasource.connectionProperties}")
private String connectionProperties;
@Bean(name="dataSourceOA",initMethod = "init", destroyMethod = "close")
@Primary
public DataSource dataSource() {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(this.dbUrl);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
/** configuration */
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
datasource
.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setValidationQuery(validationQuery);
datasource.setTestWhileIdle(testWhileIdle);
datasource.setTestOnBorrow(testOnBorrow);
datasource.setTestOnReturn(testOnReturn);
datasource.setPoolPreparedStatements(poolPreparedStatements);
datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
try {
datasource.setFilters(filters);
} catch (SQLException e) {
e.printStackTrace();
}
datasource.setConnectionProperties(connectionProperties);
return datasource;
}
@Bean
public WallFilter wallFilter(){
WallFilter wallFilter = new WallFilter();
//允许执行多条SQL
WallConfig config = new WallConfig();
config.setMultiStatementAllow(true);
wallFilter.setConfig(config);
return wallFilter;
}
/**
*
* http://127.0.0.1:8081/druid/login.html
*
* @Title: druidServlet
* @Description: 注册一个StatViewServlet 相当于在web.xml中声明了一个servlet
* @param: void
* @return: ServletRegistrationBean
* @throws
*/
@Bean
public ServletRegistrationBean druidServlet() {
ServletRegistrationBean reg = new ServletRegistrationBean();
reg.setServlet(new StatViewServlet());
reg.addUrlMappings("/druid/*");
/** 白名单 */
reg.addInitParameter("allow", "127.0.0.1");
/** IP黑名单(共同存在时,deny优先于allow) */
// reg.addInitParameter("deny", "192.168.2.105");
/** /druid/login.html登录时账号密码 */
reg.addInitParameter("loginUsername", "root");
reg.addInitParameter("loginPassword", "root");
/** 是否能够重置数据 禁用HTML页面上的“Reset All”功能 */
reg.addInitParameter("resetEnable", "false");
return reg;
}
/**
* 注册一个:filterRegistrationBean 相当于在web.xml中声明了一个Filter
*/
@Bean
public FilterRegistrationBean druidStatFilter() {
FilterRegistrationBean druidStatFilter = new FilterRegistrationBean();
druidStatFilter.setFilter(new WebStatFilter());
/** 添加过滤规则. */
druidStatFilter.addUrlPatterns("/*");
/** 监控选项滤器 */
druidStatFilter.addInitParameter("DruidWebStatFilter", "/*");
/** 添加不需要忽略的格式信息. */
druidStatFilter.addInitParameter("exclusions",
"*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
/** 配置profileEnable能够监控单个url调用的sql列表 */
druidStatFilter.addInitParameter("profileEnable", "true");
/** 当前的cookie的用户 */
druidStatFilter.addInitParameter("principalCookieName", "USER_COOKIE");
/** 当前的session的用户 */
druidStatFilter
.addInitParameter("principalSessionName", "USER_SESSION");
return druidStatFilter;
}
}
mybatis配置:
package cn.oa.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import cn.oa.utils.ArrayUtils;
@Configuration
@MapperScan(basePackages = {"cn.oa.mapper.oa.custom","cn.oa.mapper.oa"},sqlSessionFactoryRef = "sqlSessionFactory1")
public class MybatisOAConfig {
@Autowired
@Qualifier("dataSourceOA")
private DataSource ds1;
@Bean
public SqlSessionFactory sqlSessionFactory1() throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(ds1);
//使用两个mapper位置,办法是组合两个resource数组
Resource[] res = new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/oa/*.xml");
Resource[] resCustom = new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/oa/custom/*.xml");
factory.setMapperLocations(ArrayUtils.connectResource(res, resCustom));
return factory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate1() throws Exception {
SqlSessionTemplate template = new SqlSessionTemplate(sqlSessionFactory1());
return template;
}
}
4、shiro配置:
(1)自定义realm
package cn.oa.utils.shiro;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import cn.oa.pojo.Auth;
import cn.oa.pojo.Role;
import cn.oa.pojo.User;
import cn.oa.service.AuthService;
public class AuthRealm extends AuthorizingRealm {
@Autowired
private AuthService authService;
// 认证.登录
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户的输入的账号.
String username = (String) token.getPrincipal();
//通过username从数据库中查找 User对象,如果找到,没找到.
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
User user = authService.getUser(username);
if (user == null) {
return null;
}
if (user.getStatus() == 2) { //账户冻结
throw new LockedAccountException();
}
return new SimpleAuthenticationInfo(user, // 用户名
user.getPassword(), // 密码
this.getClass().getName());
}
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User user = (User) principal.getPrimaryPrincipal();
Role role = authService.getRole(user.getRoleId());
if (role != null) {
authorizationInfo.addRole(role.getRolename());
if (role.getAuths() != null && role.getAuths().size() > 0) {
for (Auth auth : role.getAuths()) {
authorizationInfo.addStringPermission(auth.getAuthPermission());
}
}
}
return authorizationInfo;
}
}
(2)自定义密码校验器:
package cn.oa.utils.shiro;
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;
import org.apache.shiro.crypto.hash.Md5Hash;
public class CredentialsMatcher extends SimpleCredentialsMatcher{
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken utoken=(UsernamePasswordToken) token;
//获得用户输入的密码:(可以采用加盐(salt)的方式去检验)
String inPassword = new String(utoken.getPassword());
/**oa加盐,md5散列两次*/
Md5Hash md5Hash=new Md5Hash(inPassword,"test",2);
//获得数据库中的密码
String dbPassword=(String) info.getCredentials();
//进行密码的比对
return this.equals(md5Hash.toString(), dbPassword);
}
}
(3)重点:因为是前后端分离,所以要将sessionId存储以后返回给前端,所以重写sessionId获取方法
package cn.oa.utils.shiro;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有 Authorization 则其值为sessionId
if (!StringUtils.isEmpty(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);
}
}
}
(4)全局异常处理自定义:
package cn.oa.utils.shiro;
import com.alibaba.fastjson.support.spring.FastJsonJsonView;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
public class MyExceptionHandler implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception ex) {
ModelAndView mv = new ModelAndView();
FastJsonJsonView view = new FastJsonJsonView();
Map<String, Object> attributes = new HashMap<String, Object>(5);
if (ex instanceof UnauthenticatedException) {
attributes.put("code", "400");
attributes.put("msg", "token错误");
} else if (ex instanceof UnauthorizedException) {
attributes.put("code", "403");
attributes.put("msg", "用户无权限");
} else {
attributes.put("code", "500");
attributes.put("msg", ex.getMessage());
}
view.setAttributesMap(attributes);
mv.setView(view);
return mv;
}
}
(5)最后的核心:shiro权限过滤器及其相关配置:
package cn.oa.config;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerExceptionResolver;
import cn.oa.utils.shiro.AuthRealm;
import cn.oa.utils.shiro.CredentialsMatcher;
import cn.oa.utils.shiro.MyExceptionHandler;
import cn.oa.utils.shiro.MySessionManager;
@Configuration
public class ShiroConfig {
@Value("${spring.shiro.redis.host}")
private String host;
@Value("${spring.shiro.redis.port}")
private int port;
@Value("${spring.shiro.redis.timeout}")
private int timeout;
@Value("${spring.shiro.redis.password}")
private String password;
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
/*Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
//注意过滤器配置顺序 不能颠倒
//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
filterChainDefinitionMap.put("/logout", "logout");
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/**", "authc");
//配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
shiroFilterFactoryBean.setLoginUrl("/unauth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);*/
return shiroFilterFactoryBean;
}
//配置自定义的权限登录器
@Bean(name="authRealm")
public AuthRealm authRealm(@Qualifier("credentialsMatcher") CredentialsMatcher matcher) {
AuthRealm authRealm=new AuthRealm();
authRealm.setCredentialsMatcher(matcher);
return authRealm;
}
//配置自定义的密码比较器
@Bean(name="credentialsMatcher")
public CredentialsMatcher credentialsMatcher() {
return new CredentialsMatcher();
}
/**
* 配置安全管理器
*/
@Bean
public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(authRealm);
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(cacheManager());
return securityManager;
}
//自定义sessionManager
@Bean
public SessionManager sessionManager() {
MySessionManager mySessionManager = new MySessionManager();
mySessionManager.setSessionDAO(redisSessionDAO());
//不在地址栏显示sessionId
mySessionManager.setSessionIdUrlRewritingEnabled(false);
mySessionManager.setSessionValidationSchedulerEnabled(true);
return mySessionManager;
}
/**
* 配置shiro redisManager
* <p>
* 使用的是shiro-redis开源插件
*
* @return
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
redisManager.setExpire(1800);// 配置缓存过期时间(30分钟)
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return redisManager;
}
/**
* cacheManager 缓存 redis实现
* <p>
* 使用的是shiro-redis开源插件
*
* @return
*/
@Bean
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* <p>
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/*@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}*/
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 注册全局异常处理
* @return
*/
@Bean(name = "exceptionHandler")
public HandlerExceptionResolver handlerExceptionResolver() {
return (HandlerExceptionResolver) new MyExceptionHandler();
}
}
5、redis配置:
package cn.oa.config;
import java.time.Duration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisClientConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
public class RedisConfig {
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;
@Value("${spring.redis.password}")
private String password;
/**
* @author gaodeqiang
* @Description redis连接池
* @date 2018年8月16日上午9:41:12
*/
@Bean
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 最大空闲数
jedisPoolConfig.setMaxIdle(maxIdle);
// 连接池的最大数据库连接数
jedisPoolConfig.setMaxTotal(maxActive);
// 最大建立连接等待时间
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMinIdle(minIdle);
return jedisPoolConfig;
}
/**
* @Description 单机版配置
* @date 2018年8月16日上午9:41:01
*/
@Bean
public JedisConnectionFactory JedisConnectionFactory(JedisPoolConfig jedisPoolConfig){
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration ();
redisStandaloneConfiguration.setHostName(host);
redisStandaloneConfiguration.setPort(port);
redisStandaloneConfiguration.setPassword(RedisPassword.of(password));
JedisClientConfiguration.JedisClientConfigurationBuilder jedisClientConfiguration = JedisClientConfiguration.builder();
jedisClientConfiguration.connectTimeout(Duration.ofMillis(timeout));// connection timeout
JedisConnectionFactory factory = new JedisConnectionFactory(redisStandaloneConfiguration,
jedisClientConfiguration.build());
return factory;
}
/**
* @Description 实例化redisTemplate对象
* @date 2018年8月16日上午9:41:37
*/
@Bean
public RedisTemplate<String, Object> functionDomainRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
initDomainRedisTemplate(redisTemplate, redisConnectionFactory);
return redisTemplate;
}
/**
* @Description 设置数据存入 redis 的序列化方式,并开启事务
* @date 2018年8月16日上午9:42:06
*/
private void initDomainRedisTemplate(RedisTemplate<String, Object> redisTemplate, RedisConnectionFactory factory) {
//如果不配置Serializer,那么存储的时候缺省使用String,如果用User类型存储,那么会提示错误User can't cast to String!
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 开启事务
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.setConnectionFactory(factory);
}
/* *//**
* @Description 注入封装RedisTemplate
* @date 2018年8月17日下午2:50:58
*//*
@Bean(name = "redisUtils")
public RedisUtils redisUtil(RedisTemplate<String, Object> redisTemplate) {
RedisUtils redisUtil = new RedisUtils();
redisUtil.setRedisTemplate(redisTemplate);
return redisUtil;
}*/
}
封装redis工具类使用:(直接autowired注入RedisUtils即可)
package cn.oa.utils;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
@Service
public class RedisUtils {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
*
* @param key
* 键
* @param time
* 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key
* 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key
* 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key
* 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key
* 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key
* 键
* @param value
* 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key
* 键
* @param value
* 值
* @param time
* 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key
* 键
* @param by
* 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key
* 键
* @param by
* 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
*
* @param key
* 键 不能为null
* @param item
* 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key
* 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key
* 键
* @param map
* 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key
* 键
* @param map
* 对应多个键值
* @param time
* 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key
* 键
* @param item
* 项
* @param value
* 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key
* 键
* @param item
* 项
* @param value
* 值
* @param time
* 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key
* 键 不能为null
* @param item
* 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key
* 键 不能为null
* @param item
* 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key
* 键
* @param item
* 项
* @param by
* 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key
* 键
* @param item
* 项
* @param by
* 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key
* 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key
* 键
* @param value
* 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key
* 键
* @param values
* 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key
* 键
* @param time
* 时间(秒)
* @param values
* 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key
* 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key
* 键
* @param values
* 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key
* 键
* @param start
* 开始
* @param end
* 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key
* 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key
* 键
* @param index
* 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key
* 键
* @param value
* 值
* @param time
* 时间(秒)
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key
* 键
* @param value
* 值
* @param time
* 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key
* 键
* @param value
* 值
* @param time
* 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key
* 键
* @param value
* 值
* @param time
* 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key
* 键
* @param index
* 索引
* @param value
* 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key
* 键
* @param count
* 移除多少个
* @param value
* 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}