写点什么

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

用户头像
极客good
关注
发布于: 刚刚

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 a


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


lgorithm = 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<B>> 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)给前台。


(2)认证时,前台请求带上 JWT 令牌,Zuul 网关能根据令牌中的 Session ID 取出分布式 Session 中的加密盐,对 JWT 令牌进行验证。在 crazy-springcloud 脚手架的会话架构中,Zuul 网关必须能和 UAA 微服务进行会话的共享,如图 6-7 所示。



图 6-7 Zuul 网关和 UAA 微服务进行会话的共享


在 crazy-springcloud 的 UAA 微服务提供者 crazymaker-uaa 实现模块中,controller(控制层)的 REST 登录接口的定义如下:


@Api(value = "用户端登录与退出", tags = {"用户信息、基础学习 DEMO"})


@RestController


@RequestMapping("/api/session" )


public class SessionController


{


//用户端会话服务


@Resource


private FrontUserEndSessionServiceImpl userService;


//用户端的登录 REST 接口


@PostMapping("/login/v1" )


@ApiOperation(value = "用户端登录" )


public RestOut<LoginOutDTO> login(@RequestBody LoginInfoDTO loginInfoDTO, HttpServlet


{


//调用服务层登录方法获取令牌


LoginOutDTO dto = userService.login(loginInfoDTO);


response.setHeader("Content-Type", "text/html;charset=utf-8" );


response.setHeader(SessionConstants.AUTHORIZATION_HEAD, dto.getToken());


return RestOut.success(dto);


}


...


}


用户登录时,在服务层,客户端会话服务


FrontUserEndSessionServiceImpl 负责从用户数据库中获取用户,然后进行密码验证。

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
微服务网关与用户身份识别,JWT+Spring Security进行网关安全认证