SpringSecurity整合JWT

v一、前言   最近负责支付宝小程序后端项目设计,这里主要分享一下用户会话、接口鉴权的设计。参考过微信小程序后端的设计,会话需要依靠redis。相关的开发人员和我说依靠Redis并不是很靠谱,redis在业务高峰期不稳定,容易出现问题,总会出现用户会话丢失、超时的问题。之前听过JWT相关的设计,决定尝试一下。 v二、什么是JWT   JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。虽然JWT可以加密以在各方之间提供保密,但我们将专注于签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则隐藏其他方的声明。当使用公钥/私钥对签名令牌时,签名还证明只有持有私钥的一方是签署它的一方。   更多参考:Introduction to JSON Web Tokens v三、JWT优势   JWT支持多种方式的信息加密,验证时并不需要依赖缓存。支持存储用户非敏感信息、超时、刷新等操作,JWT由前端在用户发送请求时自动放入header中,可以有效避免CSRF攻击,用来维护服务端和用户会话再好也不过了。 v四、JWT工具类 复制代码 public class JwtUtils { /** * 创建token * * @param claim claim中为userId * @param secret 创建token密钥 * @return token */ public static String createToken(Map claim, String secret) { long expirationDate = AlipayServiceAppletConstants.EXPIRATION_DATE; LocalDateTime nowTime = LocalDateTime.now(); return Jwts.builder().setClaims(claim) .setSubject("AlipayApplet") //设置token主题 .setIssuedAt(localDateTimeToDate(nowTime)) //设置token发布时间 .setExpiration(getExpirationDate(nowTime, expirationDate)) // 设置token过期时间 .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 将LocalDateTime转换为Date * * @param localDateTime * @return Date */ public static Date localDateTimeToDate(LocalDateTime localDateTime) { ZoneId zoneId = ZoneId.systemDefault(); ZonedDateTime zdt = localDateTime.atZone(zoneId); return Date.from(zdt.toInstant()); } /** * 获取token过期的时间 * * @param createTime token创建时间 * @param calendarInterval token有效时间间隔 * @return */ public static Date getExpirationDate(LocalDateTime createTime, long calendarInterval) { LocalDateTime expirationDate = createTime.plus(calendarInterval, ChronoUnit.MINUTES); return localDateTimeToDate(expirationDate); } /** * JWT 解析token是否正确 * * @param token * @return * @throws Exception */ public static Claims parseToken(String token) throws ExpiredJwtException { Claims claims = Jwts.parser() .setSigningKey(AlipayServiceAppletConstants.ALIPAY_APPLET_SECRET) .parseClaimsJws(token) .getBody(); return claims; } /** * token 刷新: * 1.小于TIME_OUT直接通过; * 2.大于TIME_OUT 小于FORBID_REFRES_HTIME需要刷新; * 3.超过FORBID_REFRES_HTIME 直接返回禁用刷新; * * @param oldToken * @return */ public static String refresh(String oldToken) { long tokenDurationTime = AlipayServiceAppletConstants.EXPIRATION_DATE;//token持续时间/分钟 long tokenRefreshDurationTime = AlipayServiceAppletConstants.ALIPAY_APPLET_FORBID_REFRES_HTIME;//token允许刷新时间/分钟 try { getExpirationDate(oldToken); } catch (ExpiredJwtException e) { try { long expirationTime = TimeUnit.MINUTES.convert(e.getClaims().getExpiration().toInstant().getEpochSecond(), TimeUnit.SECONDS); long nowTime = TimeUnit.MINUTES.convert(Instant.now().getEpochSecond(), TimeUnit.SECONDS); long tokenTimeout = nowTime - expirationTime; /*2.大于TIME_OUT 小于FORBID_REFRES_HTIME需要刷新*/ if (tokenTimeout >= tokenDurationTime && tokenTimeout <= tokenRefreshDurationTime) { return createToken(e.getClaims(), AlipayServiceAppletConstants.ALIPAY_APPLET_SECRET); } } catch (Exception ex) { throw new RuntimeException("会话刷新异常...", ex); } } /*3.超过FORBID_REFRES_HTIME 直接返回禁用刷新*/ throw new RuntimeException("会话不允许刷新..."); } public static Date getExpirationDate(String token) throws ExpiredJwtException { Claims claims = parseToken(token); Date expiration = claims.getExpiration(); return expiration; } public static String resolveUserId() { Assert.notNull(SecurityContextHolder.getContext().getAuthentication(), "授权信息不能为NULL."); Map userDetail = (Map) SecurityContextHolder.getContext().getAuthentication().getDetails(); String userId = (String) userDetail.get("userId"); return userId; } } 复制代码   JWT工具类主要功能:token生成、token刷新、token解析、根据token中的用户标识提取用户信息。 v五、Spring Security相关知识预热   这个类定义了spring security内置的filter的优先级 复制代码 final class FilterComparator implements Comparator, Serializable { private static final int STEP = 100; private Map filterToOrder = new HashMap(); FilterComparator() { int order = 100; put(ChannelProcessingFilter.class, order); order += STEP; put(ConcurrentSessionFilter.class, order); order += STEP; put(WebAsyncManagerIntegrationFilter.class, order); order += STEP; put(SecurityContextPersistenceFilter.class, order); order += STEP; put(HeaderWriterFilter.class, order); order += STEP; put(CorsFilter.class, order); order += STEP; put(CsrfFilter.class, order); order += STEP; put(LogoutFilter.class, order); order += STEP; put(X509AuthenticationFilter.class, order); order += STEP; put(AbstractPreAuthenticatedProcessingFilter.class, order); order += STEP; filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order); order += STEP; put(UsernamePasswordAuthenticationFilter.class, order); order += STEP; put(ConcurrentSessionFilter.class, order); order += STEP; filterToOrder.put( "org.springframework.security.openid.OpenIDAuthenticationFilter", order); order += STEP; put(DefaultLoginPageGeneratingFilter.class, order); order += STEP; put(ConcurrentSessionFilter.class, order); order += STEP; put(DigestAuthenticationFilter.class, order); order += STEP; put(BasicAuthenticationFilter.class, order); order += STEP; put(RequestCacheAwareFilter.class, order); order += STEP; put(SecurityContextHolderAwareRequestFilter.class, order); order += STEP; put(JaasApiIntegrationFilter.class, order); order += STEP; put(RememberMeAuthenticationFilter.class, order); order += STEP; put(AnonymousAuthenticationFilter.class, order); order += STEP; put(SessionManagementFilter.class, order); order += STEP; put(ExceptionTranslationFilter.class, order); order += STEP; put(FilterSecurityInterceptor.class, order); order += STEP; put(SwitchUserFilter.class, order); } //...... } 复制代码   Spring Security 的permitAll以及webIgnore的区别 web ignore比较适合配置前端相关的静态资源,它是完全绕过spring security的所有filter的; 而permitAll,会给没有登录的用户适配一个AnonymousAuthenticationToken,设置到SecurityContextHolder,方便后面的filter可以统一处理authentication。 参考链接:https://segmentfault.com/a/1190000012160850   Spring Security Authentication (认证)原理 AuthenticationManager通过委托AuthenticationProvider来实现认证; AuthenticationProvider会调用UserDetailsService拿到UserDetails对象并封装最终的 Authentication 对象放到SecurityContextHolder中; SecurityContextHolder 是 Spring Security 最基础的对象,用于存储应用程序当前安全上下文的详细信息,这些信息后续会被用于授权;   参考链接:https://www.jianshu.com/p/e8e0e366184e v六、SpringSecurity基本配置 复制代码 @Configuration public class AlipayAppletSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) { web.ignoring().antMatchers("/alipay-applet/login"); web.ignoring().antMatchers("/alipay-applet/ag"); web.ignoring().regexMatchers("^(?!(/alipay-applet)).*$"); } @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(new TokenAuthenticationProvider(new SecurityProviderManager())); } @Override protected void configure(HttpSecurity http) throws Exception { //禁用缓存 http.headers().cacheControl(); http.csrf().disable() .authorizeRequests() .antMatchers("/alipay-applet/**").authenticated() .and() .formLogin().disable() //不要UsernamePasswordAuthenticationFilter .httpBasic().disable() //不要BasicAuthenticationFilter .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .securityContext().and() .anonymous().disable() .servletApi(); AuthenticationManager authenticationManager = authenticationManager(); TokenAuthenticationFilter filter = new TokenAuthenticationFilter(authenticationManager); http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class); } @Bean public CorsFilter corsFilter() { //1.添加CORS配置信息 CorsConfiguration config = new CorsConfiguration(); //放行哪些原始域 config.addAllowedOrigin("*"); //是否发送Cookie信息 config.setAllowCredentials(true); //放行哪些原始域(请求方式) config.addAllowedMethod("*"); //放行哪些原始域(头部信息) config.addAllowedHeader("*"); //暴漏刷新token的header config.addExposedHeader(AlipayAppletSecurityConstants.RFRESH_TOKEN_HEADER_NAME); //2.添加映射路径 UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/alipay-applet/**", config); //3.返回新的CorsFilter. return new CorsFilter(configSource); } } 复制代码 web ignore配置:忽略非支付宝后端服务的请求、忽略用户登录的请求、忽略支付宝回调请求; 添加自定义AuthenticationProvider; 禁用缓存、不启用CSRF配置(因为是基于token认证,不用担心csrf攻击)、去掉UsernamePasswordAuthenticationFilter和BasicAuthenticationFilter、session策略为STATELESS、禁止匿名访问; CORS设置(针对支付宝小程序后端服务),暴露指定的response header; 添加自定义AuthenticationFilter v七、自定义AuthenticationFilter 复制代码 class TokenAuthenticationFilter extends OncePerRequestFilter { private static Logger LOGGER = LoggerFactory.getLogger(TokenAuthenticationFilter.class); private final AuthenticationManager authenticationManager; public TokenAuthenticationFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException { try { if (SecurityContextHolder.getContext().getAuthentication() != null) { filterChain.doFilter(request, response); //已经完成认证 return; } StatelessTokenAuthentication authentication = new StatelessTokenAuthentication(request, response); Authentication authResult = authenticationManager.authenticate(authentication); Assert.isTrue(authResult.isAuthenticated(), "Token is not authenticated!"); SecurityContextHolder.getContext().setAuthentication(authResult); filterChain.doFilter(request, response); } catch (Exception e) { LOGGER.error("TokenAuthenticationFilter异常...", e); try { WmhcomplexmsgcenterErrorHandler.handleCore(request, response, e); } catch (ServiceException ex) { throw new ServletException(ex); } } } } 复制代码 通过SecurityContextHolder.getContext().getAuthentication() != null来判断当前请求是否已经被认证; 构造需要认证的StatelessTokenAuthentication用户凭证信息; 通过AuthenticationManager 验证用户凭证并 返回认证后StatelessTokenAuthentication信息,并绑定到SecurityContextHolder中; v八、自定义AuthenticationProvider 复制代码 class TokenAuthenticationProvider implements AuthenticationProvider { private final SecurityProviderManager providerManager; public TokenAuthenticationProvider(SecurityProviderManager providerManager) { this.providerManager = providerManager; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { StatelessTokenAuthentication tokenAuth = (StatelessTokenAuthentication) authentication; StatelessTokenAuthentication.Credentials credentials = (StatelessTokenAuthentication.Credentials) tokenAuth.getCredentials(); //查找Token HttpServletRequest request = credentials.getRequest(); try { return providerManager.parseToken(request); } catch (ExpiredJwtException e) { HttpServletResponse response = credentials.getResponse(); try { return providerManager.tryRefreshAndParseToken(request, response); } catch (Exception ex) { throw new InternalAuthenticationServiceException("重新鉴权出错,请重新登陆...", ex); } } catch (Exception e) { throw new InternalAuthenticationServiceException("鉴权出错,请重新登陆...", e); } } @Override public boolean supports(Class authentication) { return ClassUtils.isAssignable(StatelessTokenAuthentication.class, authentication); } } 复制代码 验证StatelessTokenAuthentication信息【解析JWT】; JWT过期,在一定时间范围内,自动刷新JWT并写入response header中; 复制代码 class SecurityProviderManager { private static Logger LOGGER = LoggerFactory.getLogger(SecurityProviderManager.class); private static final String D
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信