zoukankan      html  css  js  c++  java
  • 无状态shiro认证组件(禁用默认session)

    准备内容

    简单的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 {
    
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
            return false;
        }
    
        @Override
        protected 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、生成无状态Token
            StatelessToken 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 {
    
        @Override
        public Subject createSubject(SubjectContext context) {
            //不创建session
            context.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 {
        @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;
        }
    }
    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;
        }
    
        @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());
        }
    }
    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.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>
    View Code

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

      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>
    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";
    
        @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();
        }
    }
    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 {
    
        @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);
        }
    }
    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;
    
        @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);
    
            // 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、生成无状态Token
                StatelessToken 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);
        }
    
        @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();
            }
        }
    }
    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 {
    
        @Override
        public Subject createSubject(SubjectContext context) {
            //不创建session
            context.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);
    
        @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());
        }
    
    }
    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;
        }
    
        @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;
        }
    }
    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;
    
        @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[] {});
        }
    }
    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.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>
    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 的实现分析

  • 相关阅读:
    HDU 5585 Numbers
    HDU 3308 LCIS
    POJ 2991 Crane
    POJ 1436 Horizontally Visible Segments
    POJ 3667 Hotel
    HaiHongOJ 1003 God Wang
    【SDOI 2008】 递归数列
    5月19日省中提高组题解
    【HDU 1588】 Gauss Fibonacci
    【POJ 3233】Matrix Power Series
  • 原文地址:https://www.cnblogs.com/hujunzheng/p/7210157.html
Copyright © 2011-2022 走看看