环境
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
共同学习,写下你的评论
评论加载中...
作者其他优质文章