一.业务流程

1.使用RSA生成公钥和私钥。私钥保存在授权中心,公钥保存在网关(gateway)和各个信任微服务中。
2.用户请求登录。
3.授权中心进行校验,通过后使用私钥对JWT进行签名加密。并将JWT返回给用户
4.用户携带JWT访问
5.gateway直接通过公钥解密JWT进行验证
 

二.RSA测试Demo

JWT包含三部分数据
header头部分-Payload载荷(包含用户信息身份)-Signature签名


一.工具类

用户基本信息

package entity;

/**
 * 载荷对象
 */
public class UserInfo {

    private Long id;

    private String username;

    public UserInfo() {
    }

    public UserInfo(Long id, String username) {
        this.id = id;
        this.username = username;
    }

    public Long getId() {
        return this.id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

公钥私钥生成读取类

package 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;

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(1024, 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);
    }
}

公钥私钥加解密类

package utils;


import entity.UserInfo;
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;

public class JwtUtils {
    /**
     * 私钥加密token
     *
     * @param userInfo      载荷中的数据
     * @param privateKey    私钥
     * @param expireMinutes 过期时间,单位秒
     * @return
     * @throws Exception
     */
    public static String generateToken(UserInfo userInfo, PrivateKey privateKey, int expireMinutes) throws Exception {
        return Jwts.builder()
                .claim(JwtConstans.JWT_KEY_ID, userInfo.getId())
                .claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /**
     * 私钥加密token
     *
     * @param userInfo      载荷中的数据
     * @param privateKey    私钥字节数组
     * @param expireMinutes 过期时间,单位秒
     * @return
     * @throws Exception
     */
    public static String generateToken(UserInfo userInfo, byte[] privateKey, int expireMinutes) throws Exception {
        return Jwts.builder()
                .claim(JwtConstans.JWT_KEY_ID, userInfo.getId())
                .claim(JwtConstans.JWT_KEY_USER_NAME, userInfo.getUsername())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey))
                .compact();
    }

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

    /**
     * 公钥解析token
     *
     * @param token     用户请求中的token
     * @param publicKey 公钥字节数组
     * @return
     * @throws Exception
     */
    private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception {
        return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey))
                .parseClaimsJws(token);
    }

    /**
     * 获取token中的用户信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     * @throws Exception
     */
    public static UserInfo getInfoFromToken(String token, PublicKey publicKey) throws Exception {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        return new UserInfo(
                ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),
                ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME))
        );
    }

    /**
     * 获取token中的用户信息
     *
     * @param token     用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     * @throws Exception
     */
    public static UserInfo getInfoFromToken(String token, byte[] publicKey) throws Exception {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();
        return new UserInfo(
                ObjectUtils.toLong(body.get(JwtConstans.JWT_KEY_ID)),
                ObjectUtils.toString(body.get(JwtConstans.JWT_KEY_USER_NAME))
        );
    }
}

辅助类:
 

package utils;

import org.apache.commons.lang3.StringUtils;

/**
 * 从jwt解析得到的数据是Object类型,转换为具体类型可能出现空指针,
 * 这个工具类进行了一些转换
 */
public class ObjectUtils {

    public static String toString(Object obj) {
        if (obj == null) {
            return null;
        }
        return obj.toString();
    }

    public static Long toLong(Object obj) {
        if (obj == null) {
            return 0L;
        }
        if (obj instanceof Double || obj instanceof Float) {
            return Long.valueOf(StringUtils.substringBefore(obj.toString(), "."));
        }
        if (obj instanceof Number) {
            return Long.valueOf(obj.toString());
        }
        if (obj instanceof String) {
            return Long.valueOf(obj.toString());
        } else {
            return 0L;
        }
    }

    public static Integer toInt(Object obj) {
        return toLong(obj).intValue();
    }
}
package utils;

public abstract class JwtConstans {
    public static final String JWT_KEY_ID = "id";
    public static final String JWT_KEY_USER_NAME = "username";
}
二.测试代码
import entity.UserInfo;
import org.junit.Before;
import org.junit.Test;
import utils.JwtUtils;
import utils.RsaUtils;

import java.security.PrivateKey;
import java.security.PublicKey;

public class JwtTest {

    private static final String pubKeyPath = "D:\\tmp\\rsa\\rsa.pub";

    private static final String priKeyPath = "D:\\tmp\\rsa\\rsa.pri";

    private PublicKey publicKey;

    private PrivateKey privateKey;

    @Test
    public void testRsa() throws Exception {
        RsaUtils.generateKey(pubKeyPath, priKeyPath, "234");
    }

    //在运行生成token之前,获取公钥和私钥对象
    @Before
    public void testGetRsa() throws Exception {
        this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
        this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
    }

    //通过私钥加密
    @Test
    public void testGenerateToken() throws Exception {
        // 生成token
        String token = JwtUtils.generateToken(new UserInfo(20L, "jack"), privateKey, 5);
        System.out.println("token = " + token);
    }

    //通过公钥解析token
    @Test
    public void testParseToken() throws Exception {
        String token = "eyJhbGciOiJSUzI1NiJ9.eyJpZCI6MjsInVzZXJuYW1lIjoiamFjayIsImV4cCI6MTY3MDAzOTI0M30.Y6MstaAuWwgsNenMRYQSBeG-zx9kHmh75PJuGrJuyPPuetebwqS6Xl2NQGmMYx1mQII-Tq6M-vGGMvQJ8q2Dd8GXL1u-RMC9-e7SKkAgVFP0YzwY3YJRgw9q62snWZqZllmNIgp_jFu14HHHktCS49V-bd1HR9W2PMQoWOeWquI";

        // 解析token
        UserInfo user = JwtUtils.getInfoFromToken(token, publicKey);
        System.out.println("id: " + user.getId());
        System.out.println("userName: " + user.getUsername());
    }
}
三.测试流程


指定路径一定包括rsa







三.Spring Cloud+Gateway+RSA


一.gateway模块:
yml文件

定义白名单(不需要鉴权的路径),公钥地址(解密鉴权),以及cookieName(获取加密的token)

配置类:

获取公钥

package com.yigou.gw.config;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import utils.RsaUtils;

import javax.annotation.PostConstruct;
import java.security.PublicKey;

@ConfigurationProperties(prefix = "yigou.jwt")
@Data
@Slf4j
public class JwtProperties {

    private String pubKeyPath;// 公钥

    private PublicKey publicKey; // 公钥

    private String cookieName;

    //@PostConstruct注解的方法将会在依赖注入完成后被自动调用。
    //执行顺序:Constructor >> @Autowired >> @PostConstruct
    @PostConstruct
    public void init(){
        try {
            // 获取公钥和私钥
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
        } catch (Exception e) {
            log.error("初始化公钥失败!", e);
            throw new RuntimeException();
        }
    }
}

获取定义的白名单对象

package com.yigou.gw.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.util.List;

@ConfigurationProperties(prefix = "yigou.filter")
@Data
public class FilterProperties {

    private List<String> allowPaths;
}
自定义拦截:
package com.yigou.gw.filter;

import com.yigou.gw.config.FilterProperties;
import com.yigou.gw.config.JwtProperties;
import entity.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.filter.OrderedFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import utils.JwtUtils;

import java.util.List;


@Component
@EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
@Slf4j
public class LoginFilter implements GlobalFilter, Ordered {

    @Autowired
    private JwtProperties prop;

    @Autowired
    private FilterProperties fprop;

    //核心过滤器方法
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //设置白名单,白名单里面的请求路径,直接放行
        String path = exchange.getRequest().getPath().toString();
        List<String> allowPaths = fprop.getAllowPaths();
        for (String allowPath : allowPaths) {
            //判断path是否以allowPath开头
            if (path.startsWith(allowPath)){
                //放行
                return chain.filter(exchange);
            }
        }
        //1.获取请求中的token
        HttpCookie cookie = exchange.getRequest().getCookies().getFirst(prop.getCookieName());
        String token=null;
        if (cookie!=null){
             token=cookie.getValue();
        }
        log.info("token:",token);
        try {
            //2.解析token
            UserInfo userInfo = JwtUtils.getInfoFromToken(token, prop.getPublicKey());
            //3.放行
            if (userInfo!=null){
               return chain.filter(exchange);
            }

        } catch (Exception e) {
            e.printStackTrace();
            //如果出现异常,设置异常状态
            exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        }
        return exchange.getResponse().setComplete();
    }

    //过滤器执行顺序
    @Override
    public int getOrder() {
        return 0;
    }
}
功能总结:

1.使用yml配置获取公钥存储路径以及白名单路径。
2.通过路径得到公钥,自定义拦截器先去对比是否为白名单路径,若为白名单路径直接放行。
3.若不为白名单  通过cookieName得到存储的token,并使用公钥解析是否可以获得userinfo对象来判断是否放行。

二.鉴权中心模板:
yml文件:
server:
  port: 8087
spring:
  application:
    name: auth-service
  cloud:
    nacos:
      discovery:
        server-addr: http://192.168.147.129:8848

yigou:
  jwt:
    secret: yigou@Login(Auth}*^31)&javaman% # 登录校验的密钥
    pubKeyPath: D:\\tmp\\rsa\\rsa.pub # 公钥地址
    priKeyPath: D:\\tmp\\rsa\\rsa.pri # 私钥地址
    expire: 1800 # 过期时间,单位秒
    cookieName: YG_TOKEN
    cookieMaxAge: 1800
配置文件:
package com.yigou.auth.config;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import utils.RsaUtils;

import javax.annotation.PostConstruct;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;

@ConfigurationProperties(prefix = "yigou.jwt")
@Data
@Slf4j
public class JwtProperties {

    private String secret; // 密钥

    private String pubKeyPath;// 公钥

    private String priKeyPath;// 私钥

    private int expire;// token过期时间

    private PublicKey publicKey; // 公钥

    private PrivateKey privateKey; // 私钥
    
    private String cookieName;

    private Integer cookieMaxAge;

    /**
     * @PostContruct:在构造方法执行之后执行该方法
     */
    @PostConstruct
    public void init(){
        try {
            File pubKey = new File(pubKeyPath);
            File priKey = new File(priKeyPath);
            if (!pubKey.exists() || !priKey.exists()) {
                // 生成公钥和私钥
                RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);
            }
            // 获取公钥和私钥
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
        } catch (Exception e) {
            log.error("初始化公钥和私钥失败!", e);
            throw new RuntimeException();
        }
    }

    // getter setter ...
}
controller
package com.yigou.auth.controller;

import com.yigou.auth.config.JwtProperties;
import com.yigou.auth.service.AuthService;
import com.yigou.common.enums.ExceptionEnum;
import com.yigou.common.exception.YgException;
import com.yigou.common.utils.CookieUtils;
import com.yigou.user.entity.TbUser;
import entity.UserInfo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import utils.JwtUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
@EnableConfigurationProperties(JwtProperties.class)
public class AuthController {
    @Autowired
    private JwtProperties prop;

    @Autowired
    private AuthService authService;

    //接收用户名和密码,通过密钥加密生成JWT,写入到cookie中保存
    @PostMapping("/accredit")
    public ResponseEntity<Void> authentication(@RequestParam("username") String username,
                                               @RequestParam("password") String password,
                                               HttpServletResponse response,
                                               HttpServletRequest request){
        //1.验证之后,生成JWT-token
        String token=authService.authentication(username,password);
        //2.如果token是空的,验证失败
        if(StringUtils.isEmpty(token)){
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        //3.如果不为空,说明不为空,返回token存放到cookie中
        CookieUtils.
                setCookie(request,response,prop.getCookieName(),token,prop.getCookieMaxAge(),true);
        return ResponseEntity.ok().build();
    }

    //验证用户信息,如果用户信息验证成功,返回用户信息
    @GetMapping("verify")
    public ResponseEntity<UserInfo> verify(@CookieValue("YG_TOKEN") String token,
                                           HttpServletRequest request,
                                           HttpServletResponse response){
        //1.根据token解析获取到的cookie中的token
        try {
            UserInfo userInfo = JwtUtils.getInfoFromToken(token, prop.getPublicKey());
            //重新生成token
            String newToken = JwtUtils.generateToken(userInfo, prop.getPrivateKey(), prop.getExpire());
            //重新把Token设置到cookie中,覆盖原来的cookie
            CookieUtils.setCookie(request,response,prop.getCookieName(),newToken, prop.getCookieMaxAge());
            return ResponseEntity.ok(userInfo);
        } catch (Exception e) {
            throw new YgException(ExceptionEnum.USER_TOKEN_EXISTS_FALL);
        }
    }
}
service:
package com.yigou.auth.service.impI;

import com.yigou.auth.client.UserClient;
import com.yigou.auth.config.JwtProperties;
import com.yigou.auth.service.AuthService;
import com.yigou.common.enums.ExceptionEnum;
import com.yigou.common.exception.YgException;
import com.yigou.user.entity.TbUser;
import entity.UserInfo;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import utils.JwtUtils;

@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private UserClient userClient;

    @Autowired
    private JwtProperties prop;
    //验证用户名和密码之后,生成jwt-token
    @Override
    public String authentication(String username, String password) {
        //1.通过用户名,密码验证是否存在
        TbUser user = userClient.getUserInfo(username, password);
        if (user==null){
            return null;
        }
        //2.生成token
        UserInfo userInfo = new UserInfo();
        userInfo.setId(user.getId());
        userInfo.setUsername(user.getUsername());
        try {
            String token = JwtUtils.generateToken(userInfo, prop.getPrivateKey(), prop.getExpire());
            return token;
        } catch (Exception e) {
            throw new YgException(ExceptionEnum.JWT_TOKEN_CREATE_fALL);
        }
    }
}
调用接口
package com.yigou.auth.client;

import com.yigou.user.api.UserApi;
import org.springframework.cloud.openfeign.FeignClient;

@FeignClient("user-service")
public interface UserClient extends UserApi {
}
三.注意点:
1.使用Feign调用其他模块方法:

将提供模块的对外接口包以及httpclient依赖到调用模块

 <!--httpclient支持-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

调用模块的接口类继承提供模块对外开放的接口类

注释@FeignClient的参数名和提供模块在注册中心的名相同。

2.提供模块建立对外接口包

对外接口类 所提供方法 要与controller方法行映射

3.调用模块主函数开启FeignClients
package com.yigou.auth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class    YgAuthServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(YgAuthServiceApplication.class, args);
    }

}
四.总结

1.登录请求通过网关(登录为白名单 因此不会拦截校验),网关转发到授权中心模块
2.授权中心模块通过前端发送的用户信息feign调用用户模块查询用户信息。根据是否有该用户来判断是否使用该用户信息以及拿到yml定义的私钥路径去生成token存入cookie中。


3.若登陆成功 那么随后每次请求都会携带cookie中的token 在网关进行拦截以及公钥解密。每次鉴权都会在鉴权模块将token重新刷新一遍。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部