牛骨文教育服务平台(让学习变的简单)
博文笔记

SSO-Token,服务器端实现

创建时间:2015-06-15 投稿人: 浏览次数:4929

最近在项目中遇到了一个单点登录的问题,就做了一个研究了下并做了实验,并分享给大家,有意见请指正~~~~

关于SSO的定义等一些相关的问题请自行百度,这里只说实现~~~~~~

首先呢,我做的SSO的实现在验证等操作全部是在服务器端实现的:

思路如下:

1、用户登录后,生成一个16位长度的Token字符串

2、将Token发回到客户端,在服务器端,将生成的Token和用户信息放到一个全局唯一的存储结构中

3、设置Token的过期时间和最后一次操作时间

4、每次用户进行操作时,验证Token是否过期,如过期要求重新登录,否则直接操作,并更新最近一次操作时间

5、后台线程定期检测Token的存储结构,将过期的Token移除

行了,直接来看实现吧:

我们将过期时间间隔,以及Token的字符串长度的设置都在配置文件中:如下SystemComfig.properties

TOKENTIMEOUT=1   设置的Token的过期时间,这里用的1分钟,方便实验,根据系统自行设定即可
TOKEN_BYTE_LEN=12  8--->10; 10--->14; 12--->16 具体的在实验的时候看下就可以了

解析配置文件的类:SystemConfigConstants.java

public class SystemConfigConstants {
	//Token过期时间
	public static final int TOKENTIMEOUT;
	//用于生成的Token长度
	public static final int TOKEN_BYTE_LEN;
	
	/*
	 * 取值过程
	 */
	static{
		//配置文件读取
		InputStream stream = null;
		//读取工具
		Properties properties = null;
		
		try {
			stream = SystemConfigConstants.class.getResourceAsStream("SystemComfig.properties");
			properties = new Properties();
			properties.load(stream);
		} catch (IOException e) {
			e.printStackTrace();
		}

		TOKENTIMEOUT = Integer.parseInt(properties.getProperty("TOKENTIMEOUT"));
		TOKEN_BYTE_LEN = Integer.parseInt(properties.getProperty("TOKEN_BYTE_LEN"));
	}
}

下面就是重点了:

我们需要去生成我们想要的长度的Token字符串,我这里使用的是使用是UUID,因为这个可以保证生成的字符串的不会重复的,但是这个太长了128bit呢,所有觉得太长了

所有我又使用了Base64算法对UUID字符串进行处理,至于UUID是啥,请Google, 所依赖的包:commons-codec-1.10.jar

下面看下具体的实现吧:TokenGenerateUtit.java

public class TokenGenerateUtil {

    //用于外部调用生成Token字符串的静态方法
    public static String getToken(){
        UUID uuid = UUID.randomUUID();
        String token = compressedUUID(uuid);
        //验证是否有相同的Token,为true重新生成
        if(TokenManager.DATA_MAP.containsKey(token)){
            uuid = UUID.randomUUID();
            token = compressedUUID(uuid);
        }
        return token;
    }

    //对UUID进行处理,形成想要的Token长度
    private static String compressedUUID(UUID uuid) {
        byte[] byUuid = new byte[SystemConfigConstants.TOKEN_BYTE_LEN];
        long least = uuid.getLeastSignificantBits();
        long most = uuid.getMostSignificantBits();
        long2bytes(most, byUuid, 0);
        long2bytes(least, byUuid, SystemConfigConstants.TOKEN_BYTE_LEN/2);
        String compressUUID = Base64.encodeBase64URLSafeString(byUuid);
        return compressUUID;
    }

    //长度处理
    private static void long2bytes(long value, byte[] bytes, int offset) {
        for (int i = SystemConfigConstants.TOKEN_BYTE_LEN/2-1; i > -1; i--) {
            bytes[offset++] = (byte) ((value >> 8 * i) & 0xFF);
        }
    }
}
虽然呢UUID可以保证的是唯一的,但是经过Base64处理后,为了不出现重复的情况(在存储结构中,token字符串是作为map的key的,所以不能重复),我在

生成时加了一个检测过程。

下面就是对Token的管理了:先看代码:TokenManager.java

public class TokenManager {

    //计时器,间隔一定的时间执行一次,设置为守护线程设true
    private static final Timer timer = new Timer(true);

    //构造函数私有化防止静态类被实例化  使用这种方式保障全局唯一性
    private TokenManager() {
    }

    //复合结构体,含登录的User和过期时间expried两个成员以及最后一次操作的时间
    private static class Token {
        private String userName;//登录用户名
        private Date expired; //过期时间
        private Date lastOperate; // 最近一次操作的时间
    }

    //令牌存储结构 ConcurrentHashMap:支持同步的HashMap
    public static final Map<String, Token> DATA_MAP = new ConcurrentHashMap<String, Token>();

    /**
     * 令牌有效性验证
     */
    public static boolean validate(String vt) {
        System.out.println("验证Token是否有效.......");
        boolean isValid = true;
        if(DATA_MAP.containsKey(vt)){
            Date expired = DATA_MAP.get(vt).expired;
            Date now = new Date();
            if(now.compareTo(expired) > 0){//已过期
                isValid = false;
                DATA_MAP.remove(vt);//移除
            }
        }else{
            isValid = false;
        }
        return isValid;
    }

    /**
     * 用户授权成功后存入授权信息
     */
    public static void addToken(String vt, String userName) {
        Token token = new Token();

        token.userName = userName;
        token.lastOperate = new Date();
        token.expired = new Date(token.lastOperate.getTime() + SystemConfigConstants.TOKENTIMEOUT * 60 * 1000);

        DATA_MAP.put(vt, token);
    }

    /**
     * 更新最近一次操作的时间
     */
    public static void updateLastOperate(String vt) {
        Token token = DATA_MAP.get(vt);
        token.lastOperate = new Date(new Date().getTime());//更新最近时间
        token.expired = new Date(token.lastOperate.getTime() + SystemConfigConstants.TOKENTIMEOUT * 60 * 1000); //更新过期时间
    }

    /**
     * 计时器实现
     */
    static {
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                for (Entry<String, Token> entiy : DATA_MAP.entrySet()) {
                    String vt = entiy.getKey();
                    Token token = entiy.getValue();

                    Date expired = token.expired;
                    Date now = new Date();
                    //当前时间大于过期时间
                    //当前时间大于过期时间
                    //标识已经过期时间
                    //将token移除
                    if (now.compareTo(expired) > 0) {
                        //已过期,清除
                        DATA_MAP.remove(vt);
                    }
                }
            }
            //第一个为线程启动后间隔多久启动第一次执行,第二个是两次执行的间隔时间
        }, 0 * 1000, 5 * 1000);
    }

    /**
     * 在系统启动时启动管理工具
     */
    public static void init(){ }
}
可以看到有一个Timer以守护线程的方式,运行,对存储结构进行维护,对已经过期的Token移除。

当每次操作的时候,对最近一次操作的时间进行更新,因为对Token过期的检测的线程是有时间间隔的,所以为了保证Token的有效性,每次操作前的Token有效性检测时也会检测一次是否过期。

使用的过程就是前面的思路了。

就到这吧,基本都讲完了,有的实现是把最近一次操作的时间更新和过期性检测放在客户端,觉得没必要,这样可以减少网络交互次数,也方便。

有更好的实现和想法,欢迎一起来讨论下啦,觉得有问题的欢迎指正~~~~~~~~



声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。