一.业务流程
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重新刷新一遍。
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » JWT整合Gateway实现鉴权(RSA与公私密钥工具类)
发表评论 取消回复