微服务网关与用户身份识别,JWT+Spring Security进行网关安全认证(1)

/**

*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);

}

//没有取得头部,报异常

else

{

failed = new InsufficientAuthenticationException(“请求头认证消息为空” );

unsuccessfulAuthentication(request, response, failed);

return;

}

token = StringUtils.removeStart(token, "Bearer " );

try

{

if (StringUtils.isNotBlank(token))

{

//组装令牌

JwtAuthenticationToken authToken = new JwtAuthenticationToken(JWT.decode(token));

//提交给AuthenticationManager进行令牌验证,获取认证后的令牌

passedToken = this.getAuthenticationManager()

.authenticate(authToken);

//取得认证后的用户信息,主要是用户id

UserDetails details = (UserDetails) passedToken.getDetails();

//通过details.getUsername()获取用户id,并作为请求属性进行缓存

request.setAttribute(SessionConstants.USER_IDENTIFIER, details.getUsername());

} else

{

failed = new InsufficientAuthenticationException(“请求头认证消息为空” );

}

} catch (JWTDecodeException e)

{

}

filterChain.doFilter(request, response);

}

}

AuthenticationManager将调用注册在内部的JwtAuthenticationProvider认证提供者,对JwtAuthenticationToken进行验证。

为了使得过滤器能够生效,必须将过滤器加入HTTP请求的过滤处理责任链,这一步可以通过实现一个AbstractHttpConfigurer配置类来完成。

第四步:定制一个HTTP的安全认证配置类(AbstractHttpConfigurer子类),将上一步定制的过滤器加入请求的过滤处理责任链。

package com.crazymaker.springcloud.base.security.configurer;

public class JwtAuthConfigurer<T extends JwtAuthConfigurer<T, B>, B extends HttpSecurityBuilder> extends AbstractHttpConfigu

{

private JwtAuthenticationFilter jwtAuthenticationFilter;

public JwtAuthConfigurer()

{

//创建认证过滤器

this.jwtAuthenticationFilter = new JwtAuthenticationFilter();

}

//将过滤器加入http过滤处理责任链

@Override

public void configure(B http) throws Exception

{

//获取Spring Security共享的AuthenticationManager实例

//将其设置到jwtAuthenticationFilter认证过滤器 jwtAuthenticationFilter.setAuthenticationManager(http.getSharedObject

jwtAuthenticationFilter.setAuthenticationFailureHandler(new AuthFailureHandler());

JwtAuthenticationFilter filter = postProcess(jwtAuthenticationFilter);

//将过滤器加入http过滤处理责任链

http.addFilterBefore(filter, LogoutFilter.class);

}

}

第五步:定义一个Spring Security安全配置类(

WebSecurityConfigurerAdapter子类),对Web容器的HTTP安全认证机制进行配置。这是最后一步,有两项工作:一是在HTTP安全策略上应用JwtAuthConfigurer配置实例;二是构造AuthenticationManagerBuilder认证管理者实例。这一步可以通过继承WebSecurityConfigurerAdapter适配器来完成。

package com.crazymaker.springcloud.cloud.center.zuul.config;

@ConditionalOnWebApplication

@EnableWebSecurity()

public class ZuulWebSecurityConfig extends WebSecurityConfigurerAdapter

{

//注入session存储实例,用于查找session(根据session id)

@Resource

RedisOperationsSessionRepository sessionRepository;

//配置HTTP请求的安全策略,应用DemoAuthConfigurer配置类实例

@Override

protected void configure(HttpSecurity http) throws Exception

{

http.csrf().disable()

.authorizeRequests()

.and()

.authorizeRequests().anyRequest().authenticated()

.and()

.formLogin().disable()

.sessionManagement().disable()

.cors()

.and()

//在HTTP安全策略上应用JwtAuthConfigurer配置类实例

.apply(new JwtAuthConfigurer<>()) .tokenValidSuccessHandler(jwtRefreshSuccessHandler()).permissi

.and()

.logout().disable()

.sessionManagement().disable();

}

//配置认证Builder,由其负责构造AuthenticationManager实例

//Builder所构造的AuthenticationManager实例将作为HTTP请求的共享对象

//可以通过http.getSharedObject(AuthenticationManager.class)来获取

@Override

protected void configure(AuthenticationManagerBuilder auth) throws Exception

{

//在Builder实例中加入自定义的Provider认证提供者实例

auth.authenticationProvider(jwtAuthenticationProvider());

}

//创建一个JwtAuthenticationProvider提供者实例

@DependsOn({“sessionRepository”})

@Bean(“jwtAuthenticationProvider”)

protected AuthenticationProvider jwtAuthenticationProvider()

{

return new JwtAuthenticationProvider(sessionRepository);

}

}

至此,一个基于JWT+Spring Security的用户认证处理流程就定义完了。但是,此流程仅仅涉及JWT令牌的认证,没有涉及JWT令牌的生成。一般来说,JWT令牌的生成需要由系统的UAA(用户账号与认证)服务(或者模块)负责完成。

Zuul网关与UAA微服务的配合

================

crazy-springcloud脚手架通过Zuul网关和UAA微服务相互结合来完成整个用户的登录与认证闭环流程。二者的关系大致为:

(1)登录时,UAA微服务负责用户名称和密码的验证并且将用户信息(包括令牌加密盐)放在分布式Session中,然后返回JWT令牌(含Session ID)给前台。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数网络安全工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
img

还有兄弟不知道网络安全面试可以提前刷题吗?费时一周整理的160+网络安全面试题,金九银十,做网络安全面试里的显眼包!

王岚嵚工程师面试题(附答案),只能帮兄弟们到这儿了!如果你能答对70%,找一个安全工作,问题不大。

对于有1-3年工作经验,想要跳槽的朋友来说,也是很好的温习资料!

【完整版领取方式在文末!!】

93道网络安全面试题

内容实在太多,不一一截图了

黑客学习资源推荐

最后给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

😝朋友们如果有需要的话,可以联系领取~

1️⃣零基础入门
① 学习路线

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

image

② 路线对应学习视频

同时每个成长路线对应的板块都有配套的视频提供:

image-20231025112050764

2️⃣视频配套工具&国内外网安书籍、文档
① 工具

② 视频

image1

③ 书籍

image2

资源较为敏感,未展示全面,需要的最下面获取

在这里插入图片描述在这里插入图片描述

② 简历模板

在这里插入图片描述

因篇幅有限,资料较为敏感仅展示部分资料,添加上方即可获取👆

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

频提供:

image-20231025112050764

2️⃣视频配套工具&国内外网安书籍、文档
① 工具

② 视频

image1

③ 书籍

image2

资源较为敏感,未展示全面,需要的最下面获取

在这里插入图片描述在这里插入图片描述

② 简历模板

在这里插入图片描述

因篇幅有限,资料较为敏感仅展示部分资料,添加上方即可获取👆

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-Pi6vV0vK-1713066042579)]

  • 20
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值