PHPER转JAVA记录篇-spring boot+shiro+jwt+redis
spring boot + shiro + jwt + redis
一、场景描述
1、前言
基于上一篇spring boot 整合shiro,当时想把shiro引入到我们的spring boot中,引入完了之后,我发现一个问题,那就是shiro是基于session的。那么我是不是可以引入到接口来做呢?比如我们的会员系统,会员分为好几个级别【白银、黄金、铂金、钻石、星耀、王者、荣耀王者】,目前只有王者和荣耀王者才可以打巅峰赛。
2、模拟逻辑(纯属杜撰)
我们先模拟一种场景
角色有:silver、gold、platinum、diamonds、starshine、king、glory
会员有:user1、user2、user3…userX
权限有:common_activity(普通活动)、star_activity(星耀活动)、king_activity(王者活动)、call_up_mode(征召模式)、common_mode(普通竞技模式)…
我们可以说,首先配置角色和权限关系
1、silver、gold、platinum、diamonds、starshine、king、glory 都有 common_activity、common_mode
2、diamonds、starshine、king、glory 都有call_up_mode
2、starshine、king、glory 都有 star_activity
3、king、glory 都有 king_activity
写到这里,大家觉得也没毛病啊,就用shiro做权限管理,没问题的,但是因为我们兼容安卓和IOS,所以是不是涉及到前后端分离呢?这样session无效了,怎么办。
3、JWT如何配合shiro来完成权限
这个相关的文章一大堆:
1、spring-boot-shiro-jwt-redis实现登陆授权功能
2、spring-boot-shiro-jwt-redis
这类文章太多了,我们就不在赘述了,不过这里有一个东西可以注意一下。
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
这玩意如果不知道在哪?可以看看上面2篇参考文章。这里是说要把session关闭的一个逻辑,同时备注上也说了,参考:关闭shiro自带的session,详情见文档
二、度娘告诉我们的做法
1、每次都需要登录
我们可以结合他的shiroConfig对于JwtFilter的注入知道,每个请求,都必须先登录,然后在做权限判断。
JwtFilter
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
throw new AuthenticationException("Token失效请重新登录");
}
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(CommonConstant.ACCESS_TOKEN);
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
shiroRealm类
/**
* 功能: 用来进行身份认证,也就是说验证用户输入的账号和密码是否正确,获取身份验证信息,错误抛出异常
*
* @param auth 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
if (token == null) {
log.info("————————身份认证失败——————————IP地址: " + CommonUtils.getIpAddrByRequest(SpringContextUtils.getHttpServletRequest()));
throw new AuthenticationException("token为空!");
}
// 校验token有效性
SysUser loginUser = this.checkUserTokenIsEffect(token);
return new SimpleAuthenticationInfo(loginUser, token, getName());
}
/**
* 校验token的有效性
*
* @param token
*/
public SysUser checkUserTokenIsEffect(String token) throws AuthenticationException {
// 解密获得username,用于和数据库进行对比
String username = JwtUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token非法无效!");
}
// 查询用户信息
SysUser loginUser = new SysUser();
SysUser sysUser = sysUserService.getUserByName(username);
if (sysUser == null) {
throw new AuthenticationException("用户不存在!");
}
// 校验token是否超时失效 & 或者账号密码是否错误
if (!jwtTokenRefresh(token, username, sysUser.getPassWord())) {
throw new AuthenticationException("Token失效请重新登录!");
}
// 判断用户状态
if (!"0".equals(sysUser.getDelFlag())) {
throw new AuthenticationException("账号已被删除,请联系管理员!");
}
BeanUtils.copyProperties(sysUser, loginUser);
return loginUser;
}
2、我解决的问题
你有没有发现,每次调用登录,其实每次都是在调用数据库,这样对于性能是否有所损害呢?所以我们想要改变点什么对不对?
2.1、我发现按照度娘的做法,session依然有效
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
实验证明,利用这段代码,我依然可以正常登录,说明session依然是有效的,这个事情就比较奇怪了,我命名禁用了session的存储,但是其实底层本质就是一个hash,所以不存储也没关系,全局变量还是帮我搞定了,咋办?参考文章:Shiro禁用Session,使用SSM+JWT+Shiro进行无状态RESTful API
这篇文章中,有这么一段代码
<!--Shiro-->
<!-- Realm实现 -->
<bean id="statelessRealm" class="shiro.MyStatelessRealm">
<property name="cachingEnabled" value="false"/>
</bean>
<!-- 禁用掉会话调度器 -->
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
<property name="sessionValidationSchedulerEnabled" value="false"/>
</bean>
<!--解决报错,组装默认的subjectDAO-->
<bean id="subjectDAO" class="org.apache.shiro.mgt.DefaultSubjectDAO">
<property name="sessionStorageEvaluator" ref="sessionStorageEvaluator"/>
</bean>
<bean id="sessionStorageEvaluator" class="org.apache.shiro.mgt.DefaultSessionStorageEvaluator">
<property name="sessionStorageEnabled" value="false"/>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="statelessRealm"/>
<property name="subjectDAO" ref="subjectDAO"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
<property name="arguments" ref="securityManager"/>
</bean>
翻译过来是这样子的
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
Logging.getLogger().info("关闭shiro自带的session");
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
DefaultSessionManager defaultSessionManager = new DefaultSessionManager();
defaultSessionManager.setSessionValidationSchedulerEnabled(false);
securityManager.setSubjectDAO(subjectDAO);
securityManager.setRealm(myShiroRealm());
securityManager.setSessionManager(defaultSessionManager);
return securityManager;
}
至此,我终于不能通过session去登录了,but,我发现另外一个问题,url上面,会带有sessionid,怎么办?最后我们发现shiro-1.3.2版本,有个比较好的设置,配置如下
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
Logging.getLogger().info("关闭shiro自带的session");
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setSessionIdCookieEnabled(false);
defaultWebSessionManager.setSessionIdUrlRewritingEnabled(false);
securityManager.setSubjectDAO(subjectDAO);
securityManager.setRealm(myShiroRealm());
securityManager.setSessionManager(defaultWebSessionManager);
return securityManager;
}
这时候你发现你session怎么弄都没用了,且url不会带有sessionid了。
2.2、优化mysql改为redis
其实也就是把这一块改成缓存的

这样子的话,每次授权也都是可以的了,不用再担心性能问题,另外session问题也彻底失效了。
3、理想是丰满的
本以为按照之前的步骤,是比较顺畅的,不过我发现,filter根本加载不了bean,当然有参考文章:在springboot中拦截器Filter中注入bean失败解决方案,以上的解决方案,我并没有实验,我是觉得这种方案已经偏离了我的心中所想,所以接下来,我准备用aop的方式来做token这块的东西,登录还是按照之前的逻辑来做。参考文章:springmvc 在controller层使用aop切面编程,这里引发了我的一个思考,就是感觉很多时候做法有很多,但是偏离了自己心中所想的话,自己的知识很难形成体系,所以我调转过来用aop这种最纯粹的方式来做jwt,最后通过登录的方式,怼入到shiro中去。
4、理想再一次是丰满的
我原本的打算是,在config里面释放api模块
filterChainDefinitionMap.put("/api/**", "anon");
然后通过aop的方式
@Pointcut(value = "execution( * com.cs.commonapi.controller.api.*.*(..))")
public void brokerAspect() {
}
/**
* before可以简写成@Before("execution( * com.cs.commonapi.controller.api.*.*(..))")
* 这样就不需要定义切面了
* @param point
*/
@Before("brokerAspect()")
public void doBefore(JoinPoint point) {
RequestBO requestBO = getRequestInfo();
if(!doValidToken(requestBO)){
return;
}
doLogin(requestBO);
}
最后在doLogin方法里面做shiro的登录,可以登录,但是当我写 @RequiresPermissions(“vip”) 的时候我发现完了,@RequiresPermissions注解竟然比我的aop优先执行,这时候发现理想再一次是丰满的,回想起度娘上的方法,也许这是我最后的归途,因为shiro里面定制过滤器肯定比他的注解先执行,于是乎就这么干。
6、完成
通过设置jwtFilter的方式,提前进行了登录,后面操作相对来说比较正常,紧接着有一个问题,就是发现bean的加载,在filter之后,导致了我filter加载的时候,bean还有加载。主要代码如下:
入口文件:有@SpringBootApplication注解的入口
ConfigurableApplicationContext context = SpringApplication.run(CommonApiApplication.class, args);
SpringContextUtil.setApplicationContext(context);
SpringContextUtil 类
public class SpringContextUtil{
private static ApplicationContext applicationContext;
public static void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}
/**
* 获取上下文
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通过 bena 名称获取上下文中的 bean
*/
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
/**
* 通过类型获取上下文中的bean
*/
public static Object getBean(Class<?> requiredType) {
return applicationContext.getBean(requiredType);
}
}
JwtFilter 类的部分内容
public class JwtFilter extends AccessControlFilter {
private JwtData jwtData;
private RedisUtil redisUtil;
/**
* 验证白名单,有些东西不需要认证的。
*/
private String[] uriWhiteList = {
"/api/auth/create"
};
private void initBean(){
jwtData = (JwtData)SpringContextUtil.getBean("jwtData");
redisUtil = (RedisUtil)SpringContextUtil.getBean("redisUtil");
}
}
通过这种方式,将我的逻辑串联起来了。但是我发现认证虽然做了缓存,但是获取权限接口,会有多次调用,所以为了加快效率,这个地方也行缓存处理。
对了缓存,我是这么做的,给一个获取用户信息的缓存。用了一组重载来做的。
public class UserCache {
private static final String USER_CACHE_PRE = "API-USER-CACHE:";
private static final int CACHE_TIME = 7200;
public static ShiroUser get(RedisUtil redisUtil,ShiroUserService shiroUserService,String userName,boolean isForceSelect){
if(redisUtil.hasKey(createKey(userName)) && !isForceSelect){
String userJson = redisUtil.get(createKey(userName)).toString();
Logging.getLogger().info("从缓存中获取");
return JSONArray.parseObject(userJson.getBytes(),ShiroUser.class);
}
Logging.getLogger().info("从数据库中获取");
ShiroUser shiroUser = shiroUserService.queryByUserName(userName);
redisUtil.set(createKey(userName),JSONArray.toJSON(shiroUser).toString(),CACHE_TIME);
return shiroUser;
}
public static ShiroUser get(RedisUtil redisUtil,ShiroUserService shiroUserService,String userName){
return get(redisUtil,shiroUserService,userName,false);
}
private static String createKey(String userName){
return USER_CACHE_PRE+userName;
}
}
针对Filter里面的异常无法抛出的问题:Springboot2.x 整合shiro 解决自定义异常无法抛出问题给个传送门,此文章比较长,切勿心浮气躁,可以解决问题,有必要看完
更多推荐
所有评论(0)