無狀態認證攔截器css
import com.hjzgg.stateless.shiroSimpleWeb.Constants; import com.hjzgg.stateless.shiroSimpleWeb.realm.StatelessToken; import org.apache.shiro.web.filter.AccessControlFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; /** * <p>Version: 1.0 */ public class StatelessAuthcFilter extends AccessControlFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { //一、客戶端生成的消息摘要 String clientDigest = request.getParameter(Constants.PARAM_DIGEST); //二、客戶端傳入的用戶身份 String username = request.getParameter(Constants.PARAM_USERNAME); //三、客戶端請求的參數列表 Map<String, String[]> params = new HashMap<String, String[]>(request.getParameterMap()); params.remove(Constants.PARAM_DIGEST); //四、生成無狀態Token StatelessToken token = new StatelessToken(username, params, clientDigest); try { //五、委託給Realm進行登陸 getSubject(request, response).login(token); } catch (Exception e) { e.printStackTrace(); onLoginFail(response); //六、登陸失敗 return false; } return true; } //登陸失敗時默認返回401狀態碼 private void onLoginFail(ServletResponse response) throws IOException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.getWriter().write("login error"); } }
Subject工廠java
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.SubjectContext; import org.apache.shiro.web.mgt.DefaultWebSubjectFactory; /** * <p>Version: 1.0 */ public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory { @Override public Subject createSubject(SubjectContext context) { //不建立session context.setSessionCreationEnabled(false); return super.createSubject(context); } }
注意,這裏禁用了sessiongit
無狀態Realmgithub
import com.hjzgg.stateless.shiroSimpleWeb.codec.HmacSHA256Utils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; /** * <p>Version: 1.0 */ public class StatelessRealm extends AuthorizingRealm { @Override public boolean supports(AuthenticationToken token) { //僅支持StatelessToken類型的Token return token instanceof StatelessToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //根據用戶名查找角色,請根據需求實現 String username = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addRole("admin"); return authorizationInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { StatelessToken statelessToken = (StatelessToken) token; String username = statelessToken.getUsername(); String key = getKey(username);//根據用戶名獲取密鑰(和客戶端的同樣) //在服務器端生成客戶端參數消息摘要 String serverDigest = HmacSHA256Utils.digest(key, statelessToken.getParams()); System.out.println(statelessToken.getClientDigest()); System.out.println(serverDigest); //而後進行客戶端消息摘要和服務器端消息摘要的匹配 return new SimpleAuthenticationInfo( username, serverDigest, getName()); } private String getKey(String username) {//獲得密鑰,此處硬編碼一個 if("admin".equals(username)) { return "dadadswdewq2ewdwqdwadsadasd"; } return null; } }
無狀態Tokenweb
import org.apache.shiro.authc.AuthenticationToken; import org.springframework.beans.*; import org.springframework.validation.DataBinder; import java.util.HashMap; import java.util.Map; /** * <p>Version: 1.0 */ public class StatelessToken implements AuthenticationToken { private String username; private Map<String, ?> params; private String clientDigest; public StatelessToken(String username, Map<String, ?> params, String clientDigest) { this.username = username; this.params = params; this.clientDigest = clientDigest; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public Map<String, ?> getParams() { return params; } public void setParams( Map<String, ?> params) { this.params = params; } public String getClientDigest() { return clientDigest; } public void setClientDigest(String clientDigest) { this.clientDigest = clientDigest; } @Override public Object getPrincipal() { return username; } @Override public Object getCredentials() { return clientDigest; } public static void main(String[] args) { } public static void test1() { StatelessToken token = new StatelessToken(null, null, null); BeanWrapperImpl beanWrapper = new BeanWrapperImpl(token); beanWrapper.setPropertyValue(new PropertyValue("username", "hjzgg")); System.out.println(token.getUsername()); } public static void test2() { StatelessToken token = new StatelessToken(null, null, null); DataBinder dataBinder = new DataBinder(token); Map<String, Object> params = new HashMap<>(); params.put("username", "hjzgg"); PropertyValues propertyValues = new MutablePropertyValues(params); dataBinder.bind(propertyValues); System.out.println(token.getUsername()); } }
shiro配置文件ajax
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- Realm實現 --> <bean id="statelessRealm" class="com.hjzgg.stateless.shiroSimpleWeb.realm.StatelessRealm"> <property name="cachingEnabled" value="false"/> </bean> <!-- Subject工廠 --> <bean id="subjectFactory" class="com.hjzgg.stateless.shiroSimpleWeb.mgt.StatelessDefaultSubjectFactory"/> <!-- 會話管理器 --> <bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager"> <property name="sessionValidationSchedulerEnabled" value="false"/> </bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="statelessRealm"/> <property name="subjectDAO.sessionStorageEvaluator.sessionStorageEnabled" value="false"/> <property name="subjectFactory" ref="subjectFactory"/> <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> <bean id="statelessAuthcFilter" class="com.hjzgg.stateless.shiroSimpleWeb.filter.StatelessAuthcFilter"/> <!-- Shiro的Web過濾器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="filters"> <util:map> <entry key="statelessAuthc" value-ref="statelessAuthcFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /**=statelessAuthc </value> </property> </bean> <!-- Shiro生命週期處理器--> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> </beans>
這裏禁用了回話調度器的session存儲redis
web.xml配置算法
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0" metadata-complete="false"> <display-name>shiro-example-chapter20</display-name> <!-- Spring配置文件開始 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:spring-config-shiro.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- Spring配置文件結束 --> <!-- shiro 安全過濾器 --> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <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> <dispatcher>REQUEST</dispatcher> </filter-mapping> <servlet> <servlet-name>spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>spring</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
token生成工具類spring
import org.apache.commons.codec.binary.Hex; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.List; import java.util.Map; /** * <p>Version: 1.0 */ public class HmacSHA256Utils { public static String digest(String key, String content) { try { Mac mac = Mac.getInstance("HmacSHA256"); byte[] secretByte = key.getBytes("utf-8"); byte[] dataBytes = content.getBytes("utf-8"); SecretKey secret = new SecretKeySpec(secretByte, "HMACSHA256"); mac.init(secret); byte[] doFinal = mac.doFinal(dataBytes); byte[] hexB = new Hex().encode(doFinal); return new String(hexB, "utf-8"); } catch (Exception e) { throw new RuntimeException(e); } } public static String digest(String key, Map<String, ?> map) { StringBuilder s = new StringBuilder(); for(Object values : map.values()) { if(values instanceof String[]) { for(String value : (String[])values) { s.append(value); } } else if(values instanceof List) { for(String value : (List<String>)values) { s.append(value); } } else { s.append(values); } } return digest(key, s.toString()); } }
簡單測試一下apache
import com.alibaba.fastjson.JSONObject; import com.hjzgg.stateless.shiroSimpleWeb.codec.HmacSHA256Utils; import com.hjzgg.stateless.shiroSimpleWeb.utils.RestTemplateUtils; import org.junit.Test; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.util.UriComponentsBuilder; /** * <p>Version: 1.0 */ public class ClientTest { private static final String WEB_URL = "http://localhost:8080/shiro/hello"; @Test public void testServiceHelloSuccess() { String username = "admin"; String param11 = "param11"; String param12 = "param12"; String param2 = "param2"; String key = "dadadswdewq2ewdwqdwadsadasd"; JSONObject params = new JSONObject(); params.put(Constants.PARAM_USERNAME, username); params.put("param1", param11); params.put("param1", param12); params.put("param2", param2); params.put(Constants.PARAM_DIGEST, HmacSHA256Utils.digest(key, params)); String result = RestTemplateUtils.get(WEB_URL, params); System.out.println(result); } @Test public void testServiceHelloFail() { String username = "admin"; String param11 = "param11"; String param12 = "param12"; String param2 = "param2"; String key = "dadadswdewq2ewdwqdwadsadasd"; MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>(); params.add(Constants.PARAM_USERNAME, username); params.add("param1", param11); params.add("param1", param12); params.add("param2", param2); params.add(Constants.PARAM_DIGEST, HmacSHA256Utils.digest(key, params)); params.set("param2", param2 + "1"); String url = UriComponentsBuilder .fromHttpUrl("http://localhost:8080/hello") .queryParams(params).build().toUriString(); } }
補充Spring中多重屬性賦值處理
以上參考 開濤老師的博文!
*加入session,放入redis中(user_name做爲key值,token做爲hash值,當前登陸時間做爲value值)
*用戶登陸互斥操做:若是互斥,清除redis中該用戶對應的狀態,從新寫入新的狀態;若是不互斥,寫入新的狀態,刷新key值,並檢測該用戶其餘的狀態是否已經超時(根據key值獲取到全部的 key和hashKey的組合,判斷value[登入時間]+timeout[超時時間] >= curtime[當前時間]),若是超時則清除狀態。
*使用esapi進行token的生成
*認證信息,若是是web端則從cookie中獲取,ajax從header中獲取;若是是移動端也是從header中獲取
session manager邏輯
import com.hjzgg.stateless.auth.token.ITokenProcessor; import com.hjzgg.stateless.auth.token.TokenFactory; import com.hjzgg.stateless.auth.token.TokenGenerator; import com.hjzgg.stateless.common.cache.RedisCacheTemplate; import com.hjzgg.stateless.common.esapi.EncryptException; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class ShiroSessionManager { @Autowired private RedisCacheTemplate redisCacheTemplate; @Value("${sessionMutex}") private boolean sessionMutex = false; public static final String TOKEN_SEED = "token_seed"; public static final String DEFAULT_CHARSET = "UTF-8"; private final Logger logger = LoggerFactory.getLogger(getClass()); private static String localSeedValue = null; /** * 得到當前系統的 token seed */ public String findSeed() throws EncryptException { if(localSeedValue != null){ return localSeedValue; } else { String seed = getSeedValue(TOKEN_SEED); if (StringUtils.isBlank(seed)) { seed = TokenGenerator.genSeed(); localSeedValue = seed; redisCacheTemplate.put(TOKEN_SEED, seed); } return seed; } } public String getSeedValue(String key) { return (String) redisCacheTemplate.get(key); } /** * 刪除session緩存 * * @param sid mock的sessionid */ public void removeSessionCache(String sid) { redisCacheTemplate.delete(sid); } private int getTimeout(String sid){ return TokenFactory.getTokenInfo(sid).getIntegerExpr(); } private String getCurrentTimeSeconds() { return String.valueOf(System.currentTimeMillis()/1000); } public void registOnlineSession(final String userName, final String token, final ITokenProcessor processor) { final String key = userName; logger.debug("token processor id is {}, key is {}, sessionMutex is {}!" , processor.getId(), key, sessionMutex); // 是否互斥,若是是,則踢掉全部當前用戶的session,從新建立,此變量未來從配置文件讀取 if(sessionMutex){ deleteUserSession(key); } else { // 清理此用戶過時的session,過時的常爲異常或者直接關閉瀏覽器,沒有走正常註銷的key clearOnlineSession(key); } redisCacheTemplate.hPut(userName, token, getCurrentTimeSeconds()); int timeout = getTimeout(token); if (timeout > 0) { redisCacheTemplate.expire(token, timeout); } } private void clearOnlineSession(final String key) { redisCacheTemplate.hKeys(key).forEach((obj) -> { String hashKey = (String) obj; int timeout = getTimeout(hashKey); if (timeout > 0) { int oldTimeSecondsValue = Integer.valueOf((String) redisCacheTemplate.hGet(key, hashKey)); int curTimeSecondsValue = (int) (System.currentTimeMillis()/1000); //若是 key-hashKey 對應的時間+過時時間 小於 當前時間,則剔除 if(curTimeSecondsValue - (oldTimeSecondsValue+timeout) > 0) { redisCacheTemplate.hDel(key, hashKey); } } }); } public boolean validateOnlineSession(final String key, final String hashKey) { int timeout = getTimeout(hashKey); if (timeout > 0) { String oldTimeSecondsValue = (String) redisCacheTemplate.hGet(key, hashKey); if (StringUtils.isEmpty(oldTimeSecondsValue)) { return false; } else { int curTimeSecondsValue = (int) (System.currentTimeMillis()/1000); if(Integer.valueOf(oldTimeSecondsValue)+timeout >= curTimeSecondsValue) { //刷新 key redisCacheTemplate.hPut(key, hashKey, getCurrentTimeSeconds()); redisCacheTemplate.expire(key, timeout); return true; } else { redisCacheTemplate.hDel(key, hashKey); return false; } } } else { return redisCacheTemplate.hGet(key, hashKey) != null; } } // 註銷用戶時候須要調用 public void delOnlineSession(final String key, final String hashKey){ redisCacheTemplate.hDel(key, hashKey); } // 禁用或者刪除用戶時候調用 public void deleteUserSession(final String key){ redisCacheTemplate.delete(key); } }
無狀態認證過濾器
package com.hjzgg.stateless.auth.shiro; import com.alibaba.fastjson.JSONObject; import com.hjzgg.stateless.auth.token.ITokenProcessor; import com.hjzgg.stateless.auth.token.TokenFactory; import com.hjzgg.stateless.auth.token.TokenParameter; import com.hjzgg.stateless.common.constants.AuthConstants; import com.hjzgg.stateless.common.utils.CookieUtil; import com.hjzgg.stateless.common.utils.InvocationInfoProxy; import com.hjzgg.stateless.common.utils.MapToStringUtil; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.util.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.net.URL; import java.util.*; public class StatelessAuthcFilter extends AccessControlFilter { private static final Logger log = LoggerFactory.getLogger(StatelessAuthcFilter.class); public static final int HTTP_STATUS_AUTH = 306; @Value("${filterExclude}") private String exeludeStr; @Autowired private TokenFactory tokenFactory; private String[] esc = new String[] { "/logout","/login","/formLogin",".jpg",".png",".gif",".css",".js",".jpeg" }; private List<String> excludCongtextKeys = new ArrayList<>(); public void setTokenFactory(TokenFactory tokenFactory) { this.tokenFactory = tokenFactory; } public void setEsc(String[] esc) { this.esc = esc; } public void setExcludCongtextKeys(List<String> excludCongtextKeys) { this.excludCongtextKeys = excludCongtextKeys; } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { boolean isAjax = isAjax(request); // 一、客戶端發送來的摘要 HttpServletRequest hReq = (HttpServletRequest) request; HttpServletRequest httpRequest = hReq; Cookie[] cookies = httpRequest.getCookies(); String authority = httpRequest.getHeader("Authority"); //若是header中包含,則以header爲主,不然,以cookie爲主 if(StringUtils.isNotBlank(authority)){ Set<Cookie> cookieSet = new HashSet<Cookie>(); String[] ac = authority.split(";"); for(String s : ac){ String[] cookieArr = s.split("="); String key = StringUtils.trim(cookieArr[0]); String value = StringUtils.trim(cookieArr[1]); Cookie cookie = new Cookie(key, value); cookieSet.add(cookie); } cookies = cookieSet.toArray(new Cookie[]{}); } String tokenStr = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_TOKEN); String cookieUserName = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_USERNAME); String loginTs = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_LOGINTS); // 二、客戶端傳入的用戶身份 String userName = request.getParameter(AuthConstants.PARAM_USERNAME); if (userName == null && StringUtils.isNotBlank(cookieUserName)) { userName = cookieUserName; } boolean needCheck = !include(hReq); if (needCheck) { if (StringUtils.isEmpty(tokenStr) || StringUtils.isEmpty(userName)) { if (isAjax) { onAjaxAuthFail(request, response); } else { onLoginFail(request, response); } return false; } // 三、客戶端請求的參數列表 Map<String, String[]> params = new HashMap<String, String[]>(request.getParameterMap()); ITokenProcessor tokenProcessor = tokenFactory.getTokenProcessor(tokenStr); TokenParameter tp = tokenProcessor.getTokenParameterFromCookie(cookies); // 四、生成無狀態Token StatelessToken token = new StatelessToken(userName, tokenProcessor, tp, params, new String(tokenStr)); try { // 五、委託給Realm進行登陸 getSubject(request, response).login(token); // 這個地方應該驗證上下文信息中的正確性 // 設置上下文變量 InvocationInfoProxy.setUserName(userName); InvocationInfoProxy.setLoginTs(loginTs); InvocationInfoProxy.setToken(tokenStr); //設置上下文攜帶的額外屬性 initExtendParams(cookies); initMDC(); afterValidate(hReq); } catch (Exception e) { log.error(e.getMessage(), e); if (isAjax && e instanceof AuthenticationException) { onAjaxAuthFail(request, response); // 六、驗證失敗,返回ajax調用方信息 return false; } else { onLoginFail(request, response); // 六、登陸失敗,跳轉到登陸頁 return false; } } return true; } else { return true; } } private boolean isAjax(ServletRequest request) { boolean isAjax = false; if (request instanceof HttpServletRequest) { HttpServletRequest rq = (HttpServletRequest) request; String requestType = rq.getHeader("X-Requested-With"); if (requestType != null && "XMLHttpRequest".equals(requestType)) { isAjax = true; } } return isAjax; } protected void onAjaxAuthFail(ServletRequest request, ServletResponse resp) throws IOException { HttpServletResponse response = (HttpServletResponse) resp; JSONObject json = new JSONObject(); json.put("msg", "auth check error!"); response.setStatus(HTTP_STATUS_AUTH); response.getWriter().write(json.toString()); } // 登陸失敗時默認返回306狀態碼 protected void onLoginFail(ServletRequest request, ServletResponse response) throws IOException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HTTP_STATUS_AUTH); request.setAttribute("msg", "auth check error!"); // 跳轉到登陸頁 redirectToLogin(request, httpResponse); } @Override protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException { HttpServletRequest hReq = (HttpServletRequest) request; String rURL = hReq.getRequestURI(); String errors = StringUtils.isEmpty((String) request.getAttribute("msg")) ? "" : "&msg=" + request.getAttribute("msg"); if(request.getAttribute("msg") != null) { rURL += ((StringUtils.isNotEmpty(hReq.getQueryString())) ? "&" : "") + "msg=" + request.getAttribute("msg"); } rURL = Base64.encodeBase64URLSafeString(rURL.getBytes()) ; // 加入登陸前地址, 以及錯誤信息 String loginUrl = getLoginUrl() + "?r=" + rURL + errors; WebUtils.issueRedirect(request, response, loginUrl); } public boolean include(HttpServletRequest request) { String u = request.getRequestURI(); for (String e : esc) { if (u.endsWith(e)) { return true; } } if(StringUtils.isNotBlank(exeludeStr)){ String[] customExcludes = exeludeStr.split(","); for (String e : customExcludes) { if (u.endsWith(e)) { return true; } } } return false; } @Override public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception { super.afterCompletion(request, response, exception); InvocationInfoProxy.reset(); clearMDC(); } // 設置上下文中的擴展參數,rest傳遞上下文時生效,Authority header中排除固定key的其它信息都設置到InvocationInfoProxy的parameters private void initExtendParams(Cookie[] cookies) { for (Cookie cookie : cookies) { String cname = cookie.getName(); String cvalue = cookie.getValue(); if(!excludCongtextKeys.contains(cname)){ InvocationInfoProxy.setParameter(cname, cvalue); } } } private void initMDC() { String userName = ""; Subject subject = SecurityUtils.getSubject(); if (subject != null && subject.getPrincipal() != null) { userName = (String) SecurityUtils.getSubject().getPrincipal(); } // MDC中記錄用戶信息 MDC.put(AuthConstants.PARAM_USERNAME, userName); initCustomMDC(); } protected void initCustomMDC() { MDC.put("InvocationInfoProxy", MapToStringUtil.toEqualString(InvocationInfoProxy.getResources(), ';')); } protected void afterValidate(HttpServletRequest hReq){ } protected void clearMDC() { // MDC中記錄用戶信息 MDC.remove(AuthConstants.PARAM_USERNAME); clearCustomMDC(); } protected void clearCustomMDC() { MDC.remove("InvocationInfoProxy"); } //初始化 AuthConstants類中定義的常量 { Field[] fields = AuthConstants.class.getDeclaredFields(); try { for (Field field : fields) { field.setAccessible(true); if (field.getType().toString().endsWith("java.lang.String") && Modifier.isStatic(field.getModifiers())) { excludCongtextKeys.add((String) field.get(AuthConstants.class)); } } } catch (IllegalAccessException e) { e.printStackTrace(); } } }
dubbo服務調用時上下文的傳遞問題
思路:認證過濾器中 經過MDC將上下文信息寫入到InheritableThreadLocal中,寫一個dubbo的過濾器。在過濾器中判斷,若是是消費一方,則將MDC中的上下文取出來放入dubbo的context變量中;若是是服務方,則從dubbo的context中拿出上下文,解析並放入MDC以及InvocationInfoProxy(下面會提到)類中
Subject工廠
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.SubjectContext; import org.apache.shiro.web.mgt.DefaultWebSubjectFactory; public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory { @Override public Subject createSubject(SubjectContext context) { //不建立session context.setSessionCreationEnabled(false); return super.createSubject(context); } }
一樣禁用掉session的建立
無狀態Realm
import com.hjzgg.stateless.auth.session.ShiroSessionManager; import com.hjzgg.stateless.auth.token.ITokenProcessor; import com.hjzgg.stateless.auth.token.TokenParameter; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; 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; import org.springframework.beans.factory.annotation.Autowired; import java.util.ArrayList; import java.util.List; public class StatelessRealm extends AuthorizingRealm { private static final Logger logger = LoggerFactory.getLogger(StatelessRealm.class); @Autowired private ShiroSessionManager shiroSessionManager; @Override public boolean supports(AuthenticationToken token) { // 僅支持StatelessToken類型的Token return token instanceof StatelessToken; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); List<String> roles = new ArrayList<String>(); info.addRoles(roles); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken atoken) throws AuthenticationException { StatelessToken token = (StatelessToken) atoken; TokenParameter tp = token.getTp(); String userName = (String) token.getPrincipal(); ITokenProcessor tokenProcessor = token.getTokenProcessor(); String tokenStr = tokenProcessor.generateToken(tp); if (tokenStr == null || !shiroSessionManager.validateOnlineSession(userName, tokenStr)) { logger.error("User [{}] authenticate fail in System, maybe session timeout!", userName); throw new AuthenticationException("User " + userName + " authenticate fail in System"); } return new SimpleAuthenticationInfo(userName, tokenStr, getName()); } }
這裏使用自定義 session manager去校驗
無狀態token
import com.hjzgg.stateless.auth.token.ITokenProcessor; import com.hjzgg.stateless.auth.token.TokenParameter; import org.apache.shiro.authc.AuthenticationToken; import java.util.Map; public class StatelessToken implements AuthenticationToken { private String userName; // 預留參數集合,校驗更復雜的權限 private Map<String, ?> params; private String clientDigest; ITokenProcessor tokenProcessor; TokenParameter tp; public StatelessToken(String userName, ITokenProcessor tokenProcessor, TokenParameter tp , Map<String, ?> params, String clientDigest) { this.userName = userName; this.params = params; this.tp = tp; this.tokenProcessor = tokenProcessor; this.clientDigest = clientDigest; } public TokenParameter getTp() { return tp; } public void setTp(TokenParameter tp) { this.tp = tp; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public Map<String, ?> getParams() { return params; } public void setParams( Map<String, ?> params) { this.params = params; } public String getClientDigest() { return clientDigest; } public void setClientDigest(String clientDigest) { this.clientDigest = clientDigest; } @Override public Object getPrincipal() { return userName; } @Override public Object getCredentials() { return clientDigest; } public ITokenProcessor getTokenProcessor() { return tokenProcessor; } public void setTokenProcessor(ITokenProcessor tokenProcessor) { this.tokenProcessor = tokenProcessor; } }
token處理器
import com.hjzgg.stateless.auth.session.ShiroSessionManager; import com.hjzgg.stateless.common.constants.AuthConstants; import com.hjzgg.stateless.common.esapi.EncryptException; import com.hjzgg.stateless.common.esapi.IYCPESAPI; import com.hjzgg.stateless.common.utils.CookieUtil; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.http.Cookie; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; /** * 默認Token處理器提供將cooke和TokenParameter相互轉換,Token生成的能力 * <p> * 能夠註冊多個實例 * </p> * * @author li * */ public class DefaultTokenPorcessor implements ITokenProcessor { private static Logger log = LoggerFactory.getLogger(DefaultTokenPorcessor.class); private static int HTTPVERSION = 3; static { URL res = DefaultTokenPorcessor.class.getClassLoader().getResource("javax/servlet/annotation/WebServlet.class"); if (res == null) { HTTPVERSION = 2; } } private String id; private String domain; private String path = "/"; private Integer expr; // 默認迭代次數 private int hashIterations = 2; @Autowired private ShiroSessionManager shiroSessionManager; @Override public String getId() { return id; } public void setId(String id) { this.id = id; } public String getDomain() { return domain; } public void setDomain(String domain) { this.domain = domain; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public Integer getExpr() { return expr; } public void setExpr(Integer expr) { this.expr = expr; } private List<String> exacts = new ArrayList<String>(); public void setExacts(List<String> exacts) { this.exacts = exacts; } public int getHashIterations() { return hashIterations; } public void setHashIterations(int hashIterations) { this.hashIterations = hashIterations; } @Override public String generateToken(TokenParameter tp) { try { String seed = shiroSessionManager.findSeed(); String token = IYCPESAPI.encryptor().hash( this.id + tp.getUserName() + tp.getLoginTs() + getSummary(tp) + getExpr(), seed, getHashIterations()); token = this.id + "," + getExpr() + "," + token; return Base64.encodeBase64URLSafeString(org.apache.commons.codec.binary.StringUtils.getBytesUtf8(token)); } catch (EncryptException e) { log.error("TokenParameter is not validate!", e); throw new IllegalArgumentException("TokenParameter is not validate!"); } } @Override public Cookie[] getCookieFromTokenParameter(TokenParameter tp) { List<Cookie> cookies = new ArrayList<Cookie>(); String tokenStr = generateToken(tp); Cookie token = new Cookie(AuthConstants.PARAM_TOKEN, tokenStr); if (HTTPVERSION == 3) token.setHttpOnly(true); if (StringUtils.isNotEmpty(domain)) token.setDomain(domain); token.setPath(path); cookies.add(token); try { Cookie userId = new Cookie(AuthConstants.PARAM_USERNAME, URLEncoder.encode(tp.getUserName(), "UTF-8")); if (StringUtils.isNotEmpty(domain)) userId.setDomain(domain); userId.setPath(path); cookies.add(userId); // 登陸的時間戳 Cookie logints = new Cookie(AuthConstants.PARAM_LOGINTS, URLEncoder.encode(tp.getLoginTs(), "UTF-8")); if (StringUtils.isNotEmpty(domain)) logints.setDomain(domain); logints.setPath(path); cookies.add(logints); } catch (UnsupportedEncodingException e) { log.error("encode error!", e); } if (!tp.getExt().isEmpty()) { Iterator<Entry<String, String>> it = tp.getExt().entrySet().iterator(); while (it.hasNext()) { Entry<String, String> i = it.next(); Cookie ext = new Cookie(i.getKey(), i.getValue()); if (StringUtils.isNotEmpty(domain)) ext.setDomain(domain); ext.setPath(path); cookies.add(ext); } } shiroSessionManager.registOnlineSession(tp.getUserName(), tokenStr, this); return cookies.toArray(new Cookie[] {}); } @Override public TokenParameter getTokenParameterFromCookie(Cookie[] cookies) { TokenParameter tp = new TokenParameter(); String token = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_TOKEN); TokenInfo ti = TokenFactory.getTokenInfo(token); if (ti.getIntegerExpr().intValue() != this.getExpr().intValue()) { throw new IllegalArgumentException("illegal token!"); } String userId = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_USERNAME); tp.setUserName(userId); String loginTs = CookieUtil.findCookieValue(cookies, AuthConstants.PARAM_LOGINTS); tp.setLoginTs(loginTs); if (exacts != null && !exacts.isEmpty()) { for (int i = 0; i < cookies.length; i++) { Cookie cookie = cookies[i]; String name = cookie.getName(); if (exacts.contains(name)) { tp.getExt().put(name, cookie.getValue() == null ? "" : cookie.getValue()); } } } return tp; } protected String getSummary(TokenParameter tp) { if (exacts != null && !exacts.isEmpty()) { int len = exacts.size(); String[] exa = new String[len]; for (int i = 0; i < len; i++) { String name = exacts.get(i); String value = tp.getExt().get(name); if(value == null) value = ""; exa[i] = value; } return StringUtils.join(exa, "#"); } return ""; } @Override public Cookie[] getLogoutCookie(String tokenStr, String uid) { List<Cookie> cookies = new ArrayList<Cookie>(); Cookie token = new Cookie(AuthConstants.PARAM_TOKEN, null); if (StringUtils.isNotEmpty(domain)) token.setDomain(domain); token.setPath(path); cookies.add(token); Cookie userId = new Cookie(AuthConstants.PARAM_USERNAME, null); if (StringUtils.isNotEmpty(domain)) userId.setDomain(domain); userId.setPath(path); cookies.add(userId); // 登陸的時間戳 Cookie logints = new Cookie(AuthConstants.PARAM_LOGINTS, null); if (StringUtils.isNotEmpty(domain)) logints.setDomain(domain); logints.setPath(path); cookies.add(logints); for (String exact : exacts) { Cookie ext = new Cookie(exact, null); if (StringUtils.isNotEmpty(domain)) ext.setDomain(domain); ext.setPath(path); cookies.add(ext); } shiroSessionManager.delOnlineSession(uid, tokenStr); return cookies.toArray(new Cookie[] {}); } }
將一些必須字段和擴展字段進行經過esapi 的hash算法進行加密,生成token串,最終的token = token處理器標識+過時時間+原token
shiro配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="statelessRealm" class="com.hjzgg.stateless.auth.shiro.StatelessRealm"> <property name="cachingEnabled" value="false" /> </bean> <!-- Subject工廠 --> <bean id="subjectFactory" class="com.hjzgg.stateless.auth.shiro.StatelessDefaultSubjectFactory" /> <bean id="webTokenProcessor" class="com.hjzgg.stateless.auth.token.DefaultTokenPorcessor"> <property name="id" value="web"></property> <property name="path" value="${context.name}"></property> <property name="expr" value="${sessionTimeout}"></property> <property name="exacts"> <list> <value type="java.lang.String">userType</value> </list> </property> </bean> <bean id="maTokenProcessor" class="com.hjzgg.stateless.auth.token.DefaultTokenPorcessor"> <property name="id" value="ma"></property> <property name="path" value="${context.name}"></property> <property name="expr" value="-1"></property> <property name="exacts"> <list> <value type="java.lang.String">userType</value> </list> </property> </bean> <bean id="tokenFactory" class="com.hjzgg.stateless.auth.token.TokenFactory"> <property name="processors"> <list> <ref bean="webTokenProcessor" /> <ref bean="maTokenProcessor" /> </list> </property> </bean> <!-- 會話管理器 --> <bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager"> <property name="sessionValidationSchedulerEnabled" value="false" /> </bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list> <ref bean="statelessRealm" /> </list> </property> <property name="subjectDAO.sessionStorageEvaluator.sessionStorageEnabled" value="false" /> <property name="subjectFactory" ref="subjectFactory" /> <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> <bean id="statelessAuthcFilter" class="com.hjzgg.stateless.auth.shiro.StatelessAuthcFilter"> <property name="tokenFactory" ref="tokenFactory" /> </bean> <bean id="logout" class="com.hjzgg.stateless.auth.shiro.LogoutFilter"></bean> <!-- Shiro的Web過濾器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/login" /> <property name="filters"> <util:map> <entry key="statelessAuthc" value-ref="statelessAuthcFilter" /> </util:map> </property> <property name="filterChainDefinitions"> <value> <!--swagger--> /webjars/** = anon /v2/api-docs/** = anon /swagger-resources/** = anon /login/** = anon /logout = logout /static/** = anon /css/** = anon /images/** = anon /trd/** = anon /js/** = anon /api/** = anon /cxf/** = anon /jaxrs/** = anon /** = statelessAuthc </value> </property> </bean> <!-- Shiro生命週期處理器 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> </beans>
經過InvocationInfoProxy這個類(基於ThreadLocal的),能夠拿到用戶相關的參數信息
import com.hjzgg.stateless.common.constants.AuthConstants; import java.util.HashMap; import java.util.Map; /** * Created by hujunzheng on 2017/7/18. */ public class InvocationInfoProxy { private static final ThreadLocal<Map<String, Object>> resources = ThreadLocal.withInitial(() -> { Map<String, Object> initialValue = new HashMap<>(); initialValue.put(AuthConstants.ExtendConstants.PARAM_PARAMETER, new HashMap<String, String>()); return initialValue; } ); public static String getUserName() { return (String) resources.get().get(AuthConstants.PARAM_USERNAME); } public static void setUserName(String userName) { resources.get().put(AuthConstants.PARAM_USERNAME, userName); } public static String getLoginTs() { return (String) resources.get().get(AuthConstants.PARAM_LOGINTS); } public static void setLoginTs(String loginTs) { resources.get().put(AuthConstants.PARAM_LOGINTS, loginTs); } public static String getToken() { return (String) resources.get().get(AuthConstants.PARAM_TOKEN); } public static void setToken(String token) { resources.get().put(AuthConstants.PARAM_TOKEN, token); } public static void setParameter(String key, String value) { ((Map<String, String>) resources.get().get(AuthConstants.ExtendConstants.PARAM_PARAMETER)).put(key, value); } public static String getParameter(String key) { return ((Map<String, String>) resources.get().get(AuthConstants.ExtendConstants.PARAM_PARAMETER)).get(key); } public static void reset() { resources.remove(); } }
還有esapi和cache的相關代碼到項目裏看一下吧
歡迎訪問,無狀態shiro認證組件!