承接上文 Shiro實現session和無狀態token認證共存
項目在爲前後端分離部分接口時複用shiro鑑權,由於項目的token生成沒有符合服務器無關性,所以沒有采用了將sessionid賦值給token參數,從而實現api的shiro鑑權,這樣做更快捷。
建一個過濾器攔截api請求(認證失敗時 返回json)
重寫 onAccessDenied
認證失敗時 返回json格式的錯誤碼而不是跳轉
/**
* api驗證
* @author bbq
* @version 2020-01-02
*/
@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 過濾器
<bean id="statelessAuthenticationFilter" class="路徑.Stateless.StatelessAuthenticationFilter"></bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="filters">
<map>
<entry key="authc" value-ref="formAuthenticationFilter"/>
<entry key="apifilter" value-ref="apiAuthenticationFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions"/>
</property>
</bean>
<bean id="apiAuthenticationFilter" class="路徑.ApiAuthenticationFilter">
</bean>
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
${BasePath}/**/api/** = apifilter
</value>
</constructor-arg>
</bean>
重寫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
<bean class="com.thinkgem.jeesite.modules.sys.security.ApiMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="org.apache.shiro.authz.UnauthorizedException">error/403</prop>
<prop key="java.lang.Throwable">error/500</prop>
</props>
</property>
</bean>
注意:
看一下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中處理跨域 又或者統一把跨域丟給過濾器處理