zoukankan      html  css  js  c++  java
  • shiro权限控制(二):分布式架构中shiro的实现

    前言:
    前段时间在搭建公司游戏框架安全验证的时候,就想到之前web最火的shiro框架,虽然后面实践发现在netty中不太适用,最后自己模仿shiro写了一个缩减版的,但是中间花费两天时间弄出来的shiro可不能白费,这里给大家出个简单的教程说明吧。

    shiro的基本介绍这里就不再说了,可以自行翻阅博主之前写的shiro教程,这篇文章主要说明分布式架构下shiro的session共享问题。

    一、原理描述

    无论分布式、还是集群下,项目都需要获取登录用户的信息,而不可能做的就是让客户在每个系统或者每个模块中反复登录,也不存在让客户端存载用户信息给服务端,这是很常识的问题

    而单机模式下,我们用shiro做了登录验证,他的主要方式就是在第一次登陆的时候,把我们设置的用户信息保存在cache(内存)中和自带的ehcahe(缓存管理器)中,然后给客户端一个cookie,在每次客户端访问时获取cookie值,从而得到用户信息。

    好了,那么逻辑就清楚了,分布式架构下,要与多系统共享用户信息,其实就是共享shiro保存的cache。

    要在多项目中共享,内存是不可能的了,ehcache对分布式支持不太好,或者说根本不支持。那么剩下只能是我么熟悉的mysql,redis,mongdb啥的数据库了。这么一对比,不用我说大家也明白了,最适合的无疑是redis了,速度快,主从啥的。

    二、流程描述

    查看源码我们可以知道,cacheManager最终会被set到sessionDAO中,所以我们要自己写sessionDAO。有两个类去操作保存的,那么我们只需要重写,实现这两个类,然后在注册的时候声明即可。

    1.shiroCache:cache类,可以自己写一个定时消除的MAP存放更好,文章结尾我会给出map的代码。而这里的代码我是放在redis的。

    package com.result.shiro.distributed;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.List;
    import java.util.Set;
    import org.apache.shiro.cache.Cache;
    import org.apache.shiro.cache.CacheException;
    import com.result.redis.RedisKey;
    import com.result.redis.RedisUtil;
    import com.result.tools.KyroUtil;
    /**
    * @author 作者 huangxinyu
    * @version 创建时间:2018年1月8日 下午9:33:23
    * cache共享
    */
    @SuppressWarnings("unchecked")
    public class ShiroCache<K, V> implements Cache<K, V> {
    private static final String REDIS_SHIRO_CACHE = RedisKey.CACHEKEY;
    private String cacheKey;
    private long globExpire = 30;
    @SuppressWarnings("rawtypes")
    public ShiroCache(String name) {
    this.cacheKey = REDIS_SHIRO_CACHE + name + ":";
    }
    @Override
    public V get(K key) throws CacheException {
    Object obj = RedisUtil.get(KyroUtil.serialization(getCacheKey(key)));
    if(obj==null){
    return null;
    }
    return (V) KyroUtil.deserialization((String)obj);
    }
    @Override
    public V put(K key, V value) throws CacheException {
    V old = get(key);
    RedisUtil.setex(KyroUtil.serialization(getCacheKey(key)), 18000, KyroUtil.serialization(value));
    return old;
    }
    @Override
    public V remove(K key) throws CacheException {
    V old = get(key);
    RedisUtil.del(KyroUtil.serialization(getCacheKey(key)));
    return old;
    }
    @Override
    public void clear() throws CacheException {
    for(String key : (Set<String>)keys()){
    RedisUtil.del(key);
    }
    }
    @Override
    public int size() {
    return keys().size();
    }
    @Override
    public Set<K> keys() {
    return (Set<K>) RedisUtil.keys(KyroUtil.serialization(getCacheKey("*")));
    }
    @Override
    public Collection<V> values() {
    Set<K> set = keys();
    List<V> list = new ArrayList<>();
    for (K s : set) {
    list.add(get(s));
    }
    return list;
    }
    private K getCacheKey(Object k) {
    return (K) (this.cacheKey + k);
    }
    }

    2.session操作类:这里用来把用户信息存放在redis中共享的。

    package com.result.shiro.distributed;
    /**
    * @author 作者 huangxinyu
    * @version 创建时间:2018年1月6日 上午10:12:42
    * redis实现共享session
    */
    import java.io.Serializable;
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.Set;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import com.result.redis.RedisKey;
    import com.result.redis.RedisUtil;
    import com.result.tools.KyroUtil;
    import com.result.tools.SerializationUtil;
    public class RedisSessionDao extends EnterpriseCacheSessionDAO {
    private static Logger logger = LoggerFactory.getLogger(RedisSessionDao.class);
    @Override
    public void update(Session session) throws UnknownSessionException {
    this.saveSession(session);
    }
    /**
    * 删除session
    */
    @Override
    public void delete(Session session) {
    if (session == null || session.getId() == null) {
    logger.error("==========session或sessionI 不存在");
    return;
    }
    RedisUtil.del(KyroUtil.serialization(RedisKey.SESSIONKEY + session.getId()));
    }
    /**
    * 获取存活的sessions
    */
    @Override
    public Collection<Session> getActiveSessions() {
    Set<Session> sessions = new HashSet<>();
    Set<String> keys = RedisUtil.keys(KyroUtil.serialization(RedisKey.SESSIONKEY + "*"));
    for(String key:keys){
    sessions.add((Session)KyroUtil.deserialization((String)RedisUtil.get(key)));
    }
    return sessions;
    }
    /**
    * 创建session
    */
    @Override
    protected Serializable doCreate(Session session) {
    Serializable sessionId = this.generateSessionId(session);
    this.assignSessionId(session, sessionId);
    this.saveSession(session);
    return sessionId;
    }
    /**
    * 获取session
    */
    @Override
    protected Session doReadSession(Serializable sessionId) {
    if(sessionId == null){
    logger.error("==========session id 不存在");
    return null;
    }
    Object obj = RedisUtil.get(KyroUtil.serialization(RedisKey.SESSIONKEY + sessionId));
    if(obj==null){
    return null;
    }
    Session s = (Session)KyroUtil.deserialization((String)obj);
    return s;
    }
    /**
    * 保存session并存储过期时间
    * @param session
    * @throws UnknownSessionException
    */
    public static void saveSession(String sessionId,Object obj) throws UnknownSessionException{
    if (obj == null) {
    logger.error("要存入的session为空");
    return;
    }
    //设置过期时间
    int expireTime = 1800;
    RedisUtil.setex(sessionId,expireTime,SerializationUtil.serializeToString(obj));
    }
    }
    然后还有一个类也是必要的
    
    package com.result.shiro.distributed;
    import org.apache.shiro.cache.Cache;
    import org.apache.shiro.cache.CacheException;
    import org.apache.shiro.cache.CacheManager;
    /**
    * @author 作者 huangxinyu
    * @version 创建时间:2018年1月8日 下午9:32:41
    * 类说明
    */
    public class RedisCacheManager implements CacheManager {
    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
    return new ShiroCache<K, V>(name);
    }
    }

    三:辅助类说明

    用户信息的session存放在redis中肯定是需要序列化的,然而用json这种可读性太强的东西安全性显得极低,而且长度太大,浪费存储空间和IO。所以需要找其他的序列化工具。

    常规的好用的序列化工具有kyro,protobuff,这些是性能极高而且序列化之后长度极小的序列化工具,其中protobuf支持跨语言。不过这些在之后的文章再和大家介绍去了,因为~!!session不支持这两种操作(因为上面两个类中操作的session实际是一个接口)。

    那么序列化用的什么,emmmm~一个很原生的东西,测试效率也挺高的,和protobuf差不太多。下面贴出的代码实际就是上面类中kyroUtils中的方法,因为shiro分布式在项目中被废掉了,我也没去改名字了。大家自己看仔细点就可以了。

    被注释掉的代码是kyro的序列化工具。

    package com.result.tools;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    /**
    * @author 作者 huangxinyu
    * @version 创建时间:2018年1月6日 下午2:22:14
    * Kryo工具类
    */
    public class KyroUtil {
    private static Logger logger = LoggerFactory.getLogger(KyroUtil.class);
    //private static KryoPool pool;
    //原本打算使用kyro序列化session,后来发现kyro对session序列化不支持,反序列后得不到value。 这种out序列化测试性能消耗时间更短,但是长度变大4倍意思,待优化
    // static{
    // KryoFactory factory = new KryoFactory() {
    // public Kryo create() {
    // Kryo kryo = new Kryo();
    // kryo.setReferences(false);
    // //把shiroSession的结构注册到Kryo注册器里面,提高序列化/反序列化效率
    // kryo.register(Session.class, new JavaSerializer());
    // kryo.register(String.class, new JavaSerializer());
    // kryo.register(User.class, new JavaSerializer());
    // kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
    // return kryo;
    // }
    // };
    // pool = new KryoPool.Builder(factory).build();
    // logger.info("KryoPool初始化成功====================================");
    // }
    /**
    * 对象编码
    * @param value
    * @return
    */
    public static String serialization(Object value) {
    // String str ="";
    // try {
    // Kryo kryo = pool.borrow();
    // ByteArrayOutputStream baos = new ByteArrayOutputStream();
    // Output output = new Output(baos);
    // kryo.writeClassAndObject(output, value);
    // output.flush();
    // output.close();
    // byte[] b = baos.toByteArray();
    // baos.flush();
    // baos.close();
    // str = new String(b, "ISO8859-1");
    // } catch (IOException e) {
    // e.printStackTrace();
    // }
    // return str;
    //
    ByteArrayOutputStream bos = null;
    ObjectOutputStream oos = null;
    try {
    bos = new ByteArrayOutputStream();
    oos = new ObjectOutputStream(bos);
    oos.writeObject(value);
    return new String(bos.toByteArray(), "ISO8859-1");
    } catch (Exception e) {
    throw new RuntimeException("serialize session error", e);
    } finally {
    try {
    oos.close();
    bos.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    // return new String(new Base64().encode(b));
    }
    /**
    * 对象解码
    * @param <T>
    * @param <T>
    * @param obj
    * @param clazz
    * @return
    */
    public static Object deserialization(String obj) {
    // try {
    // Kryo kryo = pool.borrow();
    // ByteArrayInputStream bais;
    // bais = new ByteArrayInputStream(obj.getBytes("ISO8859-1"));
    // //new Base64().decode(obj));
    // Input input = new Input(bais);
    // return kryo.readClassAndObject(input);
    // } catch (UnsupportedEncodingException e) {
    // // TODO Auto-generated catch block
    // e.printStackTrace();
    // }
    // return null;
    ByteArrayInputStream bis = null;
    ObjectInputStream ois = null;
    try {
    bis = new ByteArrayInputStream(obj.getBytes("ISO8859-1"));
    ois = new ObjectInputStream(bis);
    return ois.readObject();
    } catch (Exception e) {
    throw new RuntimeException("deserialize session error", e);
    } finally {
    try {
    ois.close();
    bis.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    }
    }

    四、注册

    好了,该重写的都重写了,那么最后一步就是整合spring的时候我们要告诉spring,我们要用的是我们重写过的sessiondao了。

    我这里用的是代码的方式,因为某些原因在写框架的时候不太好用xml去整合。

    反正原理都差不多,大家看看就明白了:

    package com.business.shiro;
    import java.util.LinkedHashMap;
    import java.util.Map;
    import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
    import org.apache.shiro.cache.CacheManager;
    import org.apache.shiro.cache.ehcache.EhCacheManager;
    import org.apache.shiro.codec.Base64;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler;
    import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
    import org.apache.shiro.spring.LifecycleBeanPostProcessor;
    import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
    import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
    import org.apache.shiro.web.mgt.CookieRememberMeManager;
    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
    import org.apache.shiro.web.servlet.SimpleCookie;
    import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
    import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.DependsOn;
    import com.result.shiro.distributed.RedisCacheManager;
    import com.result.shiro.distributed.RedisSessionDao;
    /**
    * @author 作者 huangxinyu
    * @version 创建时间:2018年1月8日 下午8:29:12
    * 类说明
    */
    @Configuration
    public class ShiroConfiguration {
    private static Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
    @Bean(name = "cacheShiroManager")
    public CacheManager getCacheManage() {
    return new RedisCacheManager();
    }
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
    }
    @Bean(name = "sessionValidationScheduler")
    public ExecutorServiceSessionValidationScheduler getExecutorServiceSessionValidationScheduler() {
    ExecutorServiceSessionValidationScheduler scheduler = new ExecutorServiceSessionValidationScheduler();
    scheduler.setInterval(900000);
    return scheduler;
    }
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher getHashedCredentialsMatcher() {
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
    credentialsMatcher.setHashAlgorithmName("MD5");
    credentialsMatcher.setHashIterations(1);
    credentialsMatcher.setStoredCredentialsHexEncoded(true);
    return credentialsMatcher;
    }
    @Bean(name = "sessionIdCookie")
    public SimpleCookie getSessionIdCookie() {
    SimpleCookie cookie = new SimpleCookie("sid");
    cookie.setHttpOnly(true);
    cookie.setMaxAge(-1);
    return cookie;
    }
    @Bean(name = "rememberMeCookie")
    public SimpleCookie getRememberMeCookie() {
    SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
    simpleCookie.setHttpOnly(true);
    simpleCookie.setMaxAge(2592000);
    return simpleCookie;
    }
    @Bean
    public CookieRememberMeManager getRememberManager(){
    CookieRememberMeManager meManager = new CookieRememberMeManager();
    meManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
    meManager.setCookie(getRememberMeCookie());
    return meManager;
    }
    @Bean(name = "sessionManager")
    public DefaultWebSessionManager getSessionManage() {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setGlobalSessionTimeout(1800000);
    sessionManager.setSessionValidationScheduler(getExecutorServiceSessionValidationScheduler());
    sessionManager.setSessionValidationSchedulerEnabled(true);
    sessionManager.setDeleteInvalidSessions(true);
    sessionManager.setSessionIdCookieEnabled(true);
    sessionManager.setSessionIdCookie(getSessionIdCookie());
    RedisSessionDao cacheSessionDAO = new RedisSessionDao();
    cacheSessionDAO.setCacheManager(getCacheManage());
    sessionManager.setSessionDAO(cacheSessionDAO);
    // -----可以添加session 创建、删除的监听器
    return sessionManager;
    }
    @Bean(name = "myRealm")
    public AuthorizingRealm getShiroRealm() {
    MyRealm realm = new MyRealm();
    // realm.setName("shiro_auth_cache");
    // realm.setAuthenticationCache(getCacheManage().getCache(realm.getName()));
    // realm.setAuthenticationTokenClass(UserAuthenticationToken.class);
    return realm;
    }
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getSecurityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setCacheManager(getCacheManage());
    securityManager.setSessionManager(getSessionManage());
    securityManager.setRememberMeManager(getRememberManager());
    securityManager.setRealm(getShiroRealm());
    return securityManager;
    }
    @Bean
    public MethodInvokingFactoryBean getMethodInvokingFactoryBean(){
    MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean();
    factoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
    factoryBean.setArguments(new Object[]{getSecurityManager()});
    return factoryBean;
    }
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator getAutoProxyCreator(){
    DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
    creator.setProxyTargetClass(true);
    return creator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(){
    AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
    advisor.setSecurityManager(getSecurityManager());
    return advisor;
    }
    /**
    * @return
    */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(){
    ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
    factoryBean.setSecurityManager(getSecurityManager());
    factoryBean.setLoginUrl("/toLogin");
    factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return factoryBean;
    }
    }

    优化:伪定时消除map,最好配合quartz清楚,不然内存中MAP如果不访问就不消除,容易累计。

    package com.result.security;
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map;
    import java.util.Set;
    import com.result.NettyGoConstant;
    /**
    * @author 作者 huangxinyu
    * @version 创建时间:2018年1月29日 上午10:31:50 类说明
    */
    public class ExpiryMap<K, V> extends HashMap<K, V> {
    private static final long serialVersionUID = 1L;
    /**
    * default expiry time 2m
    */
    private long EXPIRY = NettyGoConstant.LOGINSESSIONTIMEOUT;
    private HashMap<K, Long> expiryMap = new HashMap<>();
    public ExpiryMap() {
    super();
    }
    public ExpiryMap(long defaultExpiryTime) {
    this(1 << 4, defaultExpiryTime);
    }
    public ExpiryMap(int initialCapacity, long defaultExpiryTime) {
    super(initialCapacity);
    this.EXPIRY = defaultExpiryTime;
    }
    public V put(K key, V value) {
    expiryMap.put(key, System.currentTimeMillis() + EXPIRY);
    return super.put(key, value);
    }
    public boolean containsKey(Object key) {
    return !checkExpiry(key, true) && super.containsKey(key);
    }
    /**
    * @param key
    * @param value
    * @param expiryTime
    * 键值对有效期 毫秒
    * @return
    */
    public V put(K key, V value, long expiryTime) {
    expiryMap.put(key, System.currentTimeMillis() + expiryTime);
    return super.put(key, value);
    }
    public int size() {
    return entrySet().size();
    }
    public boolean isEmpty() {
    return entrySet().size() == 0;
    }
    public boolean containsValue(Object value) {
    if (value == null)
    return Boolean.FALSE;
    Set<java.util.Map.Entry<K, V>> set = super.entrySet();
    Iterator<java.util.Map.Entry<K, V>> iterator = set.iterator();
    while (iterator.hasNext()) {
    java.util.Map.Entry<K, V> entry = iterator.next();
    if (value.equals(entry.getValue())) {
    if (checkExpiry(entry.getKey(), false)) {
    iterator.remove();
    return Boolean.FALSE;
    } else
    return Boolean.TRUE;
    }
    }
    return Boolean.FALSE;
    }
    public Collection<V> values() {
    Collection<V> values = super.values();
    if (values == null || values.size() < 1)
    return values;
    Iterator<V> iterator = values.iterator();
    while (iterator.hasNext()) {
    V next = iterator.next();
    if (!containsValue(next))
    iterator.remove();
    }
    return values;
    }
    public V get(Object key) {
    if (key == null)
    return null;
    if (checkExpiry(key, true))
    return null;
    return super.get(key);
    }
    /**
    *
    * @Description: 是否过期
    * @param key
    * @return null:不存在或key为null -1:过期 存在且没过期返回value 因为过期的不是实时删除,所以稍微有点作用
    */
    public Object isInvalid(Object key) {
    if (key == null)
    return null;
    if (!expiryMap.containsKey(key)) {
    return null;
    }
    long expiryTime = expiryMap.get(key);
    boolean flag = System.currentTimeMillis() > expiryTime;
    if (flag) {
    super.remove(key);
    expiryMap.remove(key);
    return -1;
    }
    return super.get(key);
    }
    public void putAll(Map<? extends K, ? extends V> m) {
    for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
    expiryMap.put(e.getKey(), System.currentTimeMillis() + EXPIRY);
    super.putAll(m);
    }
    public Set<Map.Entry<K, V>> entrySet() {
    Set<java.util.Map.Entry<K, V>> set = super.entrySet();
    Iterator<java.util.Map.Entry<K, V>> iterator = set.iterator();
    while (iterator.hasNext()) {
    java.util.Map.Entry<K, V> entry = iterator.next();
    if (checkExpiry(entry.getKey(), false))
    iterator.remove();
    }
    return set;
    }
    /**
    *
    * @Description: 是否过期
    * @author: qd-ankang
    * @date: 2016-11-24 下午4:05:02
    * @param expiryTime
    * true 过期
    * @param isRemoveSuper
    * true super删除
    * @return
    */
    private boolean checkExpiry(Object key, boolean isRemoveSuper) {
    if (!expiryMap.containsKey(key)) {
    return Boolean.FALSE;
    }
    long expiryTime = expiryMap.get(key);
    boolean flag = System.currentTimeMillis() > expiryTime;
    if (flag) {
    if (isRemoveSuper)
    super.remove(key);
    expiryMap.remove(key);
    }
    return flag;
    }
    /**
    * 删除
    * @param key
    */
    public void del(Object key){
    super.remove(key);
    expiryMap.remove(key);
    }
    public static void main(String[] args) throws InterruptedException {
    ExpiryMap<String, String> map = new ExpiryMap<>(10);
    map.put("test", "ankang");
    map.put("test1", "ankang");
    map.put("test2", "ankang", 3000);
    System.out.println("test1" + map.get("test"));
    Thread.sleep(1000);
    System.out.println("isInvalid:" + map.isInvalid("test"));
    System.out.println("size:" + map.size());
    System.out.println("size:" + ((HashMap<String, String>) map).size());
    for (Map.Entry<String, String> m : map.entrySet()) {
    System.out.println("isInvalid:" + map.isInvalid(m.getKey()));
    map.containsKey(m.getKey());
    System.out.println("key:" + m.getKey() + " value:" + m.getValue());
    }
    System.out.println("test1" + map.get("test"));
    }
    /**
    * 是否超过过期的一半时间
    * @param key
    * @return
    */
    public boolean isHalfExpiryTime(Object key ){
    if (!expiryMap.containsKey(key)) {
    return false;
    }
    long expiryTime = expiryMap.get(key);
    boolean flag = System.currentTimeMillis()-(expiryTime-NettyGoConstant.LOGINSESSIONTIMEOUT)>=NettyGoConstant.LOGINSESSIONTIMEOUT/2;
    return flag;
    }
    }

     

  • 相关阅读:
    DataGrid
    取整、取小数点位数
    如何跨浏览器使用连续字符的换行
    如何给 legend 标签设定宽度
    25个简洁优美的网站设计
    重新发现HTML表格
    用户研究角度看设计(2):用户为何视若无睹
    lineheight 属性的继承问题
    jQuery技巧总结
    web2.0网站配色方案
  • 原文地址:https://www.cnblogs.com/tohxyblog/p/8426230.html
Copyright © 2011-2022 走看看