Shiro禁用Session,使用SSM+JWT+Shiro进行无状态RESTful API

本文源码:github

上一篇Shiro的demo:SpringMVC框架下使用shiro权限管理

最近一直在研究Shiro,因为项目里面要使用Shiro进行权限控制。由于项目不只是在Web端运行,还会在移动端App使用,想要使用JWT方式进行无状态的RESTful API交互,也就是登录后生成token并返回给前端,前端每次请求时都在请求头里面添加token,后端验证有效性。所以Shiro自带的Session要禁用掉,同时要重新写JWT的过滤器。

至于shiro的介绍,搬来官网介绍:https://shiro.apache.org/introduction.html

如上介绍,由于使用JWT需要禁用session,所以该demo只是使用了认证Authentication和授权Authorization两大块。

一开始直接照着网上的例子做demo,一直出错。所以放弃shiro写了个拦截器用来拦截,只是没法细粒度的控制权限,如下:

package filter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import util.JwtUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class ControllerInterception extends HandlerInterceptorAdapter {
    private Logger logger = LoggerFactory.getLogger(ControllerInterception.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String url = request.getServletPath();
        logger.info("url"+url);
        String token = request.getHeader("token");
        logger.info("开始验证token");
        if (JwtUtil.verifyToken(token)) {
            logger.info("验证token通过");
            String role = (String) JwtUtil.parseToken(token).get("role");
            logger.info("role"+role);
            if (url.indexOf(role) == 1){
                logger.info("匹配url通过");
                return true;
            }
        }
        logger.info("验证token失败");
        response.sendRedirect(request.getContextPath()+"/login");
        return false;

    }
}
<!--登录拦截器-->
<mvc:interceptors>
    <mvc:interceptor>
        <!--拦截器拦截的类型-->
        <mvc:mapping path="/**"/>
        <!--不拦截以下请求-->
        <mvc:exclude-mapping path="/login"/>
        <mvc:exclude-mapping path="/index"/>
        <bean id="controllerInterception" class="filter.ControllerInterception"/>
    </mvc:interceptor>
</mvc:interceptors>

但是不想放弃使用shiro,所以自己一直在研究到底禁用session后shiro如何将JWT传递并在哪里校验。最终整理出来后才发现是如此的简单。说一下,使用shiro+ssm+jwt的一些前期准备:

  • 禁用session
  • JWT实现工具,这个网上可以找到很多,这里就不贴代码了
  • 自定义realm,继承AuthorizingRealm,重写认证和授权两个方法
  • 自定义filter,继承AccessControlFilter,重写方法isAccessAllowed,onAccessDenied

一、首先,禁用shiro的session,先添加shiro的依赖到pom

<!--Shiro-->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-web</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-ehcache</artifactId>
    <version>1.4.0</version>
</dependency>

按照网上的说法要写一个类禁用(http://jinnianshilongnian.iteye.com/blog/2041909

  1. public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {  
  2.     public Subject createSubject(SubjectContext context) {  
  3.         //不创建session  
  4.         context.setSessionCreationEnabled(false);  
  5.         return super.createSubject(context);  
  6.     }  
  7. }   

一开始就是按照这种写法,但是会报错,所以上面的类就省略了,但是不影响使用JWT。我是按下面的实现直接写在spring的xml文件中的,并没有实现上面的类:

<!--Shiro-->
<!-- Realm实现 -->
<bean id="statelessRealm" class="shiro.MyStatelessRealm">
    <property name="cachingEnabled" value="false"/>
</bean>
<!-- 禁用掉会话调度器 -->
<bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
    <property name="sessionValidationSchedulerEnabled" value="false"/>
</bean>
<!--解决报错,组装默认的subjectDAO-->
<bean id="subjectDAO" class="org.apache.shiro.mgt.DefaultSubjectDAO">
    <property name="sessionStorageEvaluator" ref="sessionStorageEvaluator"/>
</bean>
<bean id="sessionStorageEvaluator" class="org.apache.shiro.mgt.DefaultSessionStorageEvaluator">
    <property name="sessionStorageEnabled" value="false"/>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    <property name="realm" ref="statelessRealm"/>
    <property name="subjectDAO" ref="subjectDAO"/>
    <property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
    <property name="arguments" ref="securityManager"/>
</bean>

二、其次:重写filter,看里面的注释,应该可以看懂:

package shiro;

import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import util.JwtUtil;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class MyStatelessShiroFilter extends AccessControlFilter {
    private Logger logger = LoggerFactory.getLogger(MyStatelessShiroFilter.class);
    /**
     *返回false
     * @param servletRequest
     * @param servletResponse
     * @param o
     * @return 返回结果是false的时候才会执行下面的onAccessDenied方法
     * @throws Exception
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        logger.info("is access allowed");
        return false;
    }

    /**
     * 从请求头获取token并验证,验证通过后交给realm进行登录
     * @param servletRequest
     * @param servletResponse
     * @return 返回结果为true表明登录通过
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        logger.info("on access denied");
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if (JwtUtil.verifyToken(jwt)) {
            UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(jwt, jwt);
            try {
                //委托realm进行登录认证
                getSubject(servletRequest, servletResponse).login(usernamePasswordToken);
                return true;
            }catch (Exception e) {
                return false;
            }
        }
        redirectToLogin(servletRequest,servletResponse);
        return false;
    }

    /**
     * 重定向到登录页
     * @param request
     * @param response
     * @throws IOException
     */
    @Override
    protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
        logger.info("redirectToLogin");
        WebUtils.issueRedirect(request, response, "/login");
    }
}

这里面使用依然是shiro自带的token实现UsernamePasswordToken,自定义的token用来用去都用糊涂了,还是自带的好用一些。

这个时候就加入配置文件即可,statelessAuth就是自己的filter:

<bean id="myStatelessFilter" class="shiro.MyStatelessAuthcFilter"/>
<!-- Shiro的Web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
    <property name="filters">
        <util:map>
            <entry key="statelessAuth" value-ref="myStatelessFilter"/>
        </util:map>
    </property>
    <property name="loginUrl" value="/shirologin"/>
    <property name="unauthorizedUrl" value="/unauthorized"/>
    <property name="filterChainDefinitions">
        <value>
            /=anon
            /shirologin=anon
            /unauthorized=anon
            /user/**=statelessAuth,roles[user]
            /admin/**=statelessAuth,roles[admin]
            /**=statelessAuth
        </value>
    </property>
</bean>

web.xml中也要配置filter,要和这里面的filter命名要一致:

<!-- The filter-name matches name of a 'shiroFilter' bean inside applicationContext.xml -->
<filter>
    <filter-name>shiroFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetFilterLifecycle</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>shiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

三、再次,就是自定义realm,实现认证和授权,这里的role我是写死的,省的查验数据库:

package shiro;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


public class MyStatelessRealm extends AuthorizingRealm {
    Logger logger = LoggerFactory.getLogger(MyStatelessRealm.class);

    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        logger.info("Realm处理登录");
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
        String token = usernamePasswordToken.getUsername();
        return new SimpleAuthenticationInfo(token, token, getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("Realm处理授权");
        String token = (String) principalCollection.getPrimaryPrincipal();
        logger.info("realm授权获取token:"+token);
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRole("user");
        return authorizationInfo;
    }

}

四、这时候就要写一些controller进行测试了,使用Json格式测试的:

package controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import util.JwtUtil;
import java.util.HashMap;
import java.util.Map;


@RestController
public class ShiroController {
    Logger logger = LoggerFactory.getLogger(ShiroController.class);

    @RequestMapping("shirologin")
    public String toLogin() {
        return "you must login firstly";
    }

    @RequestMapping(value = "shirologin",method = RequestMethod.POST)
    public String login(String username, String password) {
        logger.info("shiro登录");
        Map<String, Object> map = new HashMap<>();
        map.put("username",username);
        String token = new JwtUtil().createJwt(map);
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(token, token);
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(usernamePasswordToken);
        } catch (Exception e) {
            return "login fail ex:"+e.getMessage();
        }
        return "login success ! token is :"+token;
    }

    @RequestMapping("user")
    public String toUser(){
        logger.info("进入 user");
        return "welcome to user";
    }

    @RequestMapping("admin")
    public String toAdmin(){
        return "welcome to admin";
    }

    @RequestMapping("unauthorized")
    public String unAuth(){
        return "unauthorized";
    }
}

五、使用postman进行接口测试:

猜你喜欢

转载自blog.csdn.net/weixin_42058600/article/details/81837056
今日推荐