上文授权篇中,我们已经完成了对token的颁发及清除,上述操作实际上并不需要真正与shiro进行整合。在这一篇章中我将会说明关于整合shiro后如何进行token的鉴权,同时这也将是实现无状态登录鉴权的最后重头戏。
一、Maven配置
- 主要配置如下:
<!-- Spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</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-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- sqlserver -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>sqljdbc4</artifactId>
<scope>4.0</scope>
<version>4.0</version>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>8.4.1.jre8</version>
<scope>runtime</scope>
</dependency>
<!-- mybatis_plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- swagger ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--增加两个配置(解决swagger访问配置首页报异常问题)-->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.22</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.22</version>
</dependency>
<!--jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-spring.version}</version>
</dependency>
<!-- commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
二、Application配置
spring:
aop:
# proxy-target-class属性值决定是基于接口的还是基于类的代理被创建。属性值被设置为true,那么基于类的代理将起作用(这时需要cglib库)。
# 如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用。
proxy-target-class: true
auto: true
# 配置数据库连接池
datasource:
primary:
jdbc-url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=xxx
username: xx
password: xxxxx
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
db2:
jdbc-url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=xxx
username: xx
password: xxxxx
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
## Redis配置 - start
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间(毫秒)
timeout: 5000
## Redis配置 - end
## rabbitmq配置 - start
rabbitmq:
host: 127.0.0.1
port: 5672
username: xxx
password: xxx
virtual-host: dev
## rabbitmq配置 - end
#返回JSON的全局时间格式 -start
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
#返回JSON的全局时间格式 -start
## servlet文件上传 -start
servlet:
multipart:
#最大上传单个文件大小:默认1M
max-file-size: 1024MB
# 最大置总上传的数据大小 :默认10M
max-request-size: 1024MB
## servlet文件上传 -end
## 阿里短信配置 - start
aliyun:
sms:
accessKeyId: xxxx
accessKeySecret: xxxx
##用户注册验证码
register_template_code: xxx
##短信测试验证码
sms_test_template_code: xxx
##登陆确认验证码
login_template_code: xxx
##登陆异常验证码
logerr_template_code: xxx
##修改密码验证码
up_template_code: xxx
##身份验证验证码
auth_template_code: xxx
##活动确认验证码
activ_confirm_template_code: xxx
##信息变更验证码
info_change_template_code: xxx
sign_name: xxxx
## 阿里短信配置 - end
##oss 阿里云图片上传 - start
oss:
key: xxxx
secret: xxxx
endpoint: xxxx
bucket-name: xxxx
userInfoPic-path: xxxx
##oss 阿里云图片上传 - end
## 阿里视频点播配置 - start
vod:
accessKeyId: xxxx
accessKeySecret: xxxx
## 阿里视频点播配置 - end
## 阿里视频直播播配置 - start
live:
key: xxxx
oss: xxxx
## 阿里视频直播配置 - end
## 阿里支付配置 - start 沙箱配置
#pay:
#appId: xxxx
#rsaPrivateKey: xxxx
#alipayPublicKey: xxxx
#notifyUrl: xxxx
#returnUrl: xxxx
#signType: xxxx
#charset: xxxx
#gatewayurl: xxxx
## 阿里支付配置 - end
## 腾讯Im配置 - start
tencentyun:
sdk_appid: xxxx
secretkey: xxxx
## 腾讯Im配置 - end
#短信万能码开启状态 0 开启 1 关闭#
almightyCode:
status: 0
## 其它配置 - start
config:
# JWT认证加密私钥(Base64加密)
encrypt-jwtKey: xxxx
# PC端AccessToken过期时间(秒)
accessToken-expireTime: 604800
# PC端RefreshToken过期时间(604800秒/7天)
refreshToken-expireTime: 604800
## 其它配置 - end
三、重写过滤器
- 在总览篇中我已提到,鉴权流程主要是重写shiro的入口过滤器BasicHttpAuthenticationFilter。重写主要是做三件事情:
- 判断请求是否需要进行登录认证授权(可在此写拦截白名单),如果需要则该请求就必须在Header中添加Authorization字段存放AccessToken,无需授权即游客直接访问(有权限管控的话,以游客访问就会被拦截)。
- 调用getSubject(request,response).login(token),将AccessToken提交给shiro中的UserRealm进行认证。
- AccessToken刷新:判断RefreshToken是否过期,未过期就返回新的AccessToken及RefreshToken并让请求继续正常访问。
/**
* @author huangrongfu
* @date 2020/11/16 15:08
* Utils: Intellij Idea
* Description: JWT过滤器
*/
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Value("${config.refreshToken-expireTime}")
private String refreshTokenExpireTime;
@Autowired
private RedisClients redis;
/**
* 这里我详细说明下为什么最终返回的都是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)) {
log.info("=============================判断用户是否想要登入===================================");
try {
// 进行Shiro的登录UserRealm
this.executeLogin(request, response);
} catch (Exception e) {
// 认证出现异常,传递错误信息msg
String exceptionMessage = e.getMessage();
// 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常))
Throwable throwable = e.getCause();
if (throwable != null && throwable instanceof SignatureVerificationException) {
// 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
exceptionMessage = "token或者密钥不正确";
} else if (throwable != null && throwable instanceof TokenExpiredException) {
// 该异常为JWT的AccessToken已过期,判断RefreshToken未过期就进行AccessToken刷新
if (this.refreshToken(request, response)) {
return true;
} else {
exceptionMessage = "token已过期";
}
} else {
// 应用异常不为空
if (throwable != null) {
// 获取应用异常msg
exceptionMessage = throwable.getMessage();
}
}
/**
* 错误两种处理方式 1. 将非法请求转发到/401的Controller处理,抛出自定义无权访问异常被全局捕捉再返回Response信息 2.
* 无需转发,直接返回Response信息 一般使用第二种(更方便)
*/
// 直接返回Response信息
this.response401(request, response, exceptionMessage);
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 requestURI = ((HttpServletRequest) request).getRequestURI();
String token = this.getAuthzHeader(request);
return token != null;
// 默认全部都需校验
// return true;
}
/**
* 进行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);
String uid = JwtUtil.getClaim(token, JwtConstant.UID);
String userCode = JwtUtil.getClaim(token, JwtConstant.USERCODE);
JwtData jwtData = new JwtData();
jwtData.setAccount(account);
jwtData.setUid(String.valueOf(uid));
jwtData.setUserCode(userCode);
// 判断Redis中RefreshToken是否存在
if (redis.hHasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account,uid)) {
// Redis中RefreshToken还存在,获取RefreshToken的时间戳
String currentTimeMillisRedis = redis.hget(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account,uid).toString();
// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
if (JwtUtil.getClaim(token, JwtConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
// 获取当前最新时间戳
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
// 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间重新为30分钟过期(配置文件可配置refreshTokenExpireTime属性)
redis.hset(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account,uid, currentTimeMillis,
Integer.parseInt(refreshTokenExpireTime));
// 刷新AccessToken,设置时间戳为当前最新时间戳
// token = JwtUtil.sign(jwtData, 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 request, ServletResponse response, String msg) throws FailureException {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
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(ResultVoUtil.error(ResultCode.NOT_LOGIN,msg));
out.append(data);
} catch (IOException e) {
throw new FailureException("直接返回Response信息出现IOException异常:" + e.getMessage());
} finally {
if (out != null) {
out.close();
}
}
}
/**
* 对跨域提供支持
*/
@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);
}
}
四、自定义Realm
- 与常规shiro一致,我们在Realm中做相关的身份、权限等认证授权
/**
* @author huangrongfu
* @date 2020/11/16 19:24
* Utils: Intellij Idea
* Description: 自定义Realm
*/
@Slf4j
@Service
public class UserRealm extends AuthorizingRealm {
@Autowired
private RedisClients redisClient;
@Autowired
private YUserMapper userMapper;
/**
* 大坑,必须重写此方法,不然Shiro会报错
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
return simpleAuthorizationInfo;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
if (StringUtils.isBlank(token)) {
throw new AuthenticationException("token cannot be empty.");
}
// 解密获得account,用于和数据库进行对比
String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
// 帐号为空
if (StringUtils.isBlank(account)) {
throw new AuthenticationException("token中帐号为空(The account in Token is empty.)");
}
QueryWrapper<YUser> wrapper = new QueryWrapper<>();
wrapper.eq("MOBPHONE", DesUtil.desEncodeCBC(CommonConstant.KEY,account));
// 查询用户是否存在
YUser user = userMapper.selectOne(wrapper);
if (user == null) {
throw new AuthenticationException("该帐号不存在(The account does not exist.)");
}
// 开始认证,要AccessToken认证通过,且Redis中存在RefreshToken,且两个Token时间戳一致
boolean verify = JwtUtil.verify(token);
boolean flag = redisClient.hHasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account,user.getUid());
log.info("=================="+verify+":"+token+"==================");
log.info("=================="+flag+":"+account+"==================");
if (verify && flag) {
// 获取RefreshToken的时间戳
String currentTimeMillisRedis = redisClient.hget(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account,user.getUid()).toString();
// 获取AccessToken时间戳,与RefreshToken的时间戳对比
if (JwtUtil.getClaim(token, JwtConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
return new SimpleAuthenticationInfo(token, token, "userRealm");
}
}
throw new AuthenticationException("token expired or incorrect.");
}
}
五、Shiro配置
- 这里注意下关于JwtFilter的配置,由于spring boot中filter加载顺序原因,JwtFilter的Bean注入应放置于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.
- 代码如下:
/**
* @author huangrongfu
* @date 2020/11/16 19:24
* Utils: Intellij Idea
* Description: Shiro配置
*/
@Configuration
public class ShiroConfig {
/**
* 配置使用自定义Realm,关闭Shiro自带的session
*/
@Bean("securityManager")
public DefaultWebSecurityManager getManager(UserRealm userRealm, RedisTemplate<String, Object> template) {
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(template));
return manager;
}
/**
* 生成一个ShiroRedisCacheManager
**/
private CustomCacheManager cacheManager(RedisTemplate template) {
return new CustomCacheManager(template);
}
/**
* 添加自己的过滤器,自定义url规则
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(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;
}
/**
* <pre>
* 注入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
* </pre>
*/
@Bean("jwtFilter")
public JwtFilter jwtFilterBean() {
return new JwtFilter();
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
六、重写Shiro缓存
1. 重写Shiro Cache为Redis
/**
* @author huangrongfu
* @date 2020/11/16 19:24
* Utils: Intellij Idea
* Description: 重写Shiro的Cache保存读取
*/
public class CustomCache<K, V> implements Cache<K, V> {
// TODO redis @Autowired注入失败,因此改为下面采用传参形式
// @Autowired
// private RedisClient redis = new RedisClient();
// TODO @Value注入失败 @Value("${config.shiro-cache-expireTime}")
private String shiroCacheExpireTime = "9000";
private RedisTemplate<String, Object> redisTemplate;
public CustomCache(RedisTemplate redisTemplate) {
// 使用StringRedisSerializer做序列化
// redisTemplate.setValueSerializer(new StringRedisSerializer());
this.redisTemplate = redisTemplate;
}
/**
* 缓存的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 {
return redisTemplate.opsForValue().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缓存
try {
redisTemplate.opsForValue().set(this.getKey(key), value, Integer.parseInt(shiroCacheExpireTime), TimeUnit.SECONDS);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除缓存
*/
@Override
public Object remove(Object key) throws CacheException {
redisTemplate.delete(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;
}
/*
* @Override public void clear() throws CacheException {
* redis.getJedis().flushDB(); }
*/
/*
* @Override public int size() { Long size = JedisUtil.getJedis().dbSize();
* return size.intValue(); }
*/
/*
* @Override public Set keys() { Set<byte[]> keys =
* JedisUtil.getJedis().keys(new String("*").getBytes()); Set<Object> set = new
* HashSet<Object>(); for (byte[] bs : keys) {
* set.add(SerializableUtil.unserializable(bs)); } return set; }
*/
/*
* @Override public Collection values() { Set keys = this.keys(); List<Object>
* values = new ArrayList<Object>(); for (Object key : keys) {
* values.add(JedisUtil.getObject(this.getKey(key))); } return values; }
*/
}
2. 重写Shiro缓存管理器
/**
* @author huangrongfu
* @date 2020/11/16 19:24
* Utils: Intellij Idea
* Description: 重写Shiro缓存管理器
*/
public class CustomCacheManager implements CacheManager {
private RedisTemplate<String, Object> redisTemplate;
public CustomCacheManager(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return new CustomCache<K, V>(redisTemplate);
}
}
七、自定义异常
- 为方便返回统一Json提示,我们就需要对shiro的异常信息进行重写,代码如下:
/**
* @author huangrongfu
* @date 2020/11/6 11:34
* Utils: Intellij Idea
* Description: 全局异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandling{
/**
* 自定义异常
* */
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
@ExceptionHandler(value = CustomException.class)
public ResultVo processExcepetion(CustomException e){
log.error("位置:{} -> 错误信息:{}", e.getMethod() ,e.getLocalizedMessage());
return ResultVoUtil.error(Objects.requireNonNull(ResultEnum.getByCode(e.getCode())));
}
/**
* 拦截表单参数校验
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler({BindException.class})
public static ResultVo bindException(BindException e){
BindingResult bindingResult = e.getBindingResult();
return ResultVoUtil.error(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
}
/**
* 拦截JSON参数校验
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(MethodArgumentNotValidException.class)
public static ResultVo bindException(MethodArgumentNotValidException e){
BindingResult bindingResult = e.getBindingResult();
return ResultVoUtil.error(Objects.requireNonNull(bindingResult.getFieldError().getDefaultMessage()));
}
/**
* 路径参数缺失异常
*/
@ExceptionHandler(MissingPathVariableException.class)
public ResultVo missingPathVariableException(MissingPathVariableException e){
log.error("错误信息{}", e.getLocalizedMessage());
return ResultVoUtil.error(ResultEnum.PATH_VARIABLE_DEFECTS_ERROR);
}
/**
* 参数类型不匹配错误
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResultVo methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e){
log.error("错误信息{}", e.getLocalizedMessage());
return ResultVoUtil.error(ResultEnum.ARGUMENT_TYPE_MISMATCH);
}
/**
* 参数格式错误
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResultVo httpMessageNotReadable(HttpMessageNotReadableException e){
log.error("错误信息{}", e.getLocalizedMessage());
return ResultVoUtil.error(ResultEnum.FORMAT_ERROR);
}
/**
* 请求方式不支持
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResultVo httpReqMethodNotSupported(HttpRequestMethodNotSupportedException e){
log.error("错误信息{}", e.getLocalizedMessage());
return ResultVoUtil.error(ResultEnum.REQ_METHOD_NOT_SUPPORT);
}
/**
* 捕捉所有Shiro异常(401)
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(ShiroException.class)
public ResultVo handle401(ShiroException e) {
e.printStackTrace();
log.error("错误信息{}", e.getLocalizedMessage());
return ResultVoUtil.error(ResultCode.UNLAWFUL,"无权访问(Unauthorized):"+e.getMessage());
}
/**
* 单独捕捉Shiro(UnauthorizedException)异常 该异常为访问有权限管控的请求而该用户没有所需权限所抛出的异常(401)
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(UnauthorizedException.class)
public ResultVo handle401(UnauthorizedException e) {
e.printStackTrace();
return ResultVoUtil.error(ResultCode.UNLAWFUL, "无权访问(Unauthorized):当前Subject没有此请求所需权限(" + e.getMessage() + ")");
}
/**
* 单独捕捉Shiro(UnauthenticatedException)异常(401)
* 该异常为以游客身份访问有权限管控的请求无法对匿名主体进行授权,而授权失败所抛出的异常
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(UnauthenticatedException.class)
public ResultVo handle401(UnauthenticatedException e) {
e.printStackTrace();
return ResultVoUtil.error(ResultCode.UNLAWFUL, "无权访问(Unauthorized):当前Subject是匿名Subject,请先登录(This subject is anonymous.)");
}
/**
* 捕捉404异常 (404)
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(NoHandlerFoundException.class)
public ResultVo handle(NoHandlerFoundException e) {
e.printStackTrace();
return ResultVoUtil.error(ResultCode.NOT_FOUND, e.getMessage());
}
/**
* 捕捉用户登录失败拦截
* */
@ExceptionHandler(FailureException.class)
public ResultVo failureException(FailureException e) {
e.printStackTrace();
log.error("错误信息{}", e.getLocalizedMessage());
return ResultVoUtil.error(e.getMessage());
}
/**
* 捕捉重复提交失败拦截
* */
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(RepeatSubmitException.class)
public ResultVo repeatSubmitException(RepeatSubmitException e) {
e.printStackTrace();
log.error("错误信息{}", e.getLocalizedMessage());
return ResultVoUtil.error(e.getMessage());
}
/**
* 捕捉用户注册登录失败拦截
* */
@ExceptionHandler(TokenException.class)
public ResultVo tokenException(TokenException e) {
e.printStackTrace();
log.error("错误信息{}", e.getLocalizedMessage());
return ResultVoUtil.error(ResultCode.TOKEN_ERROR,e.getMessage());
}
/**
* 通用异常
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public ResultVo exception(Exception e){
e.printStackTrace();
handleOther();
return ResultVoUtil.error(ResultEnum.UNKNOWN_EXCEPTION);
}
/**
* 其它相关操作
*
* @date 2019/7/24 9:29
*/
private void handleOther() {
// 若线程发生异常,那么这里能保证 当前线程ThreadLocal存车处的内部信息 能被清理释放
ThreadLocalHelper.BASIC_INFO_MAP.remove();
}
八、获取当前登录用户
- 由于在上面的JwtFilter中我们已经把token提交给了shiro,因此直接从Subject中获取即可:
/**
* 获取当前登录用户
* @return 成功或者失败
*/
@RequiresAuthentication
@ApiOperation("当前登录用户信息")
@GetMapping("/login/user-info")
public ResultVo current() {
try {
UserVo user = null;
Subject subject = SecurityUtils.getSubject();
if (subject != null) {
String token = (String) subject.getPrincipal();
if (StringUtils.isNotBlank(token)) {
String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
if (StringUtils.isNotBlank(account)) {
user = userService.findUserByAccount(account);
String phone = DesUtil.desDecodeCBC(CommonConstant.KEY, user.getMobphone());
String realName = DesUtil.desDecodeCBC(CommonConstant.KEY, user.getRealname());
user.setMobphone(phone);
user.setRealname(realName);
}
}
}
log.info("AuthorizationController.current res ====>> {}",user);
return ResultVoUtil.success(user);
} catch (Exception e) {
e.printStackTrace();
return ResultVoUtil.error(ResultCode.ERROR, e.getMessage());
}
}