为了账号安全,请及时绑定邮箱和手机立即绑定

Shiro和SpringBoot简单集成

标签:
Spring

环境

  • SpringBoot 版本 1.5.15.RELEASE
    不建议使用2.x版本的Springboot,与1.x相比很多地方代码有所改动,很麻烦。

  • Shiro 版本 1.4.0

  • IntelliJ IDEA

  • jjwt 版本 0.9.0

  • lombok(可选)精简代码

思路

  1. 使用Jwt Token实现无状态登录
    平时用户登录后,服务器将会把用户信息存储到Session里,在用户数量很大的时候,服务器负担会很大。而使用token方式登录,服务器不存储用户信息,而是将其加密后生成token发送给请求方,请求方在请求需要权限的资源时,将token带上,服务器解析token即可知道登录用户的信息。

  2. 服务器自动刷新token
    token需要刷新。对于活跃的用户,服务器自动完成刷新token;对于长期不活跃的用户,服务器通过配置的 token有效期 来检查,如果时间超过有效期的两倍,则认为该用户需要重新登录。

  3. 登录流程

  • 用户通过账号密码登录
    用户登录成功后,服务器将用户信息等集合起来做成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


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消