SSO-Token,服务器端实现
最近在项目中遇到了一个单点登录的问题,就做了一个研究了下并做了实验,并分享给大家,有意见请指正~~~~
关于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有效性检测时也会检测一次是否过期。
使用的过程就是前面的思路了。
就到这吧,基本都讲完了,有的实现是把最近一次操作的时间更新和过期性检测放在客户端,觉得没必要,这样可以减少网络交互次数,也方便。
有更好的实现和想法,欢迎一起来讨论下啦,觉得有问题的欢迎指正~~~~~~~~
- 上一篇: 基于cookie-redis实现单点登录的原理浅谈
- 下一篇: 基于 Token 的身份验证