承接上文 Shiro实现session和无状态token认证共存
项目在为前后端分离部分接口时复用shiro鉴权,由于项目的token生成没有符合服务器无关性,所以没有采用了将sessionid赋值给token参数,从而实现api的shiro鉴权,这样做更快捷。
重写 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 过滤器
> >
>
>
>
>
>
>
>
>
>
>
>
>
-arg>
>
${BasePath}/**/api/** = apifilter
>
-arg>
>
重写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;
}
}
由于项目之前参数叫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中处理跨域 又或者统一把跨域丢给过滤器处理