JWT+RSA 无状态SSO原理

一 JWT+RSA 无状态SSO原理

1.1.有状态登陆

有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。

例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。

  1. 服务端保存大量数据,增加服务端压力
  2. 服务端保存用户状态,无法进行水平扩展
  3. 客户端请求依赖服务端,多次请求必须访问同一台服务器

1.2.无状态登陆

微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服 务的无状态性,即:服务端不保存任何客户端请求者信息,客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份

  1. 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
  2. 服务端的集群和状态对客户端透明
  3. 服务端可以任意的迁移和伸缩
  4. 减小服务端存储压力

1.3 RSA加密

RSA称为非对称加密,加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密 文)其逆过程就是解码(解密),加密技术的要点是加密算法,RSA会根据你给的盐值生成私钥和公钥:

  1. 私钥:通过私钥加密的数据使用私钥或者公钥来解密。
  2. 公钥:通过公钥加密的数据只能使用私钥来解密。
  3. 优点:安全,难以破解
  4. 缺点:算法比较耗时

1.4 JWT

JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的 Web应用授权。看名知意,其实就是一种加密方式,分为三部分:

Header(头部):一般只包含两部分信息:

声明加密的算法,这里使用的是HS256的加密算法。声明Token的类型,这里使用的是JWT的风格类型。

Payload(载荷):存放一些有效数据比如用户ID、用户名称,解密之后可以获取载荷中的用户信息,因为采用Base64编码格式,所以可以被解码,不要放敏感信息,例如登录密码之类的。

Signature(签名):Signature由header和payload经过 base64 编码后加 盐值得到的。生成Signature的算法如下

var encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload);

HMACSHA256(encodedString, '!Q@#$%^%&');

JjwtUtil.java
public class JjwtUtil {
    // jti:jwt的唯一身份标识
    public static final String JWT_ID = UUID.randomUUID().toString();

    // 加密密文,私钥
    public static final String JWT_SECRET = "jiamimiwen";

    // 过期时间,单位毫秒
    public static final int EXPIRE_TIME = 60 * 60 * 1000; // 一个小时
//	public static final long EXPIRE_TIME = 7 * 24 * 3600 * 1000; // 一个星期

    // 由字符串生成加密key
    public static SecretKey generalKey() {
        // 本地的密码解码
        byte[] encodedKey = Base64.decodeBase64(JWT_SECRET);
        // 根据给定的字节数组使用AES加密算法构造一个密钥
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    // 创建jwt
    public static String createJWT(String issuer, String audience, String subject) throws Exception {
        // 设置头部信息
		Map<String, Object> header = new HashMap<String, Object>();
		header.put("typ", "JWT");
		header.put("alg", "HS256");
        // 或
        // 指定header那部分签名的时候使用的签名算法,jjwt已经将这部分内容封装好了,只有{"alg":"HS256"}
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证的方式)
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", "admin");
        claims.put("password", "010203");
        // jti用户id,例如:20da39f8-b74e-4a9b-9a0f-a39f1f73fe64
        String jwtId = JWT_ID;
        // 生成JWT的时间
        long nowTime = System.currentTimeMillis();
        Date issuedAt = new Date(nowTime);
        // 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露,是你服务端的私钥,在任何场景都不应该流露出去,一旦客户端得知这个secret,那就意味着客户端是可以自我签发jwt的
        SecretKey key = generalKey();
        // 为payload添加各种标准声明和私有声明
        JwtBuilder builder = Jwts.builder() // 表示new一个JwtBuilder,设置jwt的body
				.setHeader(header) // 设置头部信息
                .setClaims(claims) // 如果有私有声明,一定要先设置自己创建的这个私有声明,这是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明
                .setId(jwtId) // jti(JWT ID):jwt的唯一身份标识,根据业务需要,可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击
                .setIssuedAt(issuedAt) // iat(issuedAt):jwt的签发时间
                .setIssuer(issuer) // iss(issuer):jwt签发者
                .setSubject(subject) // sub(subject):jwt所面向的用户,放登录的用户名,一个json格式的字符串,可存放userid,roldid之类,作为用户的唯一标志
                .signWith(signatureAlgorithm, key); // 设置签名,使用的是签名算法和签名使用的秘钥
        // 设置过期时间
        long expTime = EXPIRE_TIME;
        if (expTime >= 0) {
            long exp = nowTime + expTime;
            builder.setExpiration(new Date(exp));
        }
        // 设置jwt接收者
        if (audience == null || "".equals(audience)) {
            builder.setAudience("Tom");
        } else {
            builder.setAudience(audience);
        }
        return builder.compact();
    }

    // 解密jwt
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey key = generalKey(); // 签名秘钥,和生成的签名的秘钥一模一样
        Claims claims = Jwts.parser() // 得到DefaultJwtParser
                .setSigningKey(key) // 设置签名的秘钥
                .parseClaimsJws(jwt).getBody(); // 设置需要解析的jwt
        return claims;
    }

}
JWTTest.java
public static void main(String[] args) {
        User user = new User();
        user.setAge(1);
        user.setName("张三");
        // jwt所面向的用户,放登录的用户名等
        String subject = JSON.toJSONString(user);
        try {
            // "Jack"是jwt签发者,"李四"是jwt接收者
            String jwt = JjwtUtil.createJWT("Jack", "李四", subject);
            System.out.println("JWT:" + jwt);
            System.out.println("JWT长度:" + jwt.length());
            System.out.println("\njwt三个组成部分中间payload部分的解密:");
            Claims c = JjwtUtil.parseJWT(jwt);
            System.out.println("jti用户id:" + c.getId());
            System.out.println("iat登录时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(c.getIssuedAt()));
            System.out.println("iss签发者:" + c.getIssuer());
            System.out.println("sub用户信息列表:" + c.getSubject());
            System.out.println("exp过期时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(c.getExpiration()));
            System.out.println("aud接收者:" + c.getAudience());
            System.out.println("登录的用户名:" + c.get("username"));
            // 或
            System.out.println("登录的用户名:" + c.get("username", String.class));
            System.out.println("登录的密码:" + c.get("password", String.class));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
JWT:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ7XCJhZ2VcIjoxLFwibmFtZVwiOlwi5byg5LiJXCJ9IiwiYXVkIjoi5p2O5ZubIiwicGFzc3dvcmQiOiIwMTAyMDMiLCJpc3MiOiJKYWNrIiwiZXhwIjoxNjgzNjc3MTUwLCJpYXQiOjE2ODM2NzM1NTAsImp0aSI6ImYwNGEwZTkyLThkY2YtNGFjOC1iNzJmLWYwMGIzYzc1ZThhYSIsInVzZXJuYW1lIjoiYWRtaW4ifQ.htyiHnR3zPbufvF33Z8qT1c1UJOeMmZD_kVFAl_I2YQ
JWT长度:331

jwt三个组成部分中间payload部分的解密:
jti用户id:f04a0e92-8dcf-4ac8-b72f-f00b3c75e8aa
iat登录时间:2023-05-10 07:05:50
iss签发者:Jack
sub用户信息列表:{"age":1,"name":"张三"}
exp过期时间:2023-05-10 08:05:50
aud接收者:李四
登录的用户名:admin
登录的用户名:admin
登录的密码:010203

 分布式下JWT SSO的过程

1 访问网关,查看是否有token,如果没有教跳到登陆页面

2 当用户登录的时候,访问网关再调用授权中心进行授权,通过JWT生成token放入网关的redis中或者cookies中,然后把token返回给页面,再根据记录下来的请求地址,跳转到对应的页面.

3 用户再访问其他系统的时候,调用网关,然后网关获取token,如果不存在token就直接调跳转登陆页面,如果token存在,就调用授权中心进行验证,如果验证token存在,返回用户信息给到网关。如果token不存在就跳转登陆页面。

 JWT+RSA SSO的过程

3.1 RSA的意义

可以发现,每次鉴权都需要访问鉴权中心,系统间的网络请求频率过高,效率略差,鉴权中心的压力较大。这时就用到了前面说的RSA,我们可以把私钥留在授权中心,把公钥给网关或者其他微服务,那么就可以在网关或其他微服务当中直接解密JWT了,这样做的好处是减少了授权中心的压力。

3.2 jwt+rsa 登录过程

 1 如上访问网关未登录就跳到登陆页面。

2登录的时候同上访问授权中心授权,授权中心验证完用户信息以后,通过私钥对生成jwt的token信息。然后将token信息写入redis或者cookie带回页面。

 

 3 如上所示,客户端再访问其他资源的时候,就不需要再去调用授权服务器了,而是获取token信息,直接通过公钥进行解密就好了。当然如果没有token的话还是跳转到登陆页面。

四 无状态登录实现

  4.1 设计图

     4.1.1 交互流程

      

 

     4.1.2 交互平面

 4.2 核心代码片段

  4.2.1 risk-sso 服务

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @since 2021-04-09
 */
@Service
public class WxbMemeberServiceImpl extends ServiceImpl<WxbMemeberMapper, WxbMemeber> implements IWxbMemeberService {

    @Autowired
    private WxbMemeberMapper wxbMemeberMapper;

    @Resource
    private RedisTemplate redisTemplate;

    @Autowired
    private RestTemplate restTemplate;


    @Override
    public Result login(WxbMemeber memeber) {
        String name = memeber.getName();
        String password = memeber.getPassword();

        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        LambdaQueryWrapper<WxbMemeber> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(WxbMemeber::getName, name);
        WxbMemeber wxbMemeber = wxbMemeberMapper.selectOne(lambdaQueryWrapper);
        if (wxbMemeber == null) {
            return Result.error(1008, "用户不存在,请注册!", null);
        }
        String dbPassWord = wxbMemeber.getPassword();
        if (!bCryptPasswordEncoder.matches(password, dbPassWord)) {
            return Result.error(1008, "用户名 密码输入错误!", null);
        }
        //获取私钥
        String token = getLoginToken(wxbMemeber);
        return Result.ok(token, "success");
    }

    /**
     * 获取登录用户的token
     *
     * @param wxbMemeber
     * @return
     */
    public String getLoginToken(WxbMemeber wxbMemeber) {
        PrivateKey privateKey = getPrivateKey();
        wxbMemeber.setPassword(""); //新密设置null
        String token = JwtUtils.generateTokenExpireInMinutes(wxbMemeber, privateKey, 60);
        String redisKey = getLoginRedisKey(wxbMemeber);
        redisTemplate.opsForValue().set(redisKey, token, 30, TimeUnit.MINUTES);
        return token;
    }


    @Override
    public Result wxLogin(HttpServletRequest request, HttpServletResponse response) {
        //获取回调地址中的code
        String code = request.getParameter("code");
        //拼接url
        String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + WxConstant.APPID + "&secret="
                + WxConstant.APPSECRET + "&code=" + code + "&grant_type=authorization_code";

        JSONObject jsonObject = restTemplate.getForObject(url, JSONObject.class);
        //1.获取微信用户的openid
        String openid = jsonObject.getString("openid");
        //2.获取获取access_token
        String access_token = jsonObject.getString("access_token");
        String infoUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=" + access_token + "&openid=" + openid
                + "&lang=zh_CN";
        //3.获取微信用户信息
        WxUser userInfo = restTemplate.getForObject(infoUrl, WxUser.class);
        //至此拿到了微信用户的所有信息,剩下的就是业务逻辑处理部分了
        //保存openid和access_token到session
        request.getSession().setAttribute("openid", openid);
        request.getSession().setAttribute("access_token", access_token);

        LambdaQueryWrapper<WxbMemeber> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(WxbMemeber::getOpenId, openid);
        List<WxbMemeber> wxbMemebers = wxbMemeberMapper.selectList(lambdaQueryWrapper);
        if (CollectionUtils.isEmpty(wxbMemebers)) {
            /**
             * 没有查到跳转到注册页面
             */
            return Result.error(500, "用户不存在,请重新注册!", null);
        }
        WxbMemeber wxbMemeber = wxbMemebers.get(0);
        BeanUtils.copyProperties(userInfo, wxbMemeber);
        this.saveOrUpdate(wxbMemeber);
        wxbMemeber.setPassword("");

        String token = getLoginToken(wxbMemeber);
        return Result.ok(token, "success");
    }

    /**
     * 获取登录用户名 密码
     *
     * @param wxbMemeber
     * @return
     */
    public String getLoginRedisKey(WxbMemeber wxbMemeber) {
        String pattern = "l_g:%s:%s";
        String memeberId = wxbMemeber.getMemeberId();
        String dateDay = DateFormatUtils.format(new Date(), "yyyyMMdd");
        return String.format(pattern, dateDay, memeberId);
    }

    private PrivateKey getPrivateKey() {
        PrivateKey privateKey = null;
        try {
            privateKey = RsaUtils.getPrivateKey(ResourceUtils.getFile("classpath:rsa").getPath());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return privateKey;
    }
}

4.2.2 risk-gateway

@Component
@Slf4j
@RefreshScope
public class GlobleAuthFilter implements GlobalFilter, Ordered {

    @Resource
    private RedisTemplate redisTemplate;

    @Value("${filter.url.pre:/login}")
    private String filterUrl;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        URI uri = request.getURI();
        String path = uri.getPath();
        if (!checkIsNeedLogin(path)) {
            return chain.filter(exchange);
        }

        //1 获取token
        HttpHeaders headers = request.getHeaders();
        String token = headers.getFirst("token");
        if (StringUtils.isBlank(token)) {
            return response(response, R.failed("无效token!"));
        }

        PublicKey publicKey = getPublicKey();
        //2 对token进行公钥解密
        WxbMemeber wxbMemeber = (WxbMemeber) JwtUtils.getInfoFromToken(token, publicKey, WxbMemeber.class);
        //3 解密以后获取用户的id
        String loginRedisKey = getLoginRedisKey(wxbMemeber);
        String tokens = (String) redisTemplate.opsForValue().get(loginRedisKey);
        if (StringUtils.isBlank(tokens)) {
            return response(response, R.failed("登录超时,请重新登录!"));
        }
        if (!token.equals(tokens)) {
            return response(response, R.failed("异地登录,请重新登录!"));
        }
        //4 蓄一下超时时间
        redisTemplate.opsForValue().set(loginRedisKey, token, 30, TimeUnit.MINUTES);
        //5 把解密后的用户信息放入请求头继续往下传递
        ServerHttpRequest.Builder mutate = request.mutate();
        mutate.header("token", JSONObject.toJSONString(wxbMemeber));
        ServerWebExchange.Builder webexcahnge = exchange.mutate();
        ServerWebExchange newServerWebExchange = webexcahnge.request(mutate.build()).build();
        return chain.filter(newServerWebExchange);
    }

    public boolean checkIsNeedLogin(String path) {
        String[] split = filterUrl.split(",");
        if (split.length > 0) {
            for (String sp : split) {
                if (path.contains(sp)) {
                    return false;
                }
            }
        }
        return true;
    }

    public String getLoginRedisKey(WxbMemeber wxbMemeber) {
        String pattern = "l_g:%s:%s";
        String memeberId = wxbMemeber.getMemeberId();
        String dateDay = DateFormatUtils.format(new Date(), "yyyyMMdd");
        return String.format(pattern, dateDay, memeberId);
    }


    private PublicKey getPublicKey() {
        PublicKey privateKey = null;
        try {
            privateKey = RsaUtils.getPublicKey(ResourceUtils.getFile("classpath:rsa.pub").getPath());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return privateKey;
    }

    private Mono<Void> response(ServerHttpResponse response, R res) {
        //不能放行,直接返回,返回json信息
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");

        ObjectMapper objectMapper = new ObjectMapper();
        String jsonStr = null;
        try {
            jsonStr = objectMapper.writeValueAsString(res);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }

        DataBuffer dataBuffer = response.bufferFactory().wrap(jsonStr.getBytes());
        return response.writeWith(Flux.just(dataBuffer));//响应json数据
    }

    @Override
    public int getOrder() { //作为第一个全局过滤器
        return 0;
    }
}
/**
 * gateway跨域配置
 */
@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}

4.2.3 risk-order 

package worn.xiao.order.interceptor;

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import worn.xiao.entity.WxbMemeber;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

@Slf4j
public class UserInfoInterceptor implements HandlerInterceptor {
    private static final ThreadLocal<WxbMemeber> loginUser=new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");
        if(StringUtils.isBlank(token)){
            log.error("preHandle token StringUtils.isBlank(token)");
            return false;
        }
        WxbMemeber wxbMemebers = JSONObject.parseObject(token, WxbMemeber.class);
        loginUser.set(wxbMemebers);
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        loginUser.remove();
    }

    public static WxbMemeber getLoginUser(){
        return loginUser.get();
    }
}

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值