单点登录SSO,JWT实现无状态登录,自定义网关过滤器处理登录验证

1 单点登录(SSO)

SSO英文全称Single Sign On,单点登录。
SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。在这里插入图片描述

1.1 Cookie问题

电商平台通常由多个微服务组成,每个微服务都有独立的域名,而cookie是有作用域的。
查看浏览器控制台:在这里插入图片描述
domain:作用域名,

domain参数 gulimall.com search.gulimall.com item.gulimall.com
gulimall.com
search.gulimall.com × ×
item.gulimall.com × ×

domain有两点要注意:

1. **domain参数可以设置父域名以及自身,但不能设置其它域名,包括子域名,否则cookie不起作用。**
2. **cookie的作用域是domain本身以及domain下的所有子域名。**

Cookie的路径(Path):
response.addCookie默认放在当前路径下,访问当前路径下的所有请求都会带设置/标识项目根路径,访问项目任何位置都会携带。

1.2 有状态登录

为了保证客户端cookie的安全性,服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。
例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
缺点是什么?
服务端保存大量数据,增加服务端压力
服务端保存用户状态,无法进行水平扩展
客户端请求依赖服务端,多次请求必须访问同一台服务器
即使使用redis保存用户的信息,也会损耗服务器资源。

1.3 无状态登录

微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:
服务端不保存任何客户端请求者信息
客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份。
带来的好处是什么呢?
客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
服务端的集群和状态对客户端透明
服务端可以任意的迁移和伸缩
减小服务端存储压力

1.4 无状态登录流程

无状态登录的流程:
当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
以后每次请求,客户端都携带认证的token
服务的对token进行解密,判断是否有效。
流程图:在这里插入图片描述
整个登录过程中,最关键的点是什么?
token的安全性
token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。
采用何种方式加密才是安全可靠的呢?
我们将采用JWT + RSA非对称加密。

2 JWT实现无状态登录

JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;

官网:https://jwt.io

GitHub上jwt的java客户端:https://github.com/jwtk/jjwt

2.1 数据格式

JWT包含三部分数据:
Header:头部,通常头部有两部分信息:
token类型:JWT
加密方式:base64(HS256)
Payload:载荷,就是有效数据,一般包含下面信息:
用户身份信息(注意,这里因为采用base64编码,可解码,因此不要存放敏感信息)
注册声明:如token的签发时间,过期时间,签发人等
这部分也会采用base64编码,得到第二部分数据
Signature:签名,是整个数据的认证信息。根据前两步的数据,再加上指定的密钥(secret)(不要泄漏,最好周期性更换),通过base64编码生成。用于验证整个数据完整和可靠性。在这里插入图片描述

2.2 JWT交互流程

流程图:在这里插入图片描述
步骤翻译:
1.用户登录
2.服务的认证,通过后根据secret生成token
3.将生成的token返回给浏览器
4.用户每次请求携带token
5.服务端利用公钥解读jwt签名,判断签名有效后,从Payload中获取用户信息
6.处理请求,返回响应结果
因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,完全符合了Rest的无状态规范。

2.3 非对称加密

加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:
对称加密,如AES
基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所
有的分组密文进行合并,形成最终的密文。
优势:算法公开、计算量小、加密速度快、加密效率高
缺陷:双方都使用同样密钥,安全性得不到保证
非对称加密,如RSA
基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
私钥加密,持有公钥才可以解密
公钥加密,持有私钥才可解密
优点:安全,难以破解
缺点:算法比较耗时
不可逆加密,如MD5,SHA
基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这
种加密后的数据是无法被解密的,无法根据密文推算出明文。
RSA算法历史:
1977年,三位数学家Rivest、Shamir和Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA

3 搭建授权中心

在这里插入图片描述
用户鉴权:
接收用户的登录请求,通过用户中心的接口进行校验,通过后生成JWT
使用私钥生成JWT并返回。
3.1 JWT工具类
gulimall-common工程中已经封装了jwt相关的工具类:
JwtUtils
RsaUtils
CookUtils
并在gulimall-common中的pom.xml中引入了jwt相关的依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>joda-time</groupId>
    <artifactId>joda-time</artifactId>
    <version>2.10.9</version>
</dependency>

如图所示:
在这里插入图片描述
1.编写JwtUtils的代码如下:

package com.txw.common.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.joda.time.DateTime;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Map;
/**
 * JWT工具类 {@link JwtUtils}
 * @author Adair
 * E-mail: [email protected]
 */
@SuppressWarnings("all")  // 注解警告信息
public class JwtUtils {
   

    /**
     * 私钥加密token
     *
     * @param map           载荷中的数据
     * @param expireMinutes 过期时间,单位秒
     * @return
     * @throws Exception
     */
    public static String generateToken(Map<String, Object> map, PrivateKey key, int expireMinutes) throws Exception {
   
        return Jwts.builder()
                .setClaims(map)
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(key, SignatureAlgorithm.RS256)
                .compact();
    }

    /**
     * 公钥解析token
     *
     * @param token  用户请求中的token
     * @return
     * @throws Exception
     */
    private static Jws<Claims> parserToken(String token, PublicKey key) {
   
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
    }

    /**
     * 获取token中的用户信息
     *
     * @param token  用户请求中的令牌
     * @return 用户信息
     * @throws Exception
     */
    public static Map<String, Object> getInfoFromToken(String token, PublicKey key) throws Exception {
   
        Jws<Claims> claimsJws = parserToken(token, key);
        return claimsJws.getBody();
    }
}

如图所示:在这里插入图片描述
2.编写RsaUtils的代码如下:

package com.txw.common.utils;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
 * RSA加密工具类 {@link RsaUtils}
 * @author Adair
 * E-mail: [email protected]
 */
@SuppressWarnings("all")  // 注解警告信息
public class RsaUtils {
   

    /**
     * 从文件中读取公钥
     *
     * @param filename 公钥保存路径,相对于classpath
     * @return 公钥对象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
   
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     * 从文件中读取密钥
     *
     * @param filename 私钥保存路径,相对于classpath
     * @return 私钥对象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
   
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取公钥
     *
     * @param bytes 公钥的字节形式
     * @return
     * @throws Exception
     */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception {
   
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 获取密钥
     *
     * @param bytes 私钥的字节形式
     * @return
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
   
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文,生存rsa公钥和私钥,并写入指定文件
     *
     * @param publicKeyFilename  公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret             生成密钥的密文
     * @throws IOException
     * @throws NoSuchAlgorithmException
     */
    public static void generateKey(String publicKeyFilename, String privateKeyFilename, String secret) throws Exception {
   
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(2048, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        // 获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        // 获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String fileName) throws Exception {
   
        return Files.readAllBytes(new File(fileName).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException {
   
        File dest = new File(destPath);
        if (!dest.exists()) {
   
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }
}

如图所示:在这里插入图片描述
3.编写CookieUtils的代码如下:

package com.txw.common.utils;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
 * Cook工具类 {@link CookieUtils}
 * @author Adair
 * E-mail: [email protected]
 */
@SuppressWarnings("all")  // 注解警告信息
public class CookieUtils {
   
    static final Logger logger = LoggerFactory.getLogger(CookieUtils.class);

    /**
     * 得到Cookie的值, 不编码
     *
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName) {
   
        return getCookieValue(request, cookieName, false);
    }

    /**
     * 得到Cookie的值,
     *
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
   
        Cookie[] cookieList = request.getCookies
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学无止路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值