网上关于shiro的整合文章不少,但很多并不适用于前后端分离/移动端的项目
1.未登录,shiro会自动重定向到 /login
2.访问路径无权限,shiro会抛出401 http错误
org.apache.shiro.web.filter.authz.AuthorizationFilter
下有onAccessDenied方法
,源码:protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
Subject subject = this.getSubject(request, response);
if (subject.getPrincipal() == null) {
this.saveRequestAndRedirectToLogin(request, response); // 重定向逻辑
} else {
String unauthorizedUrl = this.getUnauthorizedUrl();
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
WebUtils.toHttp(response).sendError(401);
}
}
return false;
}
重写此方法即可解决重定向的问题
强行使用也可以,但是就必须做其它大量的工作,除了需要解决刚提到的跳转路径问题,其它的如跨域sessionId问题、或需修改为传递sessionId做shiro登录校验,以及集群下session共享问题。emmm,还有session机制本身的安全问题等等。
这显然是费时费力的,那么有没有其它的办法可以解决呢?
答案当然是有的,我们可以在整合shiro的基础上继续整合jwt,或者oauth2.0等,或者自定义登录校验,使其成为支持服务端无状态登录,即token登录。
登录认证通过后返回AccessToken信息(在AccessToken中保存当前的时间戳和帐号),同时在Redis中设置一条以帐号为Key,Value为当前时间戳(登录时间)的RefreshToken,现在认证时必须AccessToken没失效以及Redis存在所对应的RefreshToken,且RefreshToken时间戳和AccessToken信息中时间戳一致才算认证通过,这样可以做到JWT的可控性,如果重新登录获取了新的AccessToken,旧的AccessToken就认证不了,因为Redis中所存放的的RefreshToken时间戳信息只会和最新的AccessToken信息中携带的时间戳一致,这样每个用户就只能使用最新的AccessToken认证
Redis的RefreshToken也可以用来判断用户是否在线,如果删除Redis的某个RefreshToken,那这个RefreshToken所对应的AccessToken之后也无法通过认证了,就相当于控制了用户的登录,可以剔除用户
本身AccessToken的过期时间为5分钟(配置文件可配置),RefreshToken过期时间为30分钟(配置文件可配置),当登录后时间过了5分钟之后,当前AccessToken便会过期失效,再次带上AccessToken访问JWT会抛出TokenExpiredException异常说明Token过期,开始判断是否要进行AccessToken刷新,首先redis查询RefreshToken是否存在,以及时间戳和过期AccessToken所携带的时间戳是否一致,如果存在且一致就进行AccessToken刷新。
刷新后新的AccessToken过期时间依旧为5分钟(配置文件可配置),时间戳为当前最新时间戳,同时也设置RefreshToken中的时间戳为当前最新时间戳,刷新过期时间重新为30分钟过期(配置文件可配置),最终将刷新的AccessToken存放在Response的Header中的Authorization字段返回。
同时前端进行获取替换,下次用新的AccessToken进行访问
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.14version>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.0.0version>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.56version>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.7.0version>
dependency>
## tomcat配置 - start
# 指定服务端口
server.port=8999
## tomcat配置 - end
## 数据库配置 - start
#spring.datasource.url=jdbc:mysql://localhost:3306/springboot-test?serverTimezone=GMT%2B8
spring.datasource.url=jdbc:mysql://192.168.5.58:3306/181_saas?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
## 数据库配置 - end
# druid连接池配置 - start
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.filters=stat
spring.datasource.druid.max-active=20
spring.datasource.druid.initial-size=1
spring.datasource.druid.min-idle=1
spring.datasource.druid.max-wait=60000
spring.datasource.druid.time-between-eviction-runs-millis=60000
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.validation-query=select 'x'
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.pool-prepared-statements=true
spring.datasource.druid.max-open-prepared-statements=20
# druid连接池配置 - end
## myBatis - start
# 打印SQL语句
mybatis.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
## myBatis - end
## Redis配置 - start
# Redis数据库索引(默认为0)
spring.redis.database=1
# Redis服务器地址
spring.redis.host=192.168.5.58
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=yibayi_181~jishubu*007
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=5000
## Redis配置 - end
## 其它参数配置 - start
# AES密码加密私钥(Base64加密)
encryptAESKey=V2FuZzkyNuYSKIuwqTQkFQSUpXVA
# JWT认证加密私钥(Base64加密)
encryptJWTKey=U0JBUElOENhspJrzkyNjQ1NA
# AccessToken过期时间-5分钟-5*60(秒为单位)
accessTokenExpireTime=300
# RefreshToken过期时间-30分钟-30*60(秒为单位)
refreshTokenExpireTime=1800
# Shiro缓存过期时间-5分钟-5*60(秒为单位)(一般设置与AccessToken过期时间一致)
shiroCacheExpireTime=300
## 其它参数配置 - end
package com.liang.po.vo;
import com.liang.po.constant.ResultCode;
import java.util.HashMap;
import java.util.Map;
public class R {
//链式编程
private Boolean success;
private Integer code;
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Map<String, Object> getData() {
return data;
}
public void setData(Map<String, Object> data) {
this.data = data;
}
private String message;
private Map<String,Object> data = new HashMap<>();
private R(){}
//操作成功,调用这个方法,返回成功的数据
public static R ok(){
R r = new R();
r.setSuccess(true);
r.setCode(ResultCode.SUCCESS);
r.setMessage("操作成功");
return r;
}
//操作失败,调用这个方法,返回失败的数据
public static R error(){
R r = new R();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage("操作失败");
return r;
}
//操作失败,调用这个方法,返回失败的数据
public static R error(String message){
R r = new R();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage(message);
return r;
}
//使用链式编程
public R success(Boolean success){
this.setSuccess(success);
return this;
}
public R message(String message){
this.setMessage(message);
return this;
}
public R code(Integer code){
this.setCode(code);
return this;
}
public R data(String key, Object value){
this.data.put(key, value);
return this;
}
public R data(Map<String, Object> map){
this.setData(map);
return this;
}
}
package com.liang.po.constant;
//定义返回数据使用的状态码
public interface ResultCode {
int SUCCESS = 20000; //成功状态码
int ERROR = 20001;//失败状态码
int AUTH = 30000;//没有操作权限状态码
}
filter 中将无法正常注入bean 问题:
问题原因: web容器加载顺序导致, 加载顺序是listener——filter——servlet
,当项目启动时,filter先于servlet初始化, 而Spring中默认bean的初始化是在Servlet后进行的,所以会注入失败。
解决方法: 将filter添加到spring容器
FilterRegistrationBean
或者DelegatingFilterProxyRegistrationBean
注册filter, 为什么可以这么实现呢?因为FilterRegistrationBean
和DelegatingFilterProxyRegistrationBean
都继承了AbstractFilterRegistrationBean
抽象类,而该抽象类实现了ServletContextInitializer
接口, Spring容器初始化时会遍历查找实现ServletContextInitializer
接口的bean,并调用onStartup
方法注册自定义的filter
。public FilterRegistrationBean filterRegistrationBean(){
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
//registrationBean.setFilter(new JwtFilter());
//registrationBean.addInitParameter("targetFilterLifecycle","true");
//registrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico");
//registrationBean.addUrlPatterns("/*");
//registrationBean.setDispatcherTypes(DispatcherType.REQUEST);
return registrationBean;
}
@Bean("jwtFilter")
public JwtFilter jwtFilterBean() {
return new JwtFilter();
}
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
//根据 bean 的名字 获取指定的bean对象
//bean 有个 默认策略,类名首字母小写
public static Object getBean(String BeanName){
Object bean = context.getBean(BeanName);
return bean;
}
}
如果你学过 spring 你应该明白 下面的 xml 是什么意思
我们可以得知Shiro在使用注解的时的配置是
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
bean>
其基于注解的权限控制功能
AuthorizationAttributeSourceAdvisor
)DefaultAdvisorAutoProxyCreator
动态代理创建的根据上文,启用基于注解的AspectJ就很简单了,因为基于注解的Shiro一般也在SpringMVC的Context里,我们采用如下配置
<aop:aspectj-autoproxy proxy-target-class="true"/>
在两者分别配置的时候,配置方法都是对的但是一旦公用,会发现AspectJ会失效
失效的解决办法
注释之后Shiro的注解权限管理功能并不会失效,具体原因我们来细细分析
DefaultAdvisorAutoProxyCreator
加入参数proxyTargetClass
为true
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true"/>
bean>
配置失效的原因
原因在于二次代理:
解决方法生效的原因
1, 方法一
因为Shiro也是基于Spring的AOP类的,如果找不到合适的配置,就是默认采用同一个Context下的AOP代理配置,我们给了其proxy-target-class为true,自然就在第二次代理的时候找得到方法
2,方法二
方法二就更直接了,告诉 DefaultAdvisorAutoProxyCreator为True 就好
补充:
DefaultAdvisorAutoProxyCreator
是用来扫描上下文,寻找所有的Advistor(通知器),将这些Advisor应用到所有符合切入点的Bean中。所以必须在lifecycleBeanPostProcessor
创建之后创建,所以加
@DependsOn({"lifecycleBeanPostProcessor"})
保证创建DefaultAdvisorAutoProxyCreator 之前先创建LifecycleBeanPostProcessor
。
package com.yby.saas.config.shiro;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
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.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import com.yby.saas.config.jwt.JwtFilter;
import com.yby.saas.config.shiro.cache.CustomCacheManager;
@Configuration
public class ShiroConfig {
/**
* 配置使用自定义Realm,关闭Shiro自带的session 详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
@Bean("securityManager")
public DefaultWebSecurityManager getManager(UserRealm userRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自定义Realm
manager.setRealm(userRealm);
// 关闭Shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
// 设置自定义Cache缓存
manager.setCacheManager(new CustomCacheManager());
return manager;
}
/**
* 添加自己的过滤器,自定义url规则 详情见文档 http://shiro.apache.org/web.html#urls-
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器取名为jwt
Map<String, Filter> filterMap = new HashMap<>(16);
filterMap.put("jwtFilter", jwtFilterBean());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
// 自定义url规则
Map<String, String> filterRuleMap = new HashMap<>(16);
// 所有请求通过我们自己的JWTFilter
filterRuleMap.put("/**", "jwtFilter");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
*
* 注入bean,此处应注意:
*
* (1)代码顺序,应放置于shiroFilter后面,否则报错:
* No SecurityManager accessible to the calling code, either bound to the org.apache.shiro.util.
* ThreadContext or as a vm static singleton. This is an invalid application configuration.
*
* (2)如不在此注册,在filter中将无法正常注入bean
*
*/
@Bean("jwtFilter")
public JwtFilter jwtFilterBean() {
return new JwtFilter();
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题,https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
// AuthorizationAttributeSourceAdvisor匹配所有类,匹配所有加了认证注解的方法
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
用springboot写前后端分离项目的时候, 用postman测试接口没问题,但丢给前端测试的时候返回了跨域问题:
这就很奇怪,所有的controller上我都加了@CrossOrigin注解,为什么还会有跨域问题呢?
这里有个需要注意的地方: xx has been blocked by CORS policy
这个CORS是个什么东西呢?简单说是一种资源共享机制。当浏览器发起ajax请求的时候,会先发起一个method为OPTIONS的请求, 这个请求我们可以简单理解为一个探路请求, 该请求不携带信息, 只是为了测试一下目标服务器是否支持跨域,如果支持跨域的话,再发出后续的请求。
通过chrome控制台可以看出, 当我们尝试调用listByAdmin接口的时候,发出了一个OPTIONS请求,该请求返回200后,浏览器又发起了一个一模一样的请求,后边这个请求才是真正的请求,当然这是正常的情况。
问题就出在这个OPTIONS请求上, 因为我的项目是完全前后端分离的,前端请求的时候会在http header上携带token,后端利用token鉴权。然而OPTIONS请求(‘OPTIONS请求’这个叫法可能不准确,各位理解我说的是啥就行)默认是不携带token信息的,后端没有收到token,导致鉴权失败。
但是鉴权失败为什么报鉴权问题,反而报了个跨域问题呢?
先看一下一开始我是处理鉴权失败的
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 对跨域提供支持。自定义过滤器Filter进行JWT登陆令牌验证并设置响应头实现跨域
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers",
httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
判断 是否是 option 请求,如果是,给OPTIONS请求直接返回正常状态。
既然我们需要更改shiro默认的登录拦截,那首先就需得重写shiro中的BasicHttpAuthenticationFilter
,此处使用jwt做登录拦截
package com.yby.saas.config.jwt;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.yby.saas.exception.CustomException;
import com.yby.saas.po.constant.JwtConstant;
import com.yby.saas.po.constant.RedisConstant;
import com.yby.saas.po.constant.StatusCode;
import com.yby.saas.po.vo.JsonVo;
import com.yby.saas.redis.RedisClient;
import com.yby.saas.util.JwtUtil;
import com.yby.saas.util.common.JsonConvertUtil;
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Value("${refreshTokenExpireTime}")
private String refreshTokenExpireTime;
@Autowired
private RedisClient redis;
/**
* LOGGER
*/
private static final Logger LOGGER = LoggerFactory.getLogger(JwtFilter.class);
/**
* 这里我们详细说明下为什么最终返回的都是true,即允许访问 例如我们提供一个地址 GET /article 登入用户和游客看到的内容是不同的
* 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西 所以我们在这里返回true,Controller中可以通过
* subject.isAuthenticated() 来判断用户是否登入
* 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 判断用户是否想要登入
if (this.isLoginAttempt(request, response)) {
try {
// 进行Shiro的登录UserRealm
this.executeLogin(request, response);
} catch (Exception e) {
// 认证出现异常,传递错误信息msg
String msg = e.getMessage();
// 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常))
Throwable throwable = e.getCause();
if (throwable != null && throwable instanceof SignatureVerificationException) {
// 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
msg = "token或者密钥不正确(" + throwable.getMessage() + ")";
} else if (throwable != null && throwable instanceof TokenExpiredException) {
// 该异常为JWT的AccessToken已过期,判断RefreshToken未过期就进行AccessToken刷新
if (this.refreshToken(request, response)) {
return true;
} else {
msg = "token已过期(" + throwable.getMessage() + ")";
}
} else {
// 应用异常不为空
if (throwable != null) {
// 获取应用异常msg
msg = throwable.getMessage();
}
}
/**
* 错误两种处理方式 1. 将非法请求转发到/401的Controller处理,抛出自定义无权访问异常被全局捕捉再返回Response信息 2.
* 无需转发,直接返回Response信息 一般使用第二种(更方便)
*/
// 直接返回Response信息
this.response401(request, response, msg);
return false;
}
}
return true;
}
/**
* 这里我们详细说明下为什么重写 可以对比父类方法,只是将executeLogin方法调用去除了
* 如果没有去除将会循环调用doGetAuthenticationInfo方法
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
this.sendChallenge(request, response);
return false;
}
/**
* 检测Header里面是否包含Authorization字段,有就进行Token登录认证授权
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
// 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
String token = this.getAuthzHeader(request);
return token != null;
}
/**
* 进行AccessToken登录认证授权
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
// 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
JwtToken token = new JwtToken(this.getAuthzHeader(request));
// 提交给UserRealm进行认证,如果错误他会抛出异常并被捕获
this.getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 此处为AccessToken刷新,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
*/
private boolean refreshToken(ServletRequest request, ServletResponse response) {
// 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
String token = this.getAuthzHeader(request);
// 获取当前Token的帐号信息
String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
// 判断Redis中RefreshToken是否存在
if (redis.hasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
// Redis中RefreshToken还存在,获取RefreshToken的时间戳
String currentTimeMillisRedis = redis.get(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account).toString();
// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
if (JwtUtil.getClaim(token, JwtConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
// 获取当前最新时间戳
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
// 读取配置文件,获取refreshTokenExpireTime属性
// PropertiesUtil.readProperties("config.properties");
// String refreshTokenExpireTime =
// PropertiesUtil.getProperty("refreshTokenExpireTime");
// 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间重新为30分钟过期(配置文件可配置refreshTokenExpireTime属性)
redis.set(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis,
Integer.parseInt(refreshTokenExpireTime));
// 刷新AccessToken,设置时间戳为当前最新时间戳
token = JwtUtil.sign(account, currentTimeMillis);
// 将新刷新的AccessToken再次进行Shiro的登录
JwtToken jwtToken = new JwtToken(token);
// 提交给UserRealm进行认证,如果错误他会抛出异常并被捕获,如果没有抛出异常则代表登入成功,返回true
this.getSubject(request, response).login(jwtToken);
// 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Authorization", token);
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
return true;
}
}
return false;
}
/**
* 无需转发,直接返回Response信息
*/
private void response401(ServletRequest req, ServletResponse resp, String msg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
PrintWriter out = null;
try {
out = httpServletResponse.getWriter();
String data = JsonConvertUtil.objectToJson(new JsonVo(StatusCode.NOT_LOGIN, "无权访问(Unauthorized):" + msg));
out.append(data);
} catch (IOException e) {
LOGGER.error("直接返回Response信息出现IOException异常:" + e.getMessage());
throw new CustomException("直接返回Response信息出现IOException异常:" + e.getMessage());
} finally {
if (out != null) {
out.close();
}
}
}
/**
* 对跨域提供支持。自定义过滤器Filter进行JWT登陆令牌验证并设置响应头实现跨域
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers",
httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
package com.yby.saas.config.jwt;
import org.apache.shiro.authc.AuthenticationToken;
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 1900286977895826147L;
/**
* Token
*/
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
package com.yby.saas.config.shiro;
import java.util.List;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
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 org.springframework.stereotype.Service;
import com.yby.saas.config.jwt.JwtToken;
import com.yby.saas.dao.permission.PermissionCustomMapper;
import com.yby.saas.dao.role.RoleCustomMapper;
import com.yby.saas.dao.user.UserCustomMapper;
import com.yby.saas.po.Permission;
import com.yby.saas.po.Role;
import com.yby.saas.po.User;
import com.yby.saas.po.constant.JwtConstant;
import com.yby.saas.po.constant.RedisConstant;
import com.yby.saas.redis.RedisClient;
import com.yby.saas.util.JwtUtil;
import com.yby.saas.util.common.StringUtil;
@Service
public class UserRealm extends AuthorizingRealm {
@Autowired
private RedisClient redis;
@Autowired
private UserCustomMapper userMapper;
@Autowired
private RoleCustomMapper roleMapper;
@Autowired
private PermissionCustomMapper permissionMapper;
/**
* 大坑,必须重写此方法,不然Shiro会报错。
* 每一个Ream都有一个supports方法,用于检测是否支持此Toke。
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
String account = JwtUtil.getClaim(principals.toString(), JwtConstant.ACCOUNT);
// 查询用户角色
List<Role> roles = roleMapper.getRoleByMobile(account);
for (int i = 0, roleLen = roles.size(); i < roleLen; i++) {
Role role = roles.get(i);
// 添加角色
simpleAuthorizationInfo.addRole(role.getName());
// 根据用户角色查询权限
List<Permission> permissions = permissionMapper.getPermissionByRoleId(role.getId());
for (int j = 0, perLen = permissions.size(); j < perLen; j++) {
Permission permission = permissions.get(j);
// 添加权限
simpleAuthorizationInfo.addStringPermission(permission.getSn());
}
}
return simpleAuthorizationInfo;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
// 解密获得account,用于和数据库进行对比
String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
// 帐号为空
if (StringUtil.isBlank(account)) {
throw new AuthenticationException("Token中帐号为空(The account in Token is empty.)");
}
// 查询用户是否存在
User user = userMapper.getByMobile(account);
if (user == null) {
throw new AuthenticationException("该帐号不存在(The account does not exist.)");
}
// 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致
if (JwtUtil.verify(token) && redis.hasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
// 获取RefreshToken的时间戳
String currentTimeMillisRedis = redis.get(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account).toString();
// 获取AccessToken时间戳,与RefreshToken的时间戳对比
if (JwtUtil.getClaim(token, JwtConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
return new SimpleAuthenticationInfo(token, token, "userRealm");
}
}
throw new AuthenticationException("Token已过期(Token expired or incorrect.)");
}
}
package com.yby.saas.config.shiro.cache;
import java.util.Collection;
import java.util.Set;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import com.yby.saas.po.constant.JwtConstant;
import com.yby.saas.po.constant.RedisConstant;
import com.yby.saas.redis.RedisClient;
import com.yby.saas.util.JwtUtil;
public class CustomCache<K, V> implements Cache<K, V> {
@Value("${shiroCacheExpireTime}")
private String shiroCacheExpireTime;
@Autowired
private RedisClient redis;
/**
* 缓存的key名称获取为shiro:cache:account
*
* @param key
* @return java.lang.String
* @author Wang926454
* @date 2018/9/4 18:33
*/
private String getKey(Object key) {
return RedisConstant.PREFIX_SHIRO_CACHE + JwtUtil.getClaim(key.toString(), JwtConstant.ACCOUNT);
}
/**
* 获取缓存
*/
@Override
public Object get(Object key) throws CacheException {
if (!redis.hasKey(this.getKey(key))) {
return null;
}
return redis.get(this.getKey(key));
}
/**
* 保存缓存
*/
@Override
public Object put(Object key, Object value) throws CacheException {
// 读取配置文件,获取Redis的Shiro缓存过期时间
// PropertiesUtil.readProperties("config.properties");
// String shiroCacheExpireTime =
// PropertiesUtil.getProperty("shiroCacheExpireTime");
// 设置Redis的Shiro缓存
return redis.set(this.getKey(key), value, Integer.parseInt(shiroCacheExpireTime));
}
/**
* 移除缓存
*/
@Override
public Object remove(Object key) throws CacheException {
if (!redis.hasKey(this.getKey(key))) {
return null;
}
redis.del(this.getKey(key));
return null;
}
/**
* 清空所有缓存
*/
@Override
public void clear() throws CacheException {
// TODO Auto-generated method stub
}
/**
* 缓存的个数
*/
@Override
public Set<K> keys() {
// TODO Auto-generated method stub
return null;
}
/**
* 获取所有的key
*/
@Override
public int size() {
// TODO Auto-generated method stub
return 0;
}
/**
* 获取所有的value
*/
@Override
public Collection<V> values() {
// TODO Auto-generated method stub
return null;
}
}
package com.yby.saas.config.shiro.cache;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
public class CustomCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return new CustomCache<K, V>();
}
}
package com.yby.saas.config.redis;
import java.lang.reflect.Method;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
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;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.databind.ObjectMapper;
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
// 秘钥生成器类KeyGenerator
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
if (params != null && params.length > 0 && params[0] != null) {
for (Object obj : params) {
sb.append(obj.toString());
}
}
return sb.toString();
}
};
}
/**
* RedisTemplate
*/
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
package com.yby.saas.redis;
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.Component;
import org.springframework.util.CollectionUtils;
/**
*
* RedisTemplate工具类:
* 针对所有的hash都是以h开头的方法
* 针对所有的Set都是以s开头的方法(不含通用方法)
* 针对所有的List都是以l开头的方法
*
*
*/
@Component
public class RedisClient {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = 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;
}
}
}
package com.yby.saas.exception;
public class CustomException extends RuntimeException {
private static final long serialVersionUID = -6736944294947154413L;
public CustomException(String msg) {
super(msg);
}
public CustomException() {
super();
}
}
package com.yby.saas.exception;
/**
* 自定义401无权限异常(UnauthorizedException)
*/
public class CustomUnauthorizedException extends RuntimeException {
private static final long serialVersionUID = -3993376696547776573L;
public CustomUnauthorizedException(String msg) {
super(msg);
}
public CustomUnauthorizedException() {
super();
}
}
package com.yby.saas.config;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import com.yby.saas.exception.CustomException;
import com.yby.saas.po.constant.StatusCode;
import com.yby.saas.po.vo.JsonVo;
@RestControllerAdvice
public class ExceptionAdvice {
/**
* 捕捉所有Shiro异常
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(ShiroException.class)
public JsonVo handle401(ShiroException e) {
JsonVo vo = new JsonVo();
vo.setCode(StatusCode.UNLAWFUL);
vo.setMsg("无权访问(Unauthorized):" + e.getMessage());
return vo;
}
/**
* 单独捕捉Shiro(UnauthorizedException)异常 该异常为访问有权限管控的请求而该用户没有所需权限所抛出的异常
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(UnauthorizedException.class)
public JsonVo handle401(UnauthorizedException e) {
JsonVo vo = new JsonVo();
vo.setCode(StatusCode.UNLAWFUL);
vo.setMsg("无权访问(Unauthorized):当前Subject没有此请求所需权限(" + e.getMessage() + ")");
return vo;
}
/**
* 单独捕捉Shiro(UnauthenticatedException)异常
* 该异常为以游客身份访问有权限管控的请求无法对匿名主体进行授权,而授权失败所抛出的异常
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(UnauthenticatedException.class)
public JsonVo handle401(UnauthenticatedException e) {
JsonVo vo = new JsonVo();
vo.setCode(StatusCode.UNLAWFUL);
vo.setMsg("无权访问(Unauthorized):当前Subject是匿名Subject,请先登录(This subject is anonymous.)");
return vo;
}
/**
* 捕捉校验异常(BindException)
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BindException.class)
public JsonVo validException(BindException e) {
JsonVo vo = new JsonVo();
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
Map<String, Object> result = this.getValidError(fieldErrors);
vo.setCode(StatusCode.ERROR);
vo.setMsg(result.get("errorMsg").toString());
vo.setObj(result.get("errorList"));
return vo;
}
/**
* 捕捉校验异常(MethodArgumentNotValidException)
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public JsonVo validException(MethodArgumentNotValidException e) {
JsonVo vo = new JsonVo();
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
Map<String, Object> result = this.getValidError(fieldErrors);
vo.setCode(StatusCode.ERROR);
vo.setMsg(result.get("errorMsg").toString());
vo.setObj(result.get("errorList"));
return vo;
}
/**
* 捕捉404异常
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NoHandlerFoundException.class)
public JsonVo handle(NoHandlerFoundException e) {
JsonVo vo = new JsonVo();
vo.setCode(StatusCode.NOT_FOUND);
vo.setMsg(e.getMessage());
return vo;
}
/**
* 捕捉其他所有异常
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public JsonVo globalException(HttpServletRequest request, Throwable ex) {
// return new JsonVo(this.getStatus(request).value(), ex.toString() + ": " +
// ex.getMessage(), null);
JsonVo vo = new JsonVo();
vo.setCode(StatusCode.SERVER_ERROR);
vo.setMsg(ex.toString() + ": " + ex.getMessage());
return vo;
}
/**
* 捕捉其他所有自定义异常
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(CustomException.class)
public JsonVo handle(CustomException e) {
JsonVo vo = new JsonVo();
vo.NO();
vo.setMsg(e.getMessage());
return vo;
}
/**
* 获取状态码
*/
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
return HttpStatus.valueOf(statusCode);
}
/**
* 获取校验错误信息
*/
private Map<String, Object> getValidError(List<FieldError> fieldErrors) {
Map<String, Object> result = new HashMap<String, Object>(16);
List<String> errorList = new ArrayList<String>();
StringBuffer errorMsg = new StringBuffer("校验异常(ValidException):");
for (FieldError error : fieldErrors) {
errorList.add(error.getField() + "-" + error.getDefaultMessage());
errorMsg.append(error.getField() + "-" + error.getDefaultMessage() + ".");
}
result.put("errorList", errorList);
result.put("errorMsg", errorMsg);
return result;
}
}
package com.yby.saas.util;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.yby.saas.exception.CustomException;
import com.yby.saas.po.constant.JwtConstant;
import com.yby.saas.util.common.Base64Util;
@Component
public class JwtUtil {
/**
* LOGGER
*/
private static final Logger LOGGER = LoggerFactory.getLogger(JwtUtil.class);
/**
* 过期时间改为从配置文件获取
*/
private static String accessTokenExpireTime;
/**
* JWT认证加密私钥(Base64加密)
*/
private static String encryptJWTKey;
@Value("${accessTokenExpireTime}")
public void setAccessTokenExpireTime(String accessTokenExpireTime) {
JwtUtil.accessTokenExpireTime = accessTokenExpireTime;
}
@Value("${encryptJWTKey}")
public void setEncryptJWTKey(String encryptJWTKey) {
JwtUtil.encryptJWTKey = encryptJWTKey;
}
/**
* 校验token是否正确
*
* @param token
* Token
* @return boolean 是否正确
* @author Wang926454
* @date 2018/8/31 9:05
*/
public static boolean verify(String token) {
try {
// 帐号加JWT私钥解密
String secret = getClaim(token, JwtConstant.ACCOUNT) + Base64Util.decodeThrowsException(encryptJWTKey);
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (UnsupportedEncodingException e) {
LOGGER.error("JWTToken认证解密出现UnsupportedEncodingException异常:" + e.getMessage());
throw new CustomException("JWTToken认证解密出现UnsupportedEncodingException异常:" + e.getMessage());
}
}
/**
* 获得Token中的信息无需secret解密也能获得
*
* @param token
* @param claim
* @return java.lang.String
* @author Wang926454
* @date 2018/9/7 16:54
*/
public static String getClaim(String token, String claim) {
try {
DecodedJWT jwt = JWT.decode(token);
// 只能输出String类型,如果是其他类型返回null
return jwt.getClaim(claim).asString();
} catch (JWTDecodeException e) {
LOGGER.error("解密Token中的公共信息出现JWTDecodeException异常:" + e.getMessage());
throw new CustomException("解密Token中的公共信息出现JWTDecodeException异常:" + e.getMessage());
}
}
/**
* 生成签名
*
* @param account
* 帐号
* @return java.lang.String 返回加密的Token
* @author Wang926454
* @date 2018/8/31 9:07
*/
public static String sign(String account, String currentTimeMillis) {
try {
// 帐号加JWT私钥加密
String secret = account + Base64Util.decodeThrowsException(encryptJWTKey);
// 此处过期时间是以毫秒为单位,所以乘以1000
Date date = new Date(System.currentTimeMillis() + Long.parseLong(accessTokenExpireTime) * 1000);
Algorithm algorithm = Algorithm.HMAC256(secret);
// 附带account帐号信息
return JWT.create().withClaim("account", account).withClaim("currentTimeMillis", currentTimeMillis)
.withExpiresAt(date).sign(algorithm);
} catch (UnsupportedEncodingException e) {
LOGGER.error("JWTToken加密出现UnsupportedEncodingException异常:" + e.getMessage());
throw new CustomException("JWTToken加密出现UnsupportedEncodingException异常:" + e.getMessage());
}
}
}
package com.yby.saas.util;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.yby.saas.exception.CustomUnauthorizedException;
import com.yby.saas.util.common.Base64Util;
import com.yby.saas.util.common.HexConvertUtil;
@Component
public class AesUtil {
/**
* AES密码加密私钥(Base64加密)
*/
private static String encryptAESKey = "V2FuZzkyNuYSKIuwqTQkFQSUpXVA";
// private static final byte[] KEY = { 1, 1, 33, 82, -32, -85, -128, -65 };
@Value("${encryptAESKey}")
public void setEncryptAESKey(String encryptAESKey) {
AesUtil.encryptAESKey = encryptAESKey;
}
/**
* LOGGER
*/
private static final Logger LOGGER = LoggerFactory.getLogger(AesUtil.class);
/**
* 加密
*/
public static String encode(String str) {
try {
Security.addProvider(new com.sun.crypto.provider.SunJCE());
// 实例化支持AES算法的密钥生成器(算法名称命名需按规定,否则抛出异常)
// KeyGenerator 提供对称密钥生成器的功能,支持各种算法
KeyGenerator keygen = KeyGenerator.getInstance("AES");
// 将私钥encryptAESKey先Base64解密后转换为byte[]数组按128位初始化
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(Base64Util.decodeThrowsException(encryptAESKey).getBytes());
keygen.init(128, secureRandom);
// SecretKey 负责保存对称密钥 生成密钥
SecretKey deskey = keygen.generateKey();
// 生成Cipher对象,指定其支持的AES算法,Cipher负责完成加密或解密工作
Cipher c = Cipher.getInstance("AES");
// 根据密钥,对Cipher对象进行初始化,ENCRYPT_MODE表示加密模式
c.init(Cipher.ENCRYPT_MODE, deskey);
byte[] src = str.getBytes();
// 该字节数组负责保存加密的结果
byte[] cipherByte = c.doFinal(src);
// 先将二进制转换成16进制,再返回Bsae64加密后的String
return Base64Util.encodeThrowsException(HexConvertUtil.parseByte2HexStr(cipherByte));
} catch (NoSuchAlgorithmException e) {
LOGGER.error("getInstance()方法异常:" + e.getMessage());
throw new CustomUnauthorizedException("getInstance()方法异常:" + e.getMessage());
} catch (UnsupportedEncodingException e) {
LOGGER.error("Bsae64加密异常:" + e.getMessage());
throw new CustomUnauthorizedException("Bsae64加密异常:" + e.getMessage());
} catch (NoSuchPaddingException e) {
LOGGER.error("getInstance()方法异常:" + e.getMessage());
throw new CustomUnauthorizedException("getInstance()方法异常:" + e.getMessage());
} catch (InvalidKeyException e) {
LOGGER.error("初始化Cipher对象异常:" + e.getMessage());
throw new CustomUnauthorizedException("初始化Cipher对象异常:" + e.getMessage());
} catch (IllegalBlockSizeException e) {
LOGGER.error("加密异常,密钥有误:" + e.getMessage());
throw new CustomUnauthorizedException("加密异常,密钥有误:" + e.getMessage());
} catch (BadPaddingException e) {
LOGGER.error("加密异常,密钥有误:" + e.getMessage());
throw new CustomUnauthorizedException("加密异常,密钥有误:" + e.getMessage());
}
}
/**
* 解密
*/
public static String decode(String str) {
try {
Security.addProvider(new com.sun.crypto.provider.SunJCE());
// 实例化支持AES算法的密钥生成器(算法名称命名需按规定,否则抛出异常)
// KeyGenerator 提供对称密钥生成器的功能,支持各种算法
KeyGenerator keygen = KeyGenerator.getInstance("AES");
// 将私钥encryptAESKey先Base64解密后转换为byte[]数组按128位初始化
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
secureRandom.setSeed(Base64Util.decodeThrowsException(encryptAESKey).getBytes());
keygen.init(128, secureRandom);
// SecretKey 负责保存对称密钥 生成密钥
SecretKey deskey = keygen.generateKey();
// 生成Cipher对象,指定其支持的AES算法,Cipher负责完成加密或解密工作
Cipher c = Cipher.getInstance("AES");
// 根据密钥,对Cipher对象进行初始化,DECRYPT_MODE表示解密模式
c.init(Cipher.DECRYPT_MODE, deskey);
// 该字节数组负责保存加密的结果,先对str进行Bsae64解密,将16进制转换为二进制
String base64 = Base64Util.decodeThrowsException(str);
byte[] cipherByte = c.doFinal(HexConvertUtil.parseHexStr2Byte(base64));
return new String(cipherByte);
} catch (NoSuchAlgorithmException e) {
LOGGER.error("getInstance()方法异常:" + e.getMessage());
throw new CustomUnauthorizedException("getInstance()方法异常:" + e.getMessage());
} catch (UnsupportedEncodingException e) {
LOGGER.error("Bsae64加密异常:" + e.getMessage());
throw new CustomUnauthorizedException("Bsae64加密异常:" + e.getMessage());
} catch (NoSuchPaddingException e) {
LOGGER.error("getInstance()方法异常:" + e.getMessage());
throw new CustomUnauthorizedException("getInstance()方法异常:" + e.getMessage());
} catch (InvalidKeyException e) {
LOGGER.error("初始化Cipher对象异常:" + e.getMessage());
throw new CustomUnauthorizedException("初始化Cipher对象异常:" + e.getMessage());
} catch (IllegalBlockSizeException e) {
LOGGER.error("解密异常,密钥有误:" + e.getMessage());
throw new CustomUnauthorizedException("解密异常,密钥有误:" + e.getMessage());
} catch (BadPaddingException e) {
LOGGER.error("解密异常,密钥有误:" + e.getMessage());
throw new CustomUnauthorizedException("解密异常,密钥有误:" + e.getMessage());
}
}
}
package com.yby.saas.util.common;
import java.security.MessageDigest;
import java.util.UUID;
public class Md5Util {
public static String encode(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes());
byte b[] = md.digest();
int i;
StringBuffer buf = new StringBuffer("");
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
str = buf.toString();
} catch (Exception e) {
e.printStackTrace();
}
return str;
}
/**
* 带盐值加密
*
* @param str
* 待加密字符串
* @param salt
* 盐值
*/
public static String encode(String str, String salt) {
return encode(str + salt);
}
public static void main(String[] args) {
String salt = UUID.randomUUID().toString().replace("-", "");
System.out.println("salt:" + salt);
System.out.println(encode("a123456" + "de93210d922540cb8b4686b7aca08d49"));
}
}
package com.yby.saas.util.common;
import java.io.UnsupportedEncodingException;
import java.util.Base64;
/**
* Base64工具
*
* @author lwx
*/
public class Base64Util {
/**
* 加密JDK1.8
*/
public static String encode(String str) {
try {
byte[] encodeBytes = Base64.getEncoder().encode(str.getBytes("utf-8"));
return new String(encodeBytes);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
/**
* 解密JDK1.8
*/
public static String decode(String str) {
try {
byte[] decodeBytes = Base64.getDecoder().decode(str.getBytes("utf-8"));
return new String(decodeBytes);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
/**
* 加密JDK1.8
*/
public static String encodeThrowsException(String str) throws UnsupportedEncodingException {
byte[] encodeBytes = Base64.getEncoder().encode(str.getBytes("utf-8"));
return new String(encodeBytes);
}
/**
* 解密JDK1.8
*/
public static String decodeThrowsException(String str) throws UnsupportedEncodingException {
byte[] decodeBytes = Base64.getDecoder().decode(str.getBytes("utf-8"));
return new String(decodeBytes);
}
}
/**
* 登录
*/
@PostMapping("/login")
public JsonVo login(String account, String password, HttpServletResponse response) {
JsonVo vo = new JsonVo();
if (StringUtils.isEmpty(account) || StringUtils.isEmpty(password)) {
vo.setCode(StatusCode.PARAM_ERROR);
return vo;
}
// 查询数据库中的帐号信息
User affirm = userService.getByMobile(account);
if (affirm == null) {
vo.setCode(StatusCode.NOT_FOUND);
return vo;
}
// Md5加密
if (!Md5Util.encode(password + affirm.getSalt()).equals(affirm.getPassword())) {
vo.setCode(StatusCode.PASSWORD_ERROR);
return vo;
}
// 清除可能存在的shiro权限信息缓存
if (redis.hasKey(RedisConstant.PREFIX_SHIRO_CACHE + account)) {
redis.del(RedisConstant.PREFIX_SHIRO_CACHE + account);
}
// 设置RefreshToken,时间戳为当前时间戳,直接设置即可(不用先删后设,会覆盖已有的RefreshToken)
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
redis.set(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis,
Integer.parseInt(refreshTokenExpireTime));
// 从Header中Authorization返回AccessToken,时间戳为当前时间戳
String token = JwtUtil.sign(account, currentTimeMillis);
response.setHeader("Authorization", token);
response.setHeader("Access-Control-Expose-Headers", "Authorization");
vo.OK();
return vo;
}
/**
* 获取当前登录用户
*/
public User getCurrent() {
try {
Subject subject = SecurityUtils.getSubject();
if (subject != null) {
String token = (String) subject.getPrincipal();
if (StringUtil.isNotBlank(token)) {
String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
if (StringUtil.isNotBlank(account)) {
return userService.getByMobile(account);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 获取当前登录用户ID
*/
public Long getCurrentId() {
User current = getCurrent();
if (current != null) {
return current.getId();
}
return null;
}
用POST方式访问登录接口:http://localhost:8999/login?account=15813922171&password=123456
登录成功后返回如下信息:
重点在返回的Headers,红框部分即我们想要的token,前端请求接口时携带此token即可:
在Headers处添加参数如下:
Content-Type:application/json
Authorization:上一步获取到的token
上面的只有重要的代码,还有一些常量类没有贴出来,这些代码也是我看大佬的,其实代码并不重要。重要的是学习这种思路,以及遇到的问题,如何发现问题,分析问题,解决问题。我也是查了大量的博客,才看明白大佬的代码,也才解决,明白这过程中出现的问题。当初学习 shiro 的时候,并没有想到还有这么多的问题,看了这个代码,以及查阅大量博客后,学习到了很多。