Shiro学习笔记---入门Demo(JWT令牌)

前言

        上一篇博客采用了搭建Demo的方式说明了如何使用Shiro共享session实现分布式架构。而本篇博客将介绍Shiro与JWT结合,实现前后端分离。本Demo仍然力求简洁清晰,因此在工程代码中有与上一篇博客代码重合部分将被省略,如有不清楚的地方请先看第一篇关于Shiro基础博客然后再回来继续阅读。

业务设计

JWT : JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。与Shiro结合主要关注的是无状态服务,要想更好的理解什么是无状态服务就必须先理解什么是有状态服务,有状态服务服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。

其缺点是:

  • 服务端保存大量数据,增加服务端压力
  • 服务端保存用户状态,无法进行水平扩展
  • 客户端请求依赖服务端,多次请求必须访问同一台服务器

而无状态服务,在服务端不保存任何客户端请求者信息,也就是在服务端去掉了session,客户端的每次请求必须具备自描述信息,也就是携带Token通过它的信息识别客户端身份。

其优点是:

  • 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务;
  • 服务端的集群和状态对客户端透明;
  • 服务端可以任意的迁移和伸缩;
  • 减小服务端存储压力

更新工程

在原工程基础上增加JWT支持,只需要将JWT集成进工程然后增加JWT访问过滤器并修改Shiro配置类即可,通过这样简单三步即可完成。

(1)集成JWT

  pom.xml

        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

 

 (2)增加访问过滤器

   JwtFilter

package microapps.com.cn.shirodemo.shiro;

import microapps.com.cn.shirodemo.db.UserService;
import microapps.com.cn.shirodemo.domain.User;
import microapps.com.cn.shirodemo.common.JwtUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

public class JwtFilter extends BasicHttpAuthenticationFilter {

    @Autowired
    private UserService UserService;

    /**
     * 前置处理(处理跨域请求)
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 后置处理(处理跨域请求)
     */
    @Override
    protected void postHandle(ServletRequest request, ServletResponse response) {
        // 添加跨域支持
        this.fillCorsHeader(WebUtils.toHttp(request), WebUtils.toHttp(response));
    }

    /**
     * 对所有请求进行过滤(除登陆)
     * 1.登陆时不走过滤器;
     * 2.访问权限时调用过滤器,然后再走realm权限验证方法;
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        System.out.println("----------->isAccessAllowed");
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        // s1. 判断header中是否存在jwtToken
        if (this.isHeaderToken(request)){
            request.setAttribute("errorMsg","无效请求,没有携带token");
            return false;
        }
        String jwtToken = httpServletRequest.getHeader(JwtUtils.AUTH_HEADER);
        // s2. 判断token是否过期
        if(JwtUtils.isTokenExpired(jwtToken)) {
            request.setAttribute("errorMsg","无效请求,携带过期token");
            return false;
        }
        // s3. 判断Token是否有效
        if(!JwtUtils.verify(jwtToken,JwtUtils.SECRET)){
            request.setAttribute("errorMsg","无效请求,携带无效token");
            return false;
        }

        try {
            // 检测Header里的 JWT token内容是否正确,尝试使用 token进行登录
         return executeLogin(request, response);
        } catch (Exception e) {
            System.out.println("jwt 登陆错误");
        }
        return true;
    }


    /**
     * 执行登陆
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        UsernamePasswordToken token = createToken(request,response);
        try {
            Subject subject = getSubject(request, response);
            subject.login(token); // 交给 Shiro 去进行登录验证
            return onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException e) {
            return onLoginFailure(token, e, request, response);
        }
    }

    /**
     * isAccessAllowed()方法返回false,会进入该方法,表示拒绝访问
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
        httpResponse.setCharacterEncoding("UTF-8");
        httpResponse.setContentType("application/json;charset=UTF-8");
        httpResponse.setStatus(HttpStatus.OK.value());
        Object errorMsg = servletRequest.getAttribute("errorMsg");
        PrintWriter writer = httpResponse.getWriter();
        writer.print("{\"code\": 401, \"msg\": \""+errorMsg.toString()+"\"}");
        fillCorsHeader(WebUtils.toHttp(servletRequest), httpResponse);
        return false;
    }

    /**
     * 检测Header中是否包含 JWT token 字段
     */
    private boolean isHeaderToken(ServletRequest request) {
        return ((HttpServletRequest) request).getHeader(JwtUtils.AUTH_HEADER) == null;
    }

    /**
     * 从 Header 里提取 JWT token
     */
    @Override
    protected UsernamePasswordToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String authorization = httpServletRequest.getHeader(JwtUtils.AUTH_HEADER);
        String username = JwtUtils.getUsername(authorization);
        User user = UserService.getUserByName(username); // 获取用户密码
        UsernamePasswordToken token = new UsernamePasswordToken(username,user.getPassword());
        return token;
    }


    /**
     * 添加跨域支持
     */
    protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
        httpServletResponse.setHeader("Access-Control-Allow-Headers",
                httpServletRequest.getHeader("Access-Control-Request-Headers"));
    }

}

 (3)修改Shiro配置类

   ShiroConfig

package microapps.com.cn.shirodemo.config;

import microapps.com.cn.shirodemo.shiro.JwtFilter;
import microapps.com.cn.shirodemo.shiro.LoginRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Description: Shiro配置类
 * @Author: liuhe
 * @Date: 2020/10/10
 */
@Configuration
public class ShiroConfig {

   ......


    /**
     * Filter工厂,设置对应的过滤条件和跳转条件
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setLoginUrl("/login"); //登录
        shiroFilterFactoryBean.setSuccessUrl("/index"); //首页

        // 添加 jwt 专用过滤器,拦截除 /login 和 /logout 外的请求
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("jwtFilter", jwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);


        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 配置不需要权限过滤的路径
        filterChainDefinitionMap.put("/login","anon");
        filterChainDefinitionMap.put("/refreshToken","anon");
        filterChainDefinitionMap.put("/logout","logout");
        // 配置需要权限过滤的路径
        filterChainDefinitionMap.put("/**", "jwtFilter,authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

   ......

    /**
     * 不向 Spring容器中注册 JwtFilter Bean,
     * 防止 Spring 将 JwtFilter 注册为全局过滤器
     */
    @Bean
    public FilterRegistrationBean<Filter> registration(JwtFilter filter) {
        FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<Filter>(filter);
        registration.setEnabled(false);
        return registration;
    }

    @Bean
    public JwtFilter jwtFilter() {
        return new JwtFilter();
    }

    /**
     * 禁用session, 不保存用户登录状态。保证每次请求都重新认证
     */
    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator() {
        DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }

}

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值