一、前言

  最近负责支付宝小程序后端项目设计,这里主要分享一下用户会话、接口鉴权的设计。参考过微信小程序后端的设计,会话需要依靠redis。相关的开发人员和我说依靠Redis并不是很靠谱,redis在业务高峰期不稳定,容易出现问题,总会出现用户会话丢失、超时的问题。之前听过JWT相关的设计,决定尝试一下。

二、什么是JWT

  JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。虽然JWT可以加密以在各方之间提供保密,但我们将专注于签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则隐藏其他方的声明。当使用公钥/私钥对签名令牌时,签名还证明只有持有私钥的一方是签署它的一方。

  更多参考:Introduction to JSON Web Tokens

三、JWT优势

  JWT支持多种方式的信息加密,验证时并不需要依赖缓存。支持存储用户非敏感信息、超时、刷新等操作,JWT由前端在用户发送请求时自动放入header中,可以有效避免CSRF攻击,用来维护服务端和用户会话再好也不过了。

四、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<String, Object> userDetail = (Map<String, Object>) SecurityContextHolder.getContext().getAuthentication().getDetails();
String userId = (String) userDetail.get("userId");
return userId;
}
}

  JWT工具类主要功能:token生成、token刷新、token解析、根据token中的用户标识提取用户信息。

五、Spring Security相关知识预热

  这个类定义了spring security内置的filter的优先级

final class FilterComparator implements Comparator<Filter>, Serializable {
private static final int STEP = 100;
private Map<String, Integer> filterToOrder = new HashMap<String, Integer>(); 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

六、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

七、自定义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中;

八、自定义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 DEFAULT_TOKEN = "ALIPAY#APPLET_DEFAULT#TOKEN[1qa2ws3ed!@#$%^]"; private String resolveToken(HttpServletRequest request) {
String token = request.getHeader(AlipayAppletSecurityConstants.TOKEN_HEADER_NAME);
if (StringUtils.isBlank(token)) {
throw new TokenNotFoundException("找不到Token, header name is " + AlipayAppletSecurityConstants.TOKEN_HEADER_NAME);
}
return token;
} public Authentication parseToken(HttpServletRequest request) {
String token = this.resolveToken(request); Object userDetail;
try {
if (!(token.startsWith(DEFAULT_TOKEN) && (userDetail = parseDefaultToken(token)) != null)) {
userDetail = JwtUtils.parseToken(token);
}
} catch (ExpiredJwtException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException(String.format("token解析异常..., token=%s", token), e);
} if (null == userDetail) {
throw new IllegalStateException("用户对象不能为null! token=" + token);
}
return new StatelessTokenAuthentication(userDetail);
} public Authentication tryRefreshAndParseToken(HttpServletRequest request, HttpServletResponse response) {
String token = this.resolveToken(request); String refreshToken;
try {
refreshToken = JwtUtils.refresh(token);
} catch (Exception e) {
throw new IllegalStateException("token刷新异常... token=" + token, e);
} Object userDetail;
try {
userDetail = JwtUtils.parseToken(refreshToken);
} catch (Exception e) {
throw new IllegalStateException("token解析异常..., refresh_token=" + refreshToken, e);
} if (null == userDetail) {
throw new IllegalStateException("用户对象不能为null! refresh_token=" + refreshToken);
} response.addHeader(AlipayAppletSecurityConstants.RFRESH_TOKEN_HEADER_NAME, refreshToken);
return new StatelessTokenAuthentication(userDetail);
} private static Object parseDefaultToken(String token) {
String[] session = token.split(":");
if (session.length == 2) {
LOGGER.info("alipay applet default token info is " + token);
return new HashMap<String, Object>() {
{
put("userId", session[1]);
}
};
} else {
LOGGER.error(String.format("alipay applet default token= %s 不合法", token));
}
return null;
}
}
  • 解析JWT,获取用户信息;
  • 刷新JWT,通知前端,保证会话不会断开;
  • 默认Token侧率,避免测试接口不必要的麻烦;

九、测试结果

  

  

  

十、总结

  这一次后端鉴权模块的设计也是属于自己的一次突破吧,前后端的联调没有出现太大的岔子。最终顺利的上线了!!!另外分享一下在阅读spring security源码时的收获:AutowireBeanFactoryObjectPostProcessor。对,没错,就是这个对象后置处理器。如果你阅读了spring security的源码,你会发现很多对象,比如WebSecurity、ProviderManager、各个安全Filter等,这些对象的创建并不是通过bean定义的形式被容器发现和注册进入spring容器的,而是直接new出来的。AutowireBeanFactoryObjectPostProcessor这个工具类可以使这些对象具有容器bean同样的生命周期,也能注入相应的依赖,从而进入准备好被使用的状态。参考Spring Security Config 5.1.2 源码解析 -- 工具类 AutowireBeanFactoryObjectPostProcessor

SpringSecurity整合JWT的更多相关文章

  1. SpringSecurity权限管理系统实战—六、SpringSecurity整合jwt

    目录 SpringSecurity权限管理系统实战-一.项目简介和开发环境准备 SpringSecurity权限管理系统实战-二.日志.接口文档等实现 SpringSecurity权限管理系统实战-三 ...

  2. SpringSecurity 整合 JWT

    项目集成Spring Security(一) 在上一篇基础上继续集成 JWT ,实现用户身份验证. 前言 前后端分离项目中,如果直接把 API 接口对外开放,我们知道这样风险是很大的,所以在上一篇中我 ...

  3. 【SpringBoot技术专题】「JWT技术专区」SpringSecurity整合JWT授权和认证实现

    JWT基本概念 JWT,即 JSON Web Tokens(RFC 7519),是一个广泛用于验证 REST APIs 的标准.虽说是一个新兴技术,但它却得以迅速流行. JWT的验证过程是: 前端(客 ...

  4. SpringBoot整合SpringSecurity实现JWT认证

    目录 前言 目录 1.创建SpringBoot工程 2.导入SpringSecurity与JWT的相关依赖 3.定义SpringSecurity需要的基础处理类 4. 构建JWT token工具类 5 ...

  5. SpringSecurity之整合JWT

    SpringSecurity之整合JWT 目录 SpringSecurity之整合JWT 1. 写在前面的话 2. JWT依赖以及工具类的编写 3. JWT过滤器 4. 登录成功结果处理器 5. Sp ...

  6. 基于SpringSecurity和JWT的用户访问认证和授权

    发布时间:2018-12-03   技术:springsecurity+jwt+java+jpa+mysql+mysql workBench   概述 基于SpringSecurity和JWT的用户访 ...

  7. Spring Security整合JWT,实现单点登录,So Easy~!

    前面整理过一篇 SpringBoot Security前后端分离,登录退出等返回json数据,也就是用Spring Security,基于SpringBoot2.1.4 RELEASE前后端分离的情况 ...

  8. SpringBoot + SpringSecurity + Mybatis-Plus + JWT实现分布式系统认证和授权

    1. 简介   Spring Security是一个功能强大且易于扩展的安全框架,主要用于为Java程序提供用户认证(Authentication)和用户授权(Authorization)功能.    ...

  9. SpringBoot + SpringSecurity + Mybatis-Plus + JWT + Redis 实现分布式系统认证和授权(刷新Token和Token黑名单)

    1. 前提   本文在基于SpringBoot整合SpringSecurity实现JWT的前提中添加刷新Token以及添加Token黑名单.在浏览之前,请查看博客:   SpringBoot + Sp ...

随机推荐

  1. Java测试代码(很不完整,建议大家别看,过几天会再发一次难的版本)

    package ATM;  import java.io.BufferedReader;  import java.io.InputStreamReader;  class Account{ priv ...

  2. usrp-B210

    sudo add-apt-repository ppa:ettusresearch/uhd sudo apt-get update sudo apt-get install libuhd-dev li ...

  3. git教程笔记(二)

    1.首先进入自己的项目文件 在GitHub上申请一个自己的账户信息.创建一个Repository.然后复制仓库的地址在gitbush中进行clone gitbush中进行远程clone 2.gitbu ...

  4. docker日志清理

    前言:docker运行久了,会发现它的映射磁盘空间爆满,尤其是yum安装的docker的 解决方法: 1. 用脚本清理,一般yum安装的docker,其存储空间一般都在/var/lib/docker/ ...

  5. windows客户端走代理上网

    前提:在大型网络中,由于众多服务器及安全性考虑,内网服务器是不能上外网的,但是为了满足某些服务的需要,一定会搭建代理服务器的. 以下是windows客户端走代理服务器的操作:       两下确定就可 ...

  6. k-近邻算法-手写识别系统

    手写数字是32x32的黑白图像.为了能使用KNN分类器,我们需要把32x32的二进制图像转换为1x1024 1. 将图像转化为向量 from numpy import * # 导入科学计算包numpy ...

  7. SqlServerHelp

    using System; using System.Collections.Generic; using System.Reflection; using System.Text; using Sy ...

  8. 集群部署时的分布式session如何实现

    tomcat + redis 这个其实还挺方便的,就是使用session的代码跟以前一样,还是基于tomcat原生的session支持即可,然后就是用一个叫做Tomcat RedisSessionMa ...

  9. MVC异常处理(异常捕获)

    1.cshtml页面异常 2.Controller异常 3.路由参数异常. 4.页面不存在404 页面不存在404,可以通过配置config来处理 <customErrors mode=&quo ...

  10. [转] Javascript 原型链

    1. 类 在C或者Java里,int a;定义了一个int类型的变量a.其中int是类型的名字,a是具体的变量. Javascript 模仿自 Java, 有一部分面向对象编程的部分.在面向对象的编程 ...