sso与cookie登录那些事儿

一 SSO/CAS基础知识
CAS(Central Authentication Service)是Yale大学的一个开源项目,旨在为Web应用系统提供一种可靠的单点登录解决方法,CAS实现SSO过程如图(图片来源于网络)
sso与cookie登录那些事儿_第1张图片
实现步骤
1. 访问服务:SSO客户端发送请求访问应用系统提供的服务资源
2. 定向认证:SSO客户端会重定向用户请求到SSO服务器
3. 用户认证:用户身份认证
4. 发放票据:SSO服务器会产生一个随机的Service Ticket
5. 验证票据:SSO服务器验证票据Service Ticket的合法性,验证通过后允许客户端访问服务
6. 传输用户信息:SSO服务器验证票据通过后,传输用户认证结果信息给客户端

二 SSO的一般实现
假如现有a、b、c共3个系统,域名分别是http://a.com、http://b.com、 http://c.com
a、b是需要登录的子系统,即对应CAS client,c是单点登录系统,即对应CAS Server

1. 系统a拦截需要登录的页面,如果没有ticket(cookie)则重定向到系统c登录并传递参数returnURL http://c.com/login.do?returnUrl=http%3A%2F%2Fa.com
2. 如果登录验证成功,系统c生成ticket并写入cookie到域名http://c.com下,重定向到http://a.com并传递ticket,系统a写入cookie到域名http://a.com下
3. 用户访问系统a的时候,从http request获取ticket并调用系统c提供的方法验证合法性
4. 用户在浏览器输入http://b.com,由于b.com域名下没有cookie,会重定向到http://c.com的登录页面,由于c.com已经写入了cookie,且是合法未过期的,验证通过,然后重定向回到http://b.com,此时返回参数同样传递ticket,写入http://b.com域名下。期间浏览器会出现2次跳转

虽然也不需要用户登录,但体验很不好,我们可以规划好域名避免两次跳转
浏览器请求子域会带上父域的cookie,反之不会。
我们把3个系统的域名改为:http://a.test.com、http://b.test.com、http://sso.test.com
cookie统一设置到父域http://test.com下面,a.test.com、b.test.com均不要设置cookie,这样即可避免两次跳转问题

有同学可能问:步骤3中能否在本地缓存cookie,不每次都调用c去校验是否登录?
一般不建议这样做,对比下流行的SOA架构这点调用不算什么,除非你的登录系统已经成为瓶颈。
如果这样做,你需要解决某子系统退出时一并清理其他所有系统的cookie,否则出现某系统还是登录中的情况。

三 基于Cookie的登录验证
分布式环境下的登录session常见处理方式:
1. 使用应用服务器提供的session复制,实际使用较少再此不做不讨论
2. 使用redis等集中式缓存,比较简单再次不在讨论
3. 使用cookie记录登录状态,下文仅对此方案讨论

由于记录了登录状态,cookie安全就成为重点考虑的对象,cookie设置为httponly禁止js读取,如果网站有启用https,则设置cookie的secure属性为true

cookie使用AES+MD5双重加密,下面以FormTicket对象设置到cookie中为例介绍加密过程
public class FormTicket {
    private int version = 0;
    private String userName;
    private Date issueDate;
    private Date expires;
    
    public byte[] toBytes() {
        int pos = 0;
        byte verBytes = (byte)version;
        byte[] userBytes = userName.getBytes();
        byte[] issueDateBytes = date2Bytes(issueDate);
        byte[] expiresBytes = date2Bytes(expires);
        return Bytes.concat(new byte[]{verBytes}, userBytes, issueDateBytes, expiresBytes);
    }
    
    public FormTicket bytes2Ticket(byte[] bytes) {
        FormTicket ticket = new FormTicket();
        // ...
        return ticket;
    }
}

cookie里只记录用户名、登录状态等,不要有其他重要信息。后端代码解密cookie,来判断是否已登录。而不是从session查看是否存在,也不是从redis中检查是否已登录
加密过程:
public String genCookieStrs(FormTicket ticket) {
        String md5Salt = "xxx";
        String aesSalt = "yyy";
        byte[] ticketBytes = ticket.ticket2Bytes();
        
        // 使用md5生成消息签名,防止窜改
        byte[] md5SaltBytes = md5Salt.getBytes();
        byte[] allBytes = Bytes.concat(ticketBytes, md5SaltBytes);
        byte[] messageSignation = DigestUtils.md5(allBytes);
        
        //使用AES算法生成最终cookie串
        KeyGenerator kgen = KeyGenerator.getInstance("AES");
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed(aesSalt.getBytes());
        kgen.init(128, secureRandom);
        SecretKey secretKey = kgen.generateKey();
        byte[] enCodeFormat = secretKey.getEncoded();
        SecretKeySpec key = new SecretKeySpec(enCodeFormat, "AES");
        Cipher cipher = Cipher.getInstance("AES");
        byte[] byteContent = Bytes.concat(ticketBytes, messageSignation);
        cipher.init(Cipher.ENCRYPT_MODE, key);
        byte[] result = cipher.doFinal(byteContent);
        // byte数字转换为16进制字符串
        return new String(Hex.encodeHex(result));
    }


细心的同学可能发现:有时候AES加密后的字符串长得很像
public static void main(String[] args) {
        String src1 = "This is a test aes encrypt";
        String dst1 = encryptHexStr(src1, AES_SEED);
        System.out.println(dst1);
        String src2 = "This is a test aes...";
        String dst2 = encryptHexStr(src2, AES_SEED);
        System.out.println(dst2);
    }


输出结果:
f3d1fc25f6b87bebecbb57e91e456e2660e50abbacd46f64e7f1586e40df73d4
f3d1fc25f6b87bebecbb57e91e456e26fe114b1df530971a36db8d537f20d057

我们看到前32个字符完全一样,由于AES是分块加密,前16字节完全一样所以加密后也一样。
我们更改src2第16个字符看看效果:
String src2 = "This is a test Aes...";
运行结果:
f3d1fc25f6b87bebecbb57e91e456e2660e50abbacd46f64e7f1586e40df73d4
6ec4d23b68b8ce466285e6d033685e2dfe114b1df530971a36db8d537f20d057
这下就完全不一样了

附:AES加密模式与填充方式表
sso与cookie登录那些事儿_第2张图片


从cookie提前用户是否登录大致过程
1. AES解密cookie字符串
2. 验证消息摘要是否正确,防止被窜改
3. 转换为FormTicket对象,验证cookie是否过期时间

你可能感兴趣的:(web架构)