shiro复用session实现前后端分离鉴权

承接上文 Shiro实现session和无状态token认证共存
项目在为前后端分离部分接口时复用shiro鉴权,由于项目的token生成没有符合服务器无关性,所以没有采用了将sessionid赋值给token参数,从而实现api的shiro鉴权,这样做更快捷。

建一个过滤器拦截api请求(认证失败时 返回json)

重写 onAccessDenied
认证失败时 返回json格式的错误码而不是跳转

/**
 * api验证
 * @author bbq
 */
@Service
public class ApiAuthenticationFilter extends org.apache.shiro.web.filter.authc.FormAuthenticationFilter {
	/**
		这里为自己的一套重写
 	*/

	@Override
	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
		if (this.isLoginRequest(request, response)) {
			if (this.isLoginSubmission(request, response)) {
				if (log.isTraceEnabled()) {
					log.trace("Login submission detected.  Attempting to execute login.");
				}

				return this.executeLogin(request, response);
			} else {
				if (log.isTraceEnabled()) {
					log.trace("Login page view.");
				}

				return true;
			}
		} else {
			if (log.isTraceEnabled()) {
				log.trace("Attempting to access a path which requires authentication.  Forwarding to the Authentication url [" + this.getLoginUrl() + "]");
			}
			ResultDTO retDto = null;
			ErrorType et = ErrorType.getOperationType("ERROR_INT_TOKEN_INVALID");
			retDto = new ResultDTO(et.getResourceKey(), et.getType(), null);
			response.getWriter().write(GsonUtils.toJson(retDto));
			return false;
		}
	}

}


shiro.xml
拦截 api 接口 用 ApiAuthenticationFilter 过滤器

>>

>
	>
	>
           >
               -ref="formAuthenticationFilter"/>
				-ref="apiAuthenticationFilter"/>
           >
       >
	>
		>
	>
>

>
>

>
	-arg>
		>
			${BasePath}/**/api/** = apifilter
		>
	-arg>
>

重写SimpleMappingExceptionResolver 授权失败时 返回json格式

重写springmvc的SimpleMappingExceptionResolver 授权失败时不是跳转视图 而是返回json


/**
 * @author bbq
 * @version 2020-01-02
 */
package com.thinkgem.jeesite.modules.sys.security;

import com.thinkgem.jeesite.common.bean.ResultDTO;
import com.thinkgem.jeesite.common.enums.ErrorType;
import com.thinkgem.jeesite.common.utils.StringUtils;
import com.thinkgem.jeesite.common.utils.baiduface.GsonUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ApiMappingExceptionResolver extends SimpleMappingExceptionResolver {
    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        String token = request.getParameter("token");
        if(StringUtils.isNotBlank(token)) {
            ResultDTO retDto = null;
            if(ex instanceof org.apache.shiro.authz.UnauthorizedException){
                retDto = new ResultDTO("403", "操作权限不足", null);
            } else {
                retDto = new ResultDTO("500", "系统内部错误", null);
            }
            try {
                response.getWriter().write(GsonUtils.toJson(retDto));
            } catch (IOException e) {
                e.printStackTrace();
            }
            return new ModelAndView();
        }
        return super.doResolveException(request, response, handler, ex);
    }

springmvc配置如下 如果不重写的话默认为跳转视图
spring-mvc.xml

	>
	>
		>
			>error/403>
			>error/500>
		>
		>
>

注意:
看一下4.0.8版本的 springmvc 的
org.springframework.web.servlet.DispatcherServlet
源码
上面返回为空的ModelAndView对象才会return null;
网上的教程都是发送完json 后 return null
这样会被捕获异常 导致respon json失败

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    ModelAndView exMv = null;
    Iterator var6 = this.handlerExceptionResolvers.iterator();

    while(var6.hasNext()) {
        HandlerExceptionResolver handlerExceptionResolver = (HandlerExceptionResolver)var6.next();
        exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
        if (exMv != null) {
            break;
        }
    }

    if (exMv != null) {
        if (exMv.isEmpty()) {
            return null;
        } else {
            if (!exMv.hasView()) {
                exMv.setViewName(this.getDefaultViewName(request));
            }

            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Handler execution resulted in exception - forwarding to resolved error view: " + exMv, ex);
            }

            WebUtils.exposeErrorRequestAttributes(request, ex, this.getServletName());
            return exMv;
        }
    } else {
        throw ex;
    }
}

重写DefaultWebSessionManager通过request获取sessionid

由于项目之前参数叫token,不影响旧api,只要登陆接口从生产token改造成获取sessionid赋值给token就行。

/**
 * 自定义WEB会话管理类
 */
public class SessionManager extends org.apache.shiro.web.session.mgt.DefaultWebSessionManager {

	public SessionManager() {
		super();
	}
	
	@Override
	protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
		// 如果参数中包含“__sid”参数,则使用此sid会话。 例如:http://localhost/project?__sid=xxx&__cookie=true
		String sid = request.getParameter("token");
		if (StringUtils.isNotBlank(sid)) {
			// 是否将sid保存到cookie,浏览器模式下使用此参数。
			if (WebUtils.isTrue(request, "__cookie")){
		        HttpServletRequest rq = (HttpServletRequest)request;
		        HttpServletResponse rs = (HttpServletResponse)response;
				Cookie template = getSessionIdCookie();
		        Cookie cookie = new SimpleCookie(template);
				cookie.setValue(sid); cookie.saveTo(rq, rs);
			}
			// 设置当前session状态
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
                    ShiroHttpServletRequest.URL_SESSION_ID_SOURCE); // session来源与url
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sid);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
        	return sid;
		}else{
			return super.getSessionId(request, response);
		}
	}
	
}

//重新改造你的api登陆和登出接口即可

最后

session与jwt的不同:session认证是保险箱在服务器,密码在用户手中,用户把密码送到服务器解开自己的保险箱,而jwt则是保险箱放在用户手中,服务器什么都不放,当用户把保险箱送来,服务器摸一摸保险箱,敲打敲打,认为保险箱是自己家生产的就打开它。
这样当服务器开分号时,采用session方式就只能帮用户解锁在自己分号的保险箱,用户如果让a分号打开存在b分号的保险箱,就得顺丰快递从a送到b送过来。而jwt方式每一家分号都能打开任意用户的保险箱。

由于项目session加入了redis缓存,也可作为分布式使用(开启顺丰服务),而 token的生成只是加入一段随机串加密保存在redis中(分号老板认不出自家的保险箱,而是把保险箱的生产编码记在账本上,每家分号只记录自己家卖出去的保险箱),这样就离不开与服务器进行交互,所以这里的token本质和sessionid一样,并没有实现服务器无关性,所以最后没有采用多realm认证,而是将sessionid赋予token参数,api请求也复用session认证的方式。

如果你想增加一套jwt的shiro认证
看另一篇文
Shiro实现session和无状态token认证共存

注意:项目之前处理跨域问题是在后端拦截器里 而shiro是基于过滤器 优先级高于拦截器 所以改造后得在respon json中处理跨域 又或者统一把跨域丢给过滤器处理

你可能感兴趣的:(shiro)