Shiro+JWT 前后端分离方案

Shiro+JWT 前后端分离方案

理论的东西就不说了,网上一大堆教程。

因为我本身web应用做得比较多,所以本篇文章主要是结合SpringBoot来讲解。当然shiro也是支持standlon模式使用的(可以参考我的另一篇文章 自定义Realm)

使用SpringBoot,最优的依赖方案是shiro-spring-boot-starter

<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring-boot-starter</artifactId>
  <version>1.4.0</version>
</dependency>

SpringBoot集成Shiro的思路路径是 创建配置文件-->创建过滤器-->创建Realm 。(其实也不完全正确,这几个步骤得结合起来考虑)

首先,我们来创建配置文件

ShiroConfig

SecurityManager 是Shiro的核心组件,所以得首先创建它。

DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm);

注意:shiroRealm是我们通过Spring Autowired自动注入的,这里的shiroRealm也就是我们自定义的realm,后面会讲到–ShiroRealm

@Autowired
private ShiroRealm shiroRealm;

因为我们是前后端分离项目,所以shiro的Session功能我们是用不到的,得把它关闭。下面代码的作用就是关闭subject session。

DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();

DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);

securityManager.setSubjectDAO(subjectDAO);

官方文档中有这么一段话介绍 securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false

This will prevent Shiro from using a Subject’s session to store
that Subject’s state across requests/invocations/messages for all Subjects.
这将阻止Shiro使用Subject的会话存储Subject的请求/调用/消息中的状态。

Just be sure that you authenticate on every request
so Shiro will know who the Subject is for any given request/invocation/message.
一定要保证您对每个请求进行身份验证,以便于Shiro知道任何给定请求/调用/消息的subject是谁。

所以,前后端分离项目的关键是**每个请求都得交给shiro去验证身份,也就是说每个请求都会调用subject的login方法 **

继而每次请求都会调用自定义realm中的doGetAuthenticationInfo 方法。


创建SecurityManager 后,我们来创建过滤器Filter.

@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
   
  ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  shiroFilterFactoryBean.setSecurityManager(securityManager);

  // 拦截器
  Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
  filterChainDefinitionMap.put("/doc.html", "anon");
  filterChainDefinitionMap.put("/**/*.js", "anon");
  filterChainDefinitionMap.put("/**/*.css", "anon");
  filterChainDefinitionMap.put("/**/*.html", "anon");
  filterChainDefinitionMap.put("/**/*.svg", "anon");
  filterChainDefinitionMap.put("/**/*.pdf", "anon");
  filterChainDefinitionMap.put("/**/*.jpg", "anon");
  filterChainDefinitionMap.put("/**/*.png", "anon");
  filterChainDefinitionMap.put("/**/*.ico", "anon");

  // 添加自己的过滤器并且取名为jwt
  Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
  filterMap.put("jwt", new JwtFilter());
  shiroFilterFactoryBean.setFilters(filterMap);

  // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
  filterChainDefinitionMap.put("/**", "jwt");

  shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
  return shiroFilterFactoryBean;
}
  • anon–表示不进行拦截,是anonymous的缩写
  • JwtFilter是我们自定义的Filter,稍候我们会创建。

Shiro可以通过@RequiresPermissions(value = {"sys:admin:get"})@RequiresRoles 细粒度的控制资源访问权限,在Spring应用中,要想使用注解的方式控制权限。还得配置以下几个java bean。

@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
   
  return new LifecycleBeanPostProcessor();
}

/**
     * 下面的代码是添加注解支持
     * @return
     */
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
   
  DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
  defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);

  /*defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");*/
  return defaultAdvisorAutoProxyCreator;
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
   
  AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
  advisor.setSecurityManager(securityManager);
  return advisor;
}

ShiroConfig完整配置文件如下:

@Slf4j
@Configuration
public class ShiroConfig {
   

    @Autowired
    private ShiroRealm shiroRealm;
    /**
     *
     * @return
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager() {
   
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(shiroRealm);

        /*
         * 关闭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);

        return securityManager;
    }


    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
   
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        filterChainDefinitionMap.put("/doc.html", "anon");
        filterChainDefinitionMap.put("/**/*.js", "anon");
        filterChainDefinitionMap.put("/**/*.css", "anon");
        filterChainDefinitionMap.put("/**/*.html", "anon");
        filterChainDefinitionMap.put("/**/*.svg", "anon");
        filterChainDefinitionMap.put("/**/*.pdf", "anon");
        filterChainDefinitionMap.put("/**/*.jpg", "anon");
        filterChainDefinitionMap.put("/**/*.png", "anon");
        filterChainDefinitionMap.put("/**/*.ico", "anon");

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
        filterMap.put("jwt", new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);

        // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
        filterChainDefinitionMap.put("/**", "jwt");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }


    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
   
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 下面的代码是添加注解支持
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
   
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);

        /*defaultAdvisorAutoProxyCreator.setUsePrefix(true);
        defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");*/
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
   
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

JwtFilter

在配置ShiroConfig时,我们提到需要配置自定义过滤器。过滤器的作用就是拦截指定的请求,比如我们在上面设置的是filterChainDefinitionMap.put("/**", "jwt"); 拦截所有请求(这里的所有指的是流入到当前过滤器的所有请求,如果是图片、字体等请求,则不会进入,因为在此之前已经被拦截了)

我们来看具体的实现吧:JwtFilter继承了BasicHttpAuthenticationFilter 类,我们只要重写几个关键的方法即可。

public class JwtFilter extends BasicHttpAuthenticationFilter {
   
    /**
     * 执行登录认证
     *
     * @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失效,请重新登录", e);
        }
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
   
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("X-Access-Token");

        JwtToken jwtToken = new JwtToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        // 普通应用中获取subject方式 subject = SecurityUtils.getSubject();
        // 然后登录 subject.login(token)
        //login方法最后会回调到ShiroRealm.doGetAuthenticationInfo
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
   
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
   
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

  • isAccessAllowed:判断是否允许访问,内部调用executeLogin执行登录,executeLogin没有抛出异常,表示允许登录(相当于每次请求都会进行登录逻辑判断)
  • executeLogin: 从请求头中拿出token,调用subject的login方法就行登录验证(这里封装了JwtToken,[稍候讲解](### JwtToken))
  • preHandle: 跨越支持

JwtToken

JwtToken的实现很简单,但要注意两点:

  • 必须实现AuthenticationToken接口
  • 返回的Principal和Credentials是一样的,都是成员变量token

下面讲解自定义Realm时会提到这两点。

import org.apache
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值