准备内容

简单的shiro无状态认证

  无状态认证拦截器

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 {@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {return false;}@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {//1、客户端生成的消息摘要String clientDigest = request.getParameter(Constants.PARAM_DIGEST);//2、客户端传入的用户身份String username = request.getParameter(Constants.PARAM_USERNAME);//3、客户端请求的参数列表Map<String, String[]> params = new HashMap<String, String[]>(request.getParameterMap());params.remove(Constants.PARAM_DIGEST);//4、生成无状态TokenStatelessToken token = new StatelessToken(username, params, clientDigest);try {//5、委托给Realm进行登录
            getSubject(request, response).login(token);} catch (Exception e) {e.printStackTrace();onLoginFail(response); //6、登录失败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");}
}

View Code

  Subject工厂

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 {@Overridepublic Subject createSubject(SubjectContext context) {//不创建sessioncontext.setSessionCreationEnabled(false);return super.createSubject(context);}
}

View Code

  注意,这里禁用了session

  无状态Realm

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 {@Overridepublic boolean supports(AuthenticationToken token) {//仅支持StatelessToken类型的Tokenreturn token instanceof StatelessToken;}@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {//根据用户名查找角色,请根据需求实现String username = (String) principals.getPrimaryPrincipal();SimpleAuthorizationInfo authorizationInfo =  new SimpleAuthorizationInfo();authorizationInfo.addRole("admin");return authorizationInfo;}@Overrideprotected 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;}
}

View Code

  无状态Token

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;}@Overridepublic Object getPrincipal() {return username;}@Overridepublic 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());}
}

View Code

  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.xsdhttp://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsdhttp://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>

View Code

  这里禁用了回话调度器的session存储

  web.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<web-appxmlns="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>

View Code

  token生成工具类

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());}}

View Code

  简单测试一下

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";@Testpublic 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);}@Testpublic 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();}
}

View Code

  补充Spring中多重属性赋值处理

  以上参考 开涛老师的博文!

相对复杂一点的shiro无状态认证

  *加入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 {@Autowiredprivate 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);}
}

View Code

  无状态认证过滤器

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;@Autowiredprivate 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;}@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {return false;}@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {boolean isAjax = isAjax(request);// 1、客户端发送来的摘要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);// 2、客户端传入的用户身份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;}// 3、客户端请求的参数列表Map<String, String[]> params = new HashMap<String, String[]>(request.getParameterMap());ITokenProcessor tokenProcessor = tokenFactory.getTokenProcessor(tokenStr);TokenParameter tp = tokenProcessor.getTokenParameterFromCookie(cookies);// 4、生成无状态TokenStatelessToken token = new StatelessToken(userName, tokenProcessor, tp, params, new String(tokenStr));try {// 5、委托给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); // 6、验证失败,返回ajax调用方信息return false;} else {onLoginFail(request, response); // 6、登录失败,跳转到登录页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);}@Overrideprotected 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;}@Overridepublic void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {super.afterCompletion(request, response, exception);InvocationInfoProxy.reset();clearMDC();}// 设置上下文中的扩展参数,rest传递上下文时生效,Authority header中排除固定key的其它信息都设置到InvocationInfoProxy的parametersprivate 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();}}
}

View Code

  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 {@Overridepublic Subject createSubject(SubjectContext context) {//不创建sessioncontext.setSessionCreationEnabled(false);return super.createSubject(context);}
}

View Code

  同样禁用掉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);@Autowiredprivate ShiroSessionManager shiroSessionManager;@Overridepublic boolean supports(AuthenticationToken token) {// 仅支持StatelessToken类型的Tokenreturn token instanceof StatelessToken;}@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();List<String> roles = new ArrayList<String>();info.addRoles(roles);return info;}@Overrideprotected 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());}}

View Code

  这里使用自定义 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;}@Overridepublic Object getPrincipal() {return userName;}@Overridepublic Object getCredentials() {return clientDigest;}public ITokenProcessor getTokenProcessor() {return tokenProcessor;}public void setTokenProcessor(ITokenProcessor tokenProcessor) {this.tokenProcessor = tokenProcessor;}
}

View Code

  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;@Autowiredprivate ShiroSessionManager shiroSessionManager;@Overridepublic 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;}@Overridepublic 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!");}}@Overridepublic 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[] {});}@Overridepublic 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 "";}@Overridepublic 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[] {});}
}

View Code

  将一些必须字段和扩展字段进行通过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.xsdhttp://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsdhttp://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) --><beanclass="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>

View Code

  通过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();}
}

View Code

  还有esapi和cache的相关代码到项目里看一下吧

项目地址

  欢迎访问,无状态shiro认证组件!

参考拦截

    ESAPI入门使用方法

   Spring MVC 4.2 增加 CORS 支持

  HTTP访问控制(CORS)

  Slf4j MDC 使用和 基于 Logback 的实现分析

转载于:https://www.cnblogs.com/hujunzheng/p/7210157.html

无状态shiro认证组件(禁用默认session)相关推荐

  1. jwt无状态权限认证(pings-shiro-jwt)

    单用户并发访问的问题 当用户AccessToken失效,用户使用该失效的AccessToken同时发起多个请求,会产生多AccessToken和RefreshToken认证失败问题: 多AccessT ...

  2. 浅析权限认证中的有状态和无状态

    转自:https://www.cnblogs.com/shiyajian/p/10672908.html 前言 我们在设计构建一个系统的时候,权限管理和用户认证是最基本功能,其中关于用户认证这块是一个 ...

  3. 有状态服务和无状态服务的区别

    有状态服务和无状态服务的区别 有状态和无状态服务是两种不同的服务架构,两者的不同之处在于对于服务状态的处理.服务状态是服务请求所需的数据,它可以是一个变量或者一个数据结构.无状态服务不会记录服务状态, ...

  4. ipv6 无状态地址管理

    一.ipv6 无状态地址管理 与IPv4通过DHCP下发地址方式不同的是,IPv6地址可以不通过DHCP方式获取.归功于IPv6所支持的无状态地址配置机制,由RFC4862定义. 1.1 IPv6的两 ...

  5. ipv6无状态自动分配地址,是如何进行工作的。

    ipv6无状态自动分配地址,是如何进行工作的. IPV6无状态自动分配地址 主机或路由器从RA报文里获得64位前缀,然后通过EUI-64规范自动生成64bit的接口标识,然后得到IPV6单播地址. 默 ...

  6. shiro实现无状态的会话,带源码分析

    转载请在页首明显处注明作者与出处 朱小杰      http://www.cnblogs.com/zhuxiaojie/p/7809767.html 一:说明 在网上都找不到相关的信息,还是翻了大半天 ...

  7. SpringCloud 配置安全验证、服务消费端处理、无状态 Session 配置、定义公共安全配置程序类

    所有的 Rest 服务最终都是暴露在公网上的,也就是说如果你的 Rest 服务属于一些你自己公司的私人业务,这样的结果会直接 导致你信息的泄漏,所以对于 Rest 访问,安全性是首要的因素. 2.1. ...

  8. shiro手机无状态登录访问和电脑端登录访问两种方式处理

    shiro stateless Demo of shiro session and stateless 安全框架shiro的一个同时支持无状态和session登录的添加部分自定义的demo 完整dem ...

  9. Blazor中的无状态组件

    声明:本文将RenderFragment称之为组件DOM树或者是组件DOM节点,将*.razor称之为组件. 1. 什么是无状态组件 如果了解React,那就应该清楚,React中存在着一种组件,它只 ...

最新文章

  1. 数值计算领域的“圣经”,图灵出了新版本 | 11月书讯
  2. [Offer收割]编程练习赛50
  3. window.open打开新窗口不改变原窗口_我P的图不高级,就很可耻吗?
  4. 安全测试-抓包工具BurpSuite
  5. Angular property binding重复触发的问题讨论
  6. python爬虫本科容易找工作吗_python爬虫基础学完了,我真的能找到一份工作吗?...
  7. eureka注册中心HA集群搭建
  8. wxpython是干嘛的_你都用 Python 来做什么?
  9. 与资源库同步时,我的svn报错 Previous operation has not finished; run 'cleanup' if it was interrupted...
  10. oracle数据库从AIX环境expdp迁移到linux环境(sec_case_sensitive_logon=true导致连接报错ORA-01017)
  11. Jquery图片放大镜效果
  12. 构建Python软件大厦系列
  13. ubuntu16.04下载opencv3.4
  14. cesium接入加载倾斜摄影(cesium篇.16)
  15. 必应每日一图php,轻量必应每日一图HTML源码
  16. The server time zone value '?й???????' is unrecognized or represents more than one time zone.
  17. 苹果计算机访问限制,苹果手机访问限制密码忘了怎么办
  18. 如何区分m的属性_测试属性#5 –区分
  19. Vue3关于页面跳转(push)的时候,并传递数据过去
  20. Android特效专辑(六)——仿QQ聊天撒花特效,无形装逼,最为致命

热门文章

  1. matlab cell转数组_MATLAB批量修改文件名
  2. 贾又福大象鸿蒙,奏乐!继续吹!库里又创记录,射进MVP榜单,众多名记变“库吹“...
  3. deepin linux 2014 硬盘安装教程,Linux Deepin的硬盘安装
  4. Mongo 安装、配置、启动 Windows
  5. mybatis批量插入10万条数据的优化过程
  6. Jmeter5 语言中文
  7. (需求实战_03)_shell脚本 sftp协议下载文件
  8. SpringBoot聚合项目总结
  9. 计算机桌面图标变成腾讯图标,桌面图标变成了未知图标
  10. 用计算机写文章 单元备课,信息技术第一单元单元备课精要.doc