(6)Zuul验证通过后才可以访问微服务所提供的REST资源。
需要说明的是,在crazy-springcloud微服务开发脚手架中,Provider微服务提供者自身不需要进行单独的安全认证,Provider之间的内部远程调用也是不需要安全认证的,安全认证全部由网关负责。严格来说,这套安全机制是能够满足一般的生产场景安全认证要求的。如果觉得这个安全级别不是太高,单个的Provider微服务也需要进行独立的安全认证,那么实现起来也是很容易的,只需要导入公共的安全认证模块base-auth即可。实际上早期的crazy-springcloud脚手架也是这样做的,后期发现这样做纯属多虑,而且大大降低了Provider服务提供者模块的可复用性和可移植性(这是微服务架构的巨大优势之一)。所以,crazy-springcloud后来将整体架构调整为由网关(如Zuul或者Nginx)负责安全认证,去掉了Provider服务提供者的安全认证能力。
JWT安全令牌规范详解
===========
JWT(JSON Web Token)是一种用户凭证的编码规范,是一种网络环境下编码用户凭证的JSON格式的开放标准(RFC 7519)。JWT令牌的格式被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)、用户身份认证等场景。
一个编码之后的JWT令牌字符串分为三部分:header+payload+signature。这三部分通过点号“.”连接,第一部分常被称为头部(header),第二部分常被称为负载(payload),第三部分常被称为签名(signature)。
1.JWT的header
编码之前的JWT的header部分采用JSON格式,一个完整的头部就像如下的JSON内容:
{
“typ”:“JWT”,
“alg”:“HS256”
}
其中,"typ"是type(类型)的简写,值为"JWT"代表JWT类型;"alg"是加密算法的简写,值为"HS256"代表加密方式为HS256。
采用JWT令牌编码时,header的JSON字符串将进行Base64编码,编码之后的字符串构成了JWT令牌的第一部分。
2.JWT的playload
编码之前的JWT的playload部分也是采用JSON格式,playload是存放有效信息的部分,一个简单的playload就像如下的JSON内容:
{
“sub”:“session id”,
“exp”:1579315717,
“iat”:1578451717
}
采用JWT令牌编码时,playload的JSON字符串将进行Base64编码,编码之后的字符串构成了JWT令牌的第二部分。
3.JWT的signature
JWT的第三部分是一个签名字符串,这一部分是将header的Base64编码和payload的Base64编码使用点号(.)连接起来之后,通过header声明的加密算法进行加密所得到的密文。为了保证安全,加密时需要加入盐(salt)。
下面是一个演示用例:用Java代码生成JWT令牌,然后对令牌的header部分字符串和payload部分字符串进行Base64解码,并输出解码后的JSON。
package com.crazymaker.demo.auth;
//省略import
@Slf4j
public class JwtDemo
{
@Test
public void testBaseJWT()
{
try
{
/**
*JWT的演示内容
*/
String subject = “session id”;
/**
*签名的加密盐
*/
String salt = “user password”;
/**
*签名的加密算法
*/
Algorithm algorithm = Algorithm.HMAC256(salt);
//签发时间
long start = System.currentTimeMillis() - 60000;
//过期时间,在签发时间的基础上加上一个有效时长
Date end = new Date(start + SessionConstants.SESSION_TIME_OUT *1000);
/**
*获取编码后的JWT令牌
*/
String token = JWT.create()
.withSubject(subject)
.withIssuedAt(new Date(start))
.withExpiresAt(end)
.sign(algorithm);
log.info(“token=” + token);
//编码后输出demo为:
//token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzZXNza
W9uIGlkIiwiZXhwIjoxNTc5MzE1NzE3LCJpYXQiOjE1Nzg0NTE3MTd9.iANh9Fa0B_6H5TQ11bLCWcEpmWxuCwa2Rt6rnzBWteI
//以.分隔令牌
String[] parts = token.split(“\.” );
/**
*对第一部分和第二部分进行解码
*解码后的第一部分:header
*/
String headerJson =
StringUtils.newStringUtf8(Base64.decodeBase64(parts[0]));
log.info(“parts[0]=” + headerJson);
//解码后的第一部分输出的示例为: //parts[0]={“typ”:“JWT”,“alg”:“HS256”}
/**
*解码后的第二部分:payload
*/
String payloadJson;
payloadJson = StringUtils.newStringUtf8
(Base64.decodeBase64(parts[1]));
log.info(“parts[1]=” + payloadJson);
//输出的示例为:
//解码后的第二部分:parts[1]={“sub”:“session id”,“exp”:1579315535,“iat”:
1578451535}
} catch (Exception e)
{
e.printStackTrace();
}
}
…
}
在编码前的JWT中,payload部分JSON中的属性被称为JWT的声明。JWT的声明分为两类:
(1)公有的声明(如iat)。
(2)私有的声明(自定义的JSON属性)。
公有的声明也就是JWT标准中注册的声明,主要为以下JSON属性:
(1)iss:签发人。
(2)sub:主题。
(3)aud:用户。
(4)iat:JWT的签发时间。
(5)exp:JWT的过期时间,这个过期时间必须要大于签发时间。
(6)nbf:定义在什么时间之前该JWT是不可用的。
私有的声明是除了公有声明之外的自定义JSON字段,私有的声明可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息。下面的JSON例子中的uid、user_name、nick_name等都是私有声明。
{
“uid”: “123…”,
“sub”: “session id”,
“user_name”: “admin”,
“nick_name”: “管理员”,
“exp”: 1579317358,
“iat”: 1578453358
}
下面是一个向JWT令牌添加私有声明的实例,代码如下:
package com.crazymaker.demo.auth;
//省略import
@Slf4j
public class JwtDemo
{
/**
*测试私有声明
*/
@Test
public void testJWTWithClaim()
{
try
{
String subject = “session id”;
String salt = “user password”;
/**
*签名的加密算法
*/
Algorithm algorithm = Algorithm.HMAC256(salt);
//签发时间
long start = System.currentTimeMillis() - 60000;
//过期时间,在签发时间的基础上加上一个有效时长
Date end = new Date(start + SessionConstants.SESSION_TIME_OUT *1000);
/**
*JWT建造者
*/
JWTCreator.Builder builder = JWT.create();
/**
*增加私有声明
*/
builder.withClaim(“uid”, “123…”);
builder.withClaim(“user_name”, “admin”);
builder.withClaim(“nick_name”,“管理员”);
/**
*获取编码后的JWT令牌
*/
String token =builder
.withSubject(subject)
.withIssuedAt(new Date(start))
.withExpiresAt(end)
.sign(algorithm);
log.info(“token=” + token);
//以.分隔,这里需要转义
String[] parts = token.split(“\.” );
String payloadJson;
/**
*解码payload
*/
payloadJson = StringUtils.newStringUtf8
(Base64.decodeBase64(parts[1]));
log.info(“parts[1]=” + payloadJson);
//输出demo为:parts[1]=
//{“uid”:“123…”,“sub”:“session id”,“user_name”:“admin”,
“nick_name”:“管理员”,“exp”:1579317358,“iat”:1578453358}
} catch (Exception e)
{
e.printStackTrace();
}
}
}
由于JWT的payload声明(JSON属性)是可以解码的,属于明文信息,因此不建议添加敏感信息。
JWT+Spring Security认证处理流程
==========================
实际开发中如何使用JWT进行用户认证呢?疯狂创客圈的crazy-springcloud开发脚手架将JWT令牌和Spring Security相结合,设计了一个公共的、比较方便复用的用户认证模块base-auth。一般来说,在Zuul网关或者微服务提供者进行用户认证时导入这个公共的base-auth模块即可。
这里还是按照6.4.2节中请求认证处理流程的5个步骤介绍base-auth模块中JWT令牌的认证处理流程。
首先看第一步:定制一个凭证/令牌类,封装用户信息和JWT认证信息。
package com.crazymaker.springcloud.base.security.token;
//省略import
public class JwtAuthenticationToken extends AbstractAuthenticationToken
{
private static final long serialVersionUID = 3981518947978158945L;
//封装用户信息:用户id、密码
private UserDetails userDetails;
//封装的JWT认证信息
private DecodedJWT decodedJWT;
…
}
再看第二步:定制一个认证提供者类和凭证/令牌类进行配套,并完成对自制凭证/令牌实例的验证。
package com.crazymaker.springcloud.base.security.provider;
//省略import
public class JwtAuthenticationProvider implements AuthenticationProvider
{
//用于通过session id查找用户信息
private RedisOperationsSessionRepository sessionRepository;
public JwtAuthenticationProvider(RedisOperationsSessionRepository sessionRepository)
{
this.sessionRepository = sessionRepository;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
//判断JWT令牌是否过期
JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) authentication;
DecodedJWT jwt =jwtToken.getDecodedJWT();
if (jwt.getExpiresAt().before(Calendar.getInstance().getTime()))
{
throw new NonceExpiredException(“认证过期”);
}
//取得session id
String sid = jwt.getSubject();
//取得令牌字符串,此变量将用于验证是否重复登录
String newToken = jwt.getToken();
//获取session
Session session = null;
try
{
session = sessionRepository.findById(sid);
} catch (Exception e)
{
e.printStackTrace();
}
if (null == session)
{
throw new NonceExpiredException(“还没有登录,请登录系统!”);
}
String json = session.getAttribute(G_USER);
if (StringUtils.isBlank(json))
{
throw new NonceExpiredException(“认证有误,请重新登录”);
}
//取得session中的用户信息
UserDTO userDTO = JsonUtil.jsonToPojo(json, UserDTO.class);
if (null == userDTO)
{
throw new NonceExpiredException(“认证有误,请重新登录”);
}
判断是否在其他地方已经登录 //判断是否在其他地方已经登录
if (null == newToken || !newToken.equals(userDTO.getToken()))
{
throw new NonceExpiredException(“您已经在其他的地方登录!”);
}
String userID = null;
if (null == userDTO.getUserId())
{
userID = String.valueOf(userDTO.getId());
} else
{
userID = String.valueOf(userDTO.getUserId());
}
UserDetails userDetails = User.builder()
.username(userID)
.password(userDTO.getPassword())
.authorities(SessionConstants.USER_INFO)
.build();
try
{
//用户密码的密文作为JWT的加密盐
String encryptSalt = userDTO.getPassword();
Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
//创建验证器
JWTVerifier verifier = JWT.require(algorithm)
.withSubject(sid)
.build();
//进行JWTtoken验证
verifier.verify(newToken);
} catch (Exception e)
{
throw new BadCredentialsException(“认证有误:令牌校验失败,请重新登录”, e);
}
//返回认证通过的token,包含用户信息,如user id等
JwtAuthenticationToken passedToken =
new JwtAuthenticationToken(userDetails, jwt, userDetails.getAuthorities());
passedToken.setAuthenticated(true);
return passedToken;
}
//支持自定义的令牌JwtAuthenticationToken
@Override
public boolean supports(Class<?> authentication)
{
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
}
}
JwtAuthenticationProvider负责对传入的JwtAuthenticationToken凭证/令牌实例进行多方面的验证:(1)验证解码后的DecodedJWT实例是否过期;(2)由于本演示中JWT的subject(主题)信息存放的是用户的Session ID,因此还要判断会话是否存在;(3)使用会话中的用户密码作为盐,对JWT令牌进行安全性校验。
如果以上验证都顺利通过,就构建一个新的JwtAuthenticationToken令牌,将重要的用户信息(UserID)放入令牌并予以返回,供后续操作使用。
第三步:定制一个过滤器类,从请求中获取用户信息组装成JwtAuthenticationToken凭证/令牌,交给认证管理者。在crazy-springcloud脚手架中,前台有用户端和管理端的两套界面,所以,将认证头部信息区分成管理端和用户端两类:管理端的头部字段为Authorization;用户端的认证信息头部字段为token。
过滤器从请求中获取认证的头部字段,解析之后组装成JwtAuthenticationToken令牌实例,提交给AuthenticationManager进行验证。
package com.crazymaker.springcloud.base.security.filter;
//省略import
public class JwtAuthenticationFilter extends OncePerRequestFilter
{
…
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws
{
…
Authentication passedToken = null;
AuthenticationException failed = null;
//从HTTP请求取得JWT令牌的头部字段 String token = null;
//用户端存放的JWT的HTTP头部字段为token
String sessionIDStore = SessionHolder.getSessionIDStore();
if (sessionIDStore.equals(SessionConstants.SESSION_STORE))
{
token = request.getHeader(SessionConstants.AUTHORIZATION_HEAD);
}
//管理端存放的JWT的HTTP头部字段为Authorization
else if (sessionIDStore.equals
(SessionConstants.ADMIN_SESSION_STORE))
{
token = request.getHeader
(SessionConstants.ADMIN_AUTHORIZATION_HEAD);
小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Java工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
总结
机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。
对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。
你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:
请转发本文支持一下
极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-m05RdDMb-1710421386802)]
[外链图片转存中…(img-dRz4Q7va-1710421386803)]
[外链图片转存中…(img-b6lyNveT-1710421386803)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Java)
[外链图片转存中…(img-fLv3n2JL-1710421386803)]
总结
机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。
对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。
你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:
请转发本文支持一下
[外链图片转存中…(img-7l8EBtTv-1710421386803)]
[外链图片转存中…(img-lWGpViZe-1710421386804)]