SpringSecurity整合JWT

一、前言

  最近负责支付宝小程序后端项目设计,这里主要分享一下用户会话、接口鉴权的设计。参考过微信小程序后端的设计,会话需要依靠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 {@Overridepublic void configure(WebSecurity web) {web.ignoring().antMatchers("/alipay-applet/login");web.ignoring().antMatchers("/alipay-applet/ag");web.ignoring().regexMatchers("^(?!(/alipay-applet)).*$");}@Overrideprotected void configure(AuthenticationManagerBuilder auth) {auth.authenticationProvider(new TokenAuthenticationProvider(new SecurityProviderManager()));}@Overrideprotected 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);}@Beanpublic 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;}@Overrideprotected 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;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {StatelessTokenAuthentication tokenAuth = (StatelessTokenAuthentication) authentication;StatelessTokenAuthentication.Credentials credentials = (StatelessTokenAuthentication.Credentials) tokenAuth.getCredentials();//查找TokenHttpServletRequest 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);}}@Overridepublic 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

 

转载于:https://www.cnblogs.com/hujunzheng/p/10287250.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/531203.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Springboot定时任务原理及如何动态创建定时任务

一、前言 上周工作遇到了一个需求&#xff0c;同步多个省份销号数据&#xff0c;解绑微信粉丝。分省定时将销号数据放到SFTP服务器上&#xff0c;我需要开发定时任务去解析文件。因为是多省份&#xff0c;服务器、文件名规则、数据规则都不一定&#xff0c;所以要做成可配置是有…

转载:ThreadPoolExecutor 源码阅读

前言 之前研究了一下如何使用ScheduledThreadPoolExecutor动态创建定时任务(Springboot定时任务原理及如何动态创建定时任务)&#xff0c;简单了解了ScheduledThreadPoolExecutor相关源码。今天看了同学写的ThreadPoolExecutor 的源码解读&#xff0c;甚是NB&#xff0c;必须转…

使用pdfBox实现pdf转图片,解决中文方块乱码等问题

一、引入依赖 <dependency><groupId>org.apache.pdfbox</groupId><artifactId>fontbox</artifactId><version>2.0.13</version> </dependency> <dependency><groupId>org.apache.pdfbox</groupId><artif…

Spring异步调用原理及SpringAop拦截器链原理

一、Spring异步调用底层原理 开启异步调用只需一个注解EnableAsync Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) Documented Import(AsyncConfigurationSelector.class) public interface EnableAsync {/*** Indicate the async annotation type to be detec…

Spring MVC源码——Root WebApplicationContext

Spring MVC源码——Root WebApplicationContext 打算开始读一些框架的源码,先拿 Spring MVC 练练手,欢迎点击这里访问我的源码注释, SpringMVC官方文档一开始就给出了这样的两段示例: WebApplicationInitializer示例: public class MyWebApplicationInitializer implements Web…

Spring MVC源码——Servlet WebApplicationContext

上一篇笔记(Spring MVC源码——Root WebApplicationContext)中记录了下 Root WebApplicationContext 的初始化代码.这一篇来看 Servlet WebApplicationContext 的初始化代码 DispatcherServlet 是另一个需要在 web.xml 中配置的类, Servlet WebApplicationContext 就由它来创建…

Springboot源码——应用程序上下文分析

前两篇(Spring MVC源码——Root WebApplicationContext 和 Spring MVC源码——Servlet WebApplicationContext)讲述了springmvc项目创建上下文的过程&#xff0c;这一篇带大家了解一下springboot项目创建上下文的过程。 SpringApplication引导类 SpringApplication类用于启动或…

基于zookeeper实现分布式配置中心(一)

最近在学习zookeeper&#xff0c;发现zk真的是一个优秀的中间件。在分布式环境下&#xff0c;可以高效解决数据管理问题。在学习的过程中&#xff0c;要深入zk的工作原理&#xff0c;并根据其特性做一些简单的分布式环境下数据管理工具。本文首先对zk的工作原理和相关概念做一下…

基于zookeeper实现分布式配置中心(二)

上一篇&#xff08;基于zookeeper实现分布式配置中心&#xff08;一&#xff09;&#xff09;讲述了zookeeper相关概念和工作原理。接下来根据zookeeper的特性&#xff0c;简单实现一个分布式配置中心。 配置中心的优势 1、各环境配置集中管理。 2、配置更改&#xff0c;实时推…

Redis分布式锁实战

背景 目前开发过程中&#xff0c;按照公司规范&#xff0c;需要依赖框架中的缓存组件。不得不说&#xff0c;做组件的大牛对CRUD操作的封装&#xff0c;连接池、缓存路由、缓存安全性的管控都处理的无可挑剔。但是有一个小问题&#xff0c;该组件没有对分布式锁做实现&#xff…

基于RobotFramework实现自动化测试

Java robotframework seleniumlibrary 使用Robot Framework Maven Plugin&#xff08;http://robotframework.org/MavenPlugin/&#xff09;执行自动化测试chromedriver下载&#xff1a; http://chromedriver.storage.googleapis.com/index.htmlchromedriver和chrome版本对应…

Springboot国际化信息(i18n)解析

国际化信息理解 国际化信息也称为本地化信息 。 Java 通过 java.util.Locale 类来表示本地化对象&#xff0c;它通过 “语言类型” 和 “国家/地区” 来创建一个确定的本地化对象 。举个例子吧&#xff0c;比如在发送一个具体的请求的时候&#xff0c;在header中设置一个键值对…

C语言一看就能上手的干货!你确定你不来看吗?

本地环境设置 如果您想要设置 C 语言环境&#xff0c;您需要确保电脑上有以下两款可用的软件&#xff0c;文本编辑器和 C 编译器。 文本编辑器 这将用于输入您的程序。文本编辑器包括 Windows Notepad、OS Edit command、Brief、Epsilon、EMACS 和 vim/vi。文本编辑器的名称…

10万码农五年的C语言笔记!你现在知道别人为什么这么优秀了吗?

c语言对许多同学来说确实是一门比较难学的课程&#xff0c;不仅抽象&#xff0c;而且繁琐&#xff0c;但这又是一门不得不学的课程。前两节可能还有兴致听一听&#xff0c;然而&#xff0c;再过几节课就是一脸蒙比。凭空要想出一道题的算法和程序&#xff0c;根本无从下手。 所…

C语言/C++编程学习:C语言环境设置!

C语言是面向过程的&#xff0c;而C&#xff0b;&#xff0b;是面向对象的 C和C的区别&#xff1a; C是一个结构化语言&#xff0c;它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程&#xff0c;对输入&#xff08;或环境条件&#xff09;进行运算处理得…

C语言指针原来也可以这么的通俗易懂!

C语言是面向过程的&#xff0c;而C&#xff0b;&#xff0b;是面向对象的 C和C的区别&#xff1a; C是一个结构化语言&#xff0c;它的重点在于算法和数据结构。C程序的设计首要考虑的是如何通过一个过程&#xff0c;对输入&#xff08;或环境条件&#xff09;进行运算处理得…

C语言过时了?你在做梦?

为什么要使用C语言&#xff1f; 在过去的四十年里&#xff0c;C语言已经成为世界上最流行、最重要的一种编程语言。 C是一种融合了控制特性的现代语言&#xff0c;而我们已发现在计算机科学的理论和实践中&#xff0c;控制特性是很重要的。其设计使得用户可以自然地采用自顶向…

C语言深入理解!助你向大佬迈进!

Dennis Ritchie 过世了&#xff0c;他发明了C语言&#xff0c;一个影响深远并彻底改变世界的计算机语言。一门经历40多年的到今天还长盛不衰的语言&#xff0c;今天很多语言都受到C的影响&#xff0c;C&#xff0c;Java&#xff0c;C#&#xff0c;Perl&#xff0c; PHP&#xf…

【初涉C语言】程序员欢迎来到C语言的世界!

计算机发展史 机器语言所有的代码里面只有0和1优点&#xff1a;直接对硬件产生作用&#xff0c;程序的执行效率非常高缺点&#xff1a;指令又多又难记、可读性差、无可移植性汇编语言符号化的机器语言&#xff0c;用一个符号&#xff08;英文单词、数字&#xff09;来代表一条…

C语言和C++的区别整理详解!

c和c主要区别 根据书中的描述&#xff0c;进行了整理 推荐一个我自己的C/C交流裙815393895 1、 源代码文件的扩展名 摘自1.4.1 C实现源代码文件的扩展名UNIXC、cc、cxx、cGNU CC、cc、cxx、cpp、cDigital Marscpp、cxxBorland CcppWatcomcppMicrosoft Visual Ccpp、cxx、cc…