环境
SpringBoot 版本 1.5.15.RELEASE
不建议使用2.x版本的Springboot,与1.x相比很多地方代码有所改动,很麻烦。Shiro 版本 1.4.0
IntelliJ IDEA
jjwt 版本 0.9.0
lombok(可选)精简代码
思路
使用Jwt Token实现无状态登录
平时用户登录后,服务器将会把用户信息存储到Session里,在用户数量很大的时候,服务器负担会很大。而使用token方式登录,服务器不存储用户信息,而是将其加密后生成token发送给请求方,请求方在请求需要权限的资源时,将token带上,服务器解析token即可知道登录用户的信息。服务器自动刷新token
token需要刷新。对于活跃的用户,服务器自动完成刷新token;对于长期不活跃的用户,服务器通过配置的 token有效期 来检查,如果时间超过有效期的两倍,则认为该用户需要重新登录。登录流程
用户通过账号密码登录
用户登录成功后,服务器将用户信息等集合起来做成Jwt Token(字符串),然后将其放入Response里的header,并发送请求成功的json给请求方。
请求方接收到请求成功的json信息后,从header中拿出jwt token存储起来。用户请求需要验证的资源
请求方将token放入request的header,并发送请求。
服务器收到请求,检查request里的token,首先验证token合法性,不合法返回token不合法的json给请求方。
如果token合法,则检查token是否过期:
如果token签发时间到现在,已经超过了有效期,却没有超过有效期的两倍,则服务器自动生成新token,将其放入response的header,请求方接收到response后,可以检查header里是否有token,有则更新一下token预备下次请求。
如果token从签发时间到现在,已经超过有效期的两倍,则用户需要重新登录。
集成步骤
注意
@Slf4j(topic = "xxx")注解是lombok集成的日志模块,可不使用,参考:日志处理方案
数据库建表
思路:
系统里有多个角色,每个角色对于多个权限。每个权限都是一个请求url,验证权限时,后台拿到用户信息后即可知道该用户的角色,而后去数据库查询该角色所拥有的权限集合,在其中查找是否存在当前请求url,存在说明用户有访问该url的权限,否则没有权限
-- Sql -- Mysql Version 5.7-- author 1802226517@qq.com drop database if exists `rb_demo`; CREATE DATABASE rb_demo DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; USE rb_demo; -- ------------------------------ 用户部分 ------------------------------ DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `account` VARCHAR(50) NOT NULL COMMENT '账号,唯一', `password` VARCHAR(100) NOT NULL COMMENT '密码', `name` VARCHAR(100) DEFAULT '默认用户名' COMMENT '昵称', `role_id` BIGINT UNSIGNED NOT NULL COMMENT '所属角色id', `status` TINYINT UNSIGNED NOT NULL COMMENT '是否启用', `is_deleted` TINYINT UNSIGNED NOT NULL COMMENT '是否删除', `version` BIGINT UNSIGNED NOT NULL COMMENT '版本', `gmt_create` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_id` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表'; DROP TABLE IF EXISTS `role`; CREATE TABLE `role` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `name` VARCHAR(200) NOT NULL COMMENT '角色名称', `version` BIGINT UNSIGNED NOT NULL COMMENT '版本', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表'; DROP TABLE IF EXISTS `permission`; CREATE TABLE `permission` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `role_id` BIGINT UNSIGNED NOT NULL COMMENT '所属角色id', `name` VARCHAR(200) NOT NULL COMMENT '权限名称', `url` VARCHAR(200) NOT NULL COMMENT '匹配url', `version` BIGINT UNSIGNED NOT NULL COMMENT '版本', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限表';
建立Springboot项目
组件选择 web、redis和lombok,Springboot版本选择 1.5.15.RELEASE
连接数据库参考:Mybatis-Plus
编写Shiro配置类
ShiroConfig.java 这个配置类主要配置了Shiro拦截器、自定义的Realm和禁用了Session。
禁用Session方法参考代码注释。
为什么要禁用?因为我们采用Jwt Token方式完成登录验证,不需要存用户信息到Session。
package com.spz.demo.security.shiro.config;import com.spz.demo.security.shiro.filter.ShiroLoginFilter;import com.spz.demo.security.shiro.matcher.PasswordCredentialsMatcher;import com.spz.demo.security.shiro.realm.UserRealm;import com.spz.demo.security.shiro.token.UserAuthenticationToken;import lombok.extern.slf4j.Slf4j;import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;import org.apache.shiro.mgt.DefaultSubjectDAO;import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.realm.Realm;import org.apache.shiro.session.mgt.DefaultSessionManager;import org.apache.shiro.spring.web.ShiroFilterFactoryBean;import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;import java.util.*;/**
* Shiro 配置
* 禁用 Shiro Session 步骤:
* 1. SubjectContext 在创建的时候,需要关闭 session 的创建,这个由 DefaultWebSubjectFactory.createSubject 管理。
* 参考自定义类:ASubjectFactory.java
* 2. 禁用使用 Sessions 作为存储策略的实现,这个由 securityManager 的 subjectDao.sessionStorageEvaluator 管理
* 3. 禁用掉会话调度器,这个由 sessionManager 管理
*/@Slf4j(topic = "SYSTEM_LOG")@Configurationpublic class ShiroConfig { @Autowired
private UserRealm userRealm; /**
* Shiro 安全管理器
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); // 设置自定义的 SubjectFactory
manager.setSubjectFactory(subjectFactory()); // 设置自定义的 SessionManager
manager.setSessionManager(sessionManager()); // 禁用 Session
((DefaultSessionStorageEvaluator)((DefaultSubjectDAO)manager.getSubjectDAO()).getSessionStorageEvaluator())
.setSessionStorageEnabled(false); // 设置自定义的 Realm
manager.setRealms(getRealms()); return manager;
} /**
* 设置过滤规则
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager); //自定义拦截器 参考 ShiroLoginFilter.java
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
filtersMap.put("shiroLoginFilter", new ShiroLoginFilter());//登录验证拦截器
shiroFilterFactoryBean.setFilters(filtersMap); // 所有请求给这个拦截器处理
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
filterChainDefinitionMap.put("/**", "shiroLoginFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean;
} /**
* 自定义的 subjectFactory
* 禁用了 Session
* @return
*/
@Bean
public DefaultWebSubjectFactory subjectFactory(){
ASubjectFactory mySubjectFactory = new ASubjectFactory(); return mySubjectFactory;
} /**
* session管理器
* 禁用了 Session
* sessionManager通过sessionValidationSchedulerEnabled禁用掉会话调度器,
* @return
*/
@Bean
public DefaultSessionManager sessionManager(){
DefaultSessionManager sessionManager = new DefaultSessionManager();
sessionManager.setSessionValidationSchedulerEnabled(false); return sessionManager;
} /**
* 配置自定义的 Realm
* @return
*/
@Bean
public Collection<Realm> getRealms(){
Collection<Realm> realms = new ArrayList<>(); // 配置自定义 UserRealm
// 由于UserRealm里使用了自动注入,所以这里需要注入Realm而不是new新建
userRealm.setAuthenticationTokenClass(UserAuthenticationToken.class);
userRealm.setCredentialsMatcher(new PasswordCredentialsMatcher());//使用自定义的密码匹配器
realms.add(userRealm); return realms;
}
}ASubjectFactory.java 和ShiroConfig配套使用,用于禁用Session。
package com.spz.demo.security.shiro.config;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;import org.apache.shiro.subject.Subject;import org.apache.shiro.subject.SubjectContext;import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;/**
* 自定义的 SubjectFactory
* 禁用Session
* 对于无状态的TOKEN不创建session 这里都不使用session
*/public class ASubjectFactory extends DefaultWebSubjectFactory { @Override
public Subject createSubject(SubjectContext context) {
context.setSessionCreationEnabled(Boolean.FALSE); return super.createSubject(context);
}
}编写自定义Shiro拦截器
ShiroLoginFilter.java
Message类是包装返回给请求方的类,需要将Message实例转为json输出到Response输出流,参考:[SpringMVC] Web层返回值包装JSON
WebUtil.isPublicRequest()方法判断请求是否为公共请求
建议将不需要验证权限的请求设置一个前缀,比如/public/,这样,isPublicRequest方法就可以检查请求url里是否有/public,有则说明是公共请求,直接放行。所有请求(公共请求除外)都给* onAccessDenied*方法处理
在onAccessDenied方法里,通过检查请求url的方式来得知当前请求是什么类型的请求。
如果是登录请求,则直接放行,因为登录逻辑放在了controller层方法。
如果是其他请求,则需要验证登录和权限。检查用户是否具备权限
将请求url和permission表里的url进行匹配,如果存在匹配,则说明有权限。
package com.spz.demo.security.shiro.filter;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.spz.demo.security.bean.Message;import com.spz.demo.security.common.MessageCode;import com.spz.demo.security.common.RequestMappingConst;import com.spz.demo.security.common.WebConst;import com.spz.demo.security.entity.Role;import com.spz.demo.security.exception.custom.RoleException;import com.spz.demo.security.util.CommonUtil;import com.spz.demo.security.util.JwtUtil;import com.spz.demo.security.util.WebUtil;import com.spz.demo.security.vo.JwtToken;import lombok.extern.slf4j.Slf4j;import org.apache.shiro.web.filter.AccessControlFilter;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Lazy;import org.springframework.stereotype.Component;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;/**
* 重写shiro拦截器
* 所有请求由此拦截器拦截
*/@Slf4j(topic = "USER_LOG")@Componentpublic class ShiroLoginFilter extends AccessControlFilter { //由于项目启动时,Shiro加载比其他bean快,所以这里需要加入Lazy注解,在使用时再加载。否则会出现jwtUtil为null的情况
@Autowired
@Lazy
private JwtUtil jwtUtil; @Override
protected boolean isAccessAllowed(ServletRequest request,ServletResponse response, Object mappedValue) { // 判断请求是否是公共请求,通过请求的url判断
if(WebUtil.isPublicRequest((HttpServletRequest) request)){ return true;
} return false;// 拒绝,统一交给 onAccessDenied 处理
} @Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest)request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response; // ========== 判断是否是登录请求,是就放行,登录处理放在了controller层 ==========
if(WebUtil.isLoginRequest(httpServletRequest)){ return true;
} // ========== 其他请求,都需要验证 ==========
//验证是否登录(检查json token)
if(CommonUtil.isBlank(httpServletRequest.getHeader(WebConst.TOKEN))){ // 返回JSON给请求方
WebUtil.writeStringToResponse(httpServletResponse,JSON.toJSONString( new Message()
.setErrorMessage("[" + WebConst.TOKEN + "] 不能为空,请将token存入header")
)); return false;
}
String token = httpServletRequest.getHeader(WebConst.TOKEN);
JwtToken jwtToken; try {
jwtToken = jwtUtil.parseJwt(token);
}catch (RoleException re){//出现异常,说明验证失败
Message message = new Message(); if(re.getMessage().equals(RoleException.MSG_TOKEN_ERROR)){//token错误异常
message.setMessage(MessageCode.TOKEN_ERROR,RoleException.MSG_TOKEN_ERROR);
}else{//token过期异常
message.setMessage(MessageCode.TOKEN_OVERDUE,RoleException.MSG_TOKEN_OVERDUE);
}
WebUtil.writeStringToResponse((HttpServletResponse) response,JSON.toJSONString(message));//返回json
return false;
} if(jwtToken.getIsFlushed()){//需要刷新token
httpServletResponse.setHeader(WebConst.TOKEN,jwtToken.getToken());// 更新response
} // 检查用户是否具备权限
if(!jwtToken.hasUrl(((HttpServletRequest) request).getRequestURI())){
WebUtil.writeStringToResponse((HttpServletResponse) response,JSON.toJSONString( new Message()
.setPermissionDeniedMessage("没有权限")
)); return false;
}else{//登录验证通过
return true;
}
}
}
作者:萌璐琉璃
链接:https://www.jianshu.com/p/96a7b509706f
共同学习,写下你的评论
评论加载中...
作者其他优质文章