token安全认证的实践
背景介绍:
因项目需求,有PC端 APP端和小程序端,但登陆接口是同一个,然而微服务也无法使用传统的session解决用户登录问题(注意这里是传统的session不是spring session),使用户信息在其他服务共享。
如此一来就想到了token安全认证,而JWT生成token可以包含用户信息,也就果断选择了JWT作为SpringCloud gateway网关的token校验工具,这样,我们便可以直接解析token获取用户信息了。
具体实现思路:
-
让JWT在其他所有服务可以共同使用,父工程需要引入JWT jar。避免在其他服务重复引入。
-
如何使用JWT生成token。
-
如何解析token。
-
如何让网关拦截用户请求校验token。
-
如何避免首次登录被网关拦截。
代码实现:
1.创建SpringCloud项目
SpringCloud子项目包含 eureka,gateway,auth三个工程,父工程maven依赖如下。
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>6.0</version>
</dependency>
2.Auth和gateway编写TOKEN工具类
public class Token {
private static final Logger log = LoggerFactory.getLogger(Token.class);
/**
* 1.创建一个32-byte的密匙JWT生成TOKEN
*/
private static final byte[] secret = "geiwodiangasfdjsikolkjikolkijswe".getBytes();
//生成一个token
public static String creatToken(Map<String,Object> payloadMap) throws JOSEException {
//3.先建立一个头部Header
/**
* JWSHeader参数:1.加密算法法则,2.类型,3.。。。。。。。
* 一般只需要传入加密算法法则就可以。
* 这里则采用HS256
* JWSAlgorithm类里面有所有的加密算法法则,直接调用。
*/
JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);
//建立一个载荷Payload
Payload payload = new Payload(new JSONObject(payloadMap));
//将头部和载荷结合在一起
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
//建立一个密匙
JWSSigner jwsSigner = new MACSigner(secret);
//签名
jwsObject.sign(jwsSigner);
//生成token
return jwsObject.serialize();
}
/**
* 解析一个token
* @param token
* @return
* @throws ParseException
* @throws JOSEException
*/
public static Map<String,Object> valid(String token) throws ParseException, JOSEException {
//解析token
JWSObject jwsObject = JWSObject.parse(token);
//获取到载荷
Payload payload=jwsObject.getPayload();
//建立一个解锁密匙
JWSVerifier jwsVerifier = new MACVerifier(secret);
Map<String, Object> resultMap = new HashMap<>();
//判断token
if (jwsObject.verify(jwsVerifier)) {
resultMap.put("Result", 0);
//载荷的数据解析成json对象。
JSONObject jsonObject = payload.toJSONObject();
resultMap.put("data", jsonObject);
//判断token是否过期
if (jsonObject.containsKey("exp")) {
Long expTime = Long.valueOf(jsonObject.get("exp").toString());
Long nowTime = new Date().getTime();
//判断是否过期
if (nowTime > expTime) {
//已经过期
resultMap.clear();
resultMap.put("Result", 2);
}
}
}else {
resultMap.put("Result", 1);
}
return resultMap;
}
/**
* 生成token的业务逻辑 登录接口调用次业务
* @param uid
* @return
*/
public static String TokenTest(Long uid,Long deptId,String userType,int companyId) {
//获取生成token
Map<String, Object> map = new HashMap<>();
//建立载荷,这些数据根据业务,自己定义。
map.put("uid", uid);
map.put("deptId", deptId);
map.put("userType", userType);
map.put("companyId", companyId);
//生成时间
map.put("sta", new Date().getTime());
//过期时间
map.put("exp", new Date().getTime()+1000*3600*24*15);
try {
String token = Token.creatToken(map);
System.out.println("token="+token);
return token;
} catch (JOSEException e) {
System.out.println("生成token失败");
e.printStackTrace();
}
return null;
}
/**
* 处理解析的业务逻辑 gateway JWT认证过滤器解析
* @param token
*/
public static Map<String,Object> ValidToken(String token) {
Map<String, Object> userMsg = new HashMap<String, Object>();
//解析token
try {
if (token != null) {
Map<String, Object> validMap = Token.valid(token);
int i = (int) validMap.get("Result");
if (i == 0) {
log.info("token解析成功");
JSONObject jsonObject = (JSONObject) validMap.get("data");
log.info("uid是:" + jsonObject.get("uid"));
log.info("deptId是:" + jsonObject.get("deptId"));
log.info("userType是:" + jsonObject.get("userType"));
log.info("companyId是:" + jsonObject.get("companyId"));
log.info("生成时间是:"+jsonObject.get("sta"));
log.info("过期时间是:"+jsonObject.get("exp"));
userMsg.put("token",token);
userMsg.put("uid",jsonObject.get("uid"));
userMsg.put("deptId",jsonObject.get("deptId"));
userMsg.put("companyId",jsonObject.get("companyId"));
userMsg.put("userType",jsonObject.get("userType"));
return userMsg;
} else if (i == 2) {
log.info("token已经过期");
return userMsg;
}
}
} catch (ParseException e) {
e.printStackTrace();
} catch (JOSEException e) {
e.printStackTrace();
}
return userMsg;
}
public static void main(String[] ages) {
//获取token
Long uid = 1L;
Long deptId = 2L;
String userType = "3";
int companyId = 4;
String token = TokenTest(uid,deptId,userType,companyId);
//解析token
log.info(ValidToken(token).toString());
}
}
特别提示:以上工具类可以在用户登录授权接口中调用,用以生成token,示例代码如下(可以借鉴不可复制哦,请根据自己业务逻辑在合适的地方调用TOKEN工具)
@RestController
@RequestMapping("/currency")
public class CurrencyLoginController {
//** (需要前端和后端保持一致)
private static final String KEY = "abcdefgabcdefg12";
//redis初始KEY值
private static final String LOGIN_USER = "login_user";
@Autowired
private RedisUtil ru;
@PostMapping("/login")
public Map<String, Object> ajaxLogin(String username, String password, Boolean rememberMe) throws Exception{
password = AESUtil.aesDecrypt(password,KEY);//双向加密规则
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
User user = ShiroUtils.getUser();
String access_token = Token.generateToken(user.getUserId(), user.getDeptId(),user.getLoginUserType(), user.getCompanyId());
UserMsg resultUser = new UserMsg();
resultUser.setCompanyId(user.getCompanyId());
resultUser.setUserType(user.getLoginUserType());
resultUser.setDeptId(user.getDeptId());
resultUser.setUid(user.getUserId());
resultUser.setToken(access_token);
ru.set(LOGIN_USER+user.getUserId(), resultUser, 3600*24*15);
return ResultMap.ok("登录成功", resultUser);//改造——》》获取用户信息保存到redis中实现用户信息在微服务*享,生成token
}catch (AuthenticationException e){
String msg = "用户或密码错误";
if (StringUtils.isNotEmpty(e.getMessage())){
msg = e.getMessage();
}
return ResultMap.error(msg);
}
}
}
好了,此时呢,我们已经通过auth工程完成了用户登录授权,并且生成了token。那么如何在gateway网关中进行token认证呢?
3.gateway网关中编写JwtCheckGatewayFilterFactory过滤器。
此类需要继承gateway的AbstractGatewayFilterFactory。
代码实现如下:
首先gateway网关yml文件中需要代理auth路由。
spring:
cloud:
gateway:
routes:
- id: neo_route
uri: lb://YUNXI-AUTH
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
- JwtCheck
自定义 JwtCheckGatewayFilterFactory 继承 AbstractGatewayFilterFactory 抽象类,代码如下:
public class JwtCheckGatewayFilterFactory extends AbstractGatewayFilterFactory<JwtCheckGatewayFilterFactory.Config> {
private static final Logger log = LoggerFactory.getLogger(JwtCheckGatewayFilterFactory .class);
//定义用户认证登录接口
private static final String CURRENCY_URL="/currency/login";
//redis初始KEY值
private static final String LOGIN_USER = "login_user";
@Autowired
private RedisUtil ru;
public JwtCheckGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String jwtToken = exchange.getRequest().getHeaders().getFirst("Authorization");
log.info(exchange.getRequest().getURI().toString());
//校验jwtToken的合法性,如果当前请求url和认证url相同跳过认证,表示用户首次登录认证
if(exchange.getRequest().getURI().toString().contains(CURRENCY_URL)){
return chain.filter(exchange);
}
if(jwtToken != null){
log.info(Token.ValidToken(jwtToken).toString());
//解析TOKEN
Map<String, Object> userMsg = Token.ValidToken(jwtToken);
Long uid = (Long) userMsg.get("uid");
if(ru.hasKey(LOGIN_USER+uid)){
Object obj = ru.get(LOGIN_USER+uid);
UserMsg userModel = (UserMsg) obj;
//解析客户端传过来的TOKEN是否和缓存中的TOKEN相同,并且判断TOKEN过期时间是否大于当前时间
if(userModel.getToken().equals(jwtToken)){
return chain.filter(exchange);
}else{
ServerHttpResponse response = exchange.getResponse();
String warningStr = "不合法的请求";
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
}
}else{
ServerHttpResponse response = exchange.getResponse();
String warningStr = "登录超时";
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
}
}
//不合法(响应未登录的异常)
ServerHttpResponse response = exchange.getResponse();
//设置headers
HttpHeaders httpHeaders = response.getHeaders();
httpHeaders.add("Content-Type", "application/json; charset=UTF-8");
httpHeaders.add("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
//设置body
String warningStr = "未授权的请求,请登录";
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(warningStr.getBytes());
return response.writeWith(Mono.just(bodyDataBuffer));
};
}
public static class Config {
//Put the configuration properties for your filter here
}
}
编写config文件将JWT认证过滤器添加到Spring bean中。
@Configuration
public class AppConfig {
@Bean
public JwtCheckGatewayFilterFactory jwtCheckGatewayFilterFactory(){
return new JwtCheckGatewayFilterFactory();
}
}
此时我们就完成了整个token认证过程,其实简单的来说就是:
-
第一步:Auth工程配合用户登录生成token,并将token和用户信息存储在redis中。
-
第二步:在gayeway中编写JWT认证过滤器,用以校验用户请求中携带的token。
有图有真相
特别提示:我的auth工程端口是8766,登录认证接口路由是/currency/login。而此时我请求的认证接口是/main/currency/login,端口是8765,我们在文章开头就已说明,gateway网关在yml文件中配置auth代理为auth/,和这里的main是同一个道理。
如果此时我们再去请求项目中其他端口携带过期的token试试看效果:
我们登陆认证返回的token是:
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlA;
携带的其他过期的token是:
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjQ1NjUwNzIwMiwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1Nzc1MjUwNzIwMn0._yF2TeaR4MTmF-Re9QciMZOeRKBOQmfvi3o4hWeGSMU
再携带错误的token试试看:
登陆认证返回的token是:
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlA;
携带错误的token是:
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInN0YSI6MTU1NjcxODU2Nzc3NCwiY29tcGFueUlkIjowLCJkZXB0SWQiOjEwMCwidXNlclR5cGUiOm51bGwsImV4cCI6MTU1ODAxNDU2Nzc3NH0.6oXx4Wk-eWHSWTHyJHmoiGowKnAmBdCHIRCzsMq5XlD
携带正确的token:
到这里我么你的整个SpringCloud gateway网关+JWT安全认证就结束啦,非常抱歉,由于项目保密性不能为大家提供项目源码。
上一篇: ORB_SLAM2源码解析-系统
下一篇: 嵌入式系统(三):ARM 指令汇编(4)
推荐阅读
-
QQ浏览器在体验的同时如何开启安全认证
-
【SSL证书行业】HTTPS认证支撑网络安全的防护门!
-
vue路由导航守卫和请求拦截以及基于node的token认证的方法
-
spring cloud-给Eureka Server加上安全的用户认证详解
-
QQ浏览器开启安全认证以保证电脑的安全
-
ASP.NET Core 2.2 : 二十六. 应用JWT进行用户认证及Token的刷新
-
Django+JWT实现Token认证的实现方法
-
在ASP.NET Core中实现一个Token base的身份认证实例
-
详解在ASP.NET Core中使用Angular2以及与Angular2的Token base身份认证
-
基于JWT的Token登录认证