当前位置: 首页 > 后端技术 > Java

实战!SpringBootSecurity+JWT前后端分离架构登录认证!

时间:2023-04-01 13:31:47 Java

大家好,我是Chen~认证授权是实际项目中必不可少的部分,而SpringSecurity会是首选的安全组件,所以Chen新开了一个专栏《Spring Security 进阶》,写一篇Writeauthenticationandauthorizationfrommonolithic体系结构到OAuth2分布式体系结构。SpringSecurity这里就不做过多介绍了。相信大家都用过,也害怕过。与Shiro相比,SpringSecurity更加重量级。之前的SSM项目更多的企业使用Shiro,但是SpringBoot出来后,集成SpringSecurity更加方便,使用的企业也更多。今天小陈就来介绍一下在一个前后端分离的项目中如何使用SpringSecurity进行登录认证。文章内容如下:前后端分离认证的思路前后端分离不同于传统的web服务,不能使用session。所以我们使用JWT这种无状态的机制来生成token。大致思路如下:客户端调用服务端登录接口,输入用户名和密码登录,登录成功后返回两个token,如下:serverrefreshToken:刷新令牌,一旦accessToken过期,客户端需要使用refreshToken重新获取accessToken。所以refreshToken的过期时间一般要比accessToken长。客户端在请求头中携带accessToken访问服务端的资源,服务端对accessToken进行鉴权(签名验证,是否无效...),如果accessToken没有问题就放行。一旦accessToken过期,客户端需要携带refreshToken调用refreshtoken接口获取新的accessToken。Chen使用SpringBoot框架来构建项目。演示项目创建了两个新模块,即common-base和security-authentication-jwt。1.common-base模块是抽象出来的公共模块。该模块主要包含一些常用的类。目录如下:2.security-authentication-jwt模块有一些类需要自定义,比如security全局配置类,Jwt登录过滤器的配置类,目录如下:3.五个表权限设计往往根据业务需要有不同的设计。Chen使用的RBAC规范主要涉及到五张表,分别是usertable,roletable,andpermissiontable,user<->roletable,role<->permissiontable,如下图所示:以上表的SQL会放在案例源码中(这些表的字段不是为了图省事,可以根据业务逐步扩充的。)登录鉴权过滤器的登录界面的逻辑写法有很多种。今天Chen介绍了一个使用过滤器定义的登录界面。SpringSecurity默认的表单登录认证过滤器是UsernamePasswordAuthenticationFilter,不适合前后端分离的架构,所以我们需要自定义一个过滤器。逻辑很简单。参考UsernamePasswordAuthenticationFilter修改过滤器。代码如下:AuthenticationSuccessHandler上面的filter接口一旦认证成功,就会调用AuthenticationSuccessHandler进行处理,所以我们可以自定义一个认证成功handler来进行自己的业务处理,代码如下:Chen只返回accessToken和refreshToken,其他业务逻辑处理由自己完成。认证失败处理程序AuthenticationFailureHandler类似的,一旦登录失败,比如用户名密码错误等,会调用AuthenticationFailureHandler进行处理,所以我们需要自定义一个认证失败处理程序,返回具体的JSON数据给客户端,代码如下:逻辑很简单,AuthenticationException有不同的实现类,可以根据异常的类型返回具体的提示信息。AuthenticationEntryPoint配置AuthenticationEntryPoint接口。当用户未经认证访问受保护的资源时,会调用commence()方法进行处理。比如客户端携带的token被篡改了,那么我们需要自定义一个AuthenticationEntryPoint来返回特定的提示信息,代码如下:权限不够,就会进入这个处理器进行处理,我们可以实现这个处理器返回具体的提示信息给客户端,代码如下:UserDetailsS??ervice配置UserDetailsS??ervice这个类用来加载用户信息,包括用户名,密码,authority,roleset...方法之一如下:UserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException;在认证逻辑中,SpringSecurity会根据客户端传入的用户名,调用该方法加载用户的详细信息。该方法中需要完成的逻辑如下:密码匹配加载权限,角色设置。我们需要实现这个接口来从数据库中加载用户信息。代码如下:LoginService根据用户名,从数据库中查询密码、角色和权限。代码如下:UserDetails也是一个接口,里面定义了几个方法,都是围绕用户名、密码、权限+角色集这三个属性展开的,所以我们可以实现这个类来扩展这些字段。SecurityUser代码如下:扩展:UserDetailsS??ervice类的实现一般涉及到5张表,分别是用户表、角色表、权限表、用户<->角色对应表、角色<->权限对应表,实现在企业必须遵循RBAC设计规则。这个规则后面会详细介绍。Token验证过滤器客户端的请求头中携带了token,服务端每次请求都要解析验证token,所以必须定义一个Token过滤器。该过滤器的主要逻辑如下:从请求头中获取accessToken解析后验证签名,并验证accessToken的过期时间,验证成功,并将鉴权保存在ThreadLocal中,使其方便以后直接获取用户的详细信息。以上只是最基本的逻辑,在实际开发中还有具体的处理,比如将用户的详细信息放入Request属性和Redis缓存中,从而实现feign的token中继效果。验证过滤器的代码如下:刷新令牌接口的accessToken一旦过期,客户端必须携带refreshToken重新获取令牌。传统的webservice是放在cookie中,只需要服务端完成刷新,完全不感知token。刷新,但是在前后端分离架构下,客户端必须使用refreshToken调用接口手动刷新。代码如下:主要逻辑很简单,如下:验证refreshToken并重新生成accessToken,并将refreshToken返回给客户端。注意:实际生产中的refreshToken令牌生成方式和加密算法可以和accessToken不同。登录鉴权过滤器接口配置上面定义了一个鉴权过滤器JwtAuthenticationLoginFilter,它是一个登录的过滤器,但是并没有注入到SpringSecurity添加的过滤器链中。需要定义配置。代码如下:/***@作者公众号:码猿科技专栏*登录过滤器的配置类*/@ConfigurationpublicclassJwtAuthenticationSecurityConfigextendsSecurityConfigurerAdapter{/***userDetailService*/@Qualifier("jwtTokenUserDetailsS??ervice")@AutoServicewireduserprivate*UserDetail/****登录成功处理器*/@AutowiredprivateLoginAuthenticationSuccessHandlerloginAuthenticationSuccessHandler;/***登录失败处理程序*/@AutowiredprivateLoginAuthenticationFailureHandlerloginAuthenticationFailureHandler;/***加密*/@AutowiredprivatePasswordEncoderpasswordEncoder;/***登录接口过滤器在过滤器链中配置*1.配置登录成功和失败处理器*2.配置自定义userDetailService(从数据库获取用户数据)*3.配置自定义过滤器在spring中进行过滤security在过滤器链中,在UsernamePasswordAuthenticationFilter*@paramhttp之前配置*/@Overridepublicvoidconfigure(HttpSecurityhttp){JwtAuthenticationLoginFilterfilter=newJwtAuthenticationLoginFilter();filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));//认证成功处理器filter.setAuthenticationSuccessHandler(loginAuthenticationSuccess);//认证失败处理器filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);//直接使用DaoAuthenticationProviderDaoAuthenticationProviderprovider=newDaoAuthenticationProvider();//设置用户详情服务provider.setUserDetailsS??ervice(userDetailsS??ervice);//设置加密算法provider.setPasswordEncoder(;passwordEncoderhttpProvider)provider);//在将此过滤器添加到UsernamePasswordAuthenticationFilter之前执行http.addFilterBefore(filter,UsernamePasswordAuthenticationFilter.class);}}所有的逻辑都在publicvoidconfigure(HttpSecurityhttp)方法中,如下:setauthenticationsuccesshandlerloginAuthenticationSuccessHandler设置认证失败handlerloginAuthenticationFailureHandler设置userDetailService的实现类JwtTokenUserDetailsS??ervice设置加密算法passwordEncoder在过滤器链中添加JwtAuthenticationLoginFilter过滤器,在SpringSecurity全局配置之前直接添加UsernamePasswordAuthenticationFilter过滤器上面只配置了登录过滤器对于设备,需要在全局配置类中做一些配置,如下:应用loginfilter的配置释放登录界面和token刷新界面,不需要拦截配置AuthenticationEntryPoint、AccessDeniedHandler禁用session,并且前后端分离+JWT方式不需要session将token验证过滤器TokenAuthenticationFilter添加到过滤器链中,在UsernamePasswordAuthenticationFilter之前。完整配置如下:/***@author公众号:码猿技术专栏*@EnableGlobalMethodSecurity注解开启权限验证*/@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true)publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateJwtAuthenticationSecurityConfigjwtAuthenticationSecurityConfig;@AutowiredprivateEntryPointUnauthorizedHandlerentryPointUnauthorizedHandler;@AutowiredprivateRequestAccessDeniedHandlerrequestAccessDeniedHandler;@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http.formLogin()//禁止使用表单登录,前后端分离使用不上.disable()//应用登录过滤器的配置,分离配置.apply(jwtAuthenticationSecurityConfig).and()//设置URL的授权。authorizeRequests()//这里需要释放登录页面,permitAll()表示不再拦截,/login登录url,/refreshToken刷新tokenurl//TODO还有这里也是正常项目发布的url很多,比如swagger相关的url,druid后台的url,还有一些静态资源。antMatchers("/login","/refreshToken").permitAll()//hasRole()表示需要指定角色才能访问资源。antMatchers("/hello").hasRole("ADMIN")//anyRequest()所有请求authenticated()必须经过身份验证。anyRequest().authenticated()//异常处理:认证失败,权限不足。and().exceptionHandling()//鉴权失败,不允许访问异常handler.authenticationEntryPoint(entryPointUnauthorizedHandler)//鉴权通过,但没有授权handler.accessDeniedHandler(requestAccessDeniedHandler).and()//禁用session,JWT验证不通过requiresession.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()//将TOKEN验证过滤器配置到过滤器链中,否则不生效,放在UsernamePasswordAuthenticationFilter.addFilterBefore(authenticationTokenFilterBean(),UsernamePasswordAuthenticationFilter.class)//关闭csrf.csrf().disable();}//自定义JwtToken验证过滤器@BeanpublicTokenAuthenticationFilterauthenticationTokenFilterBean(){returnnewTokenAuthenticationFilter();}/***加密算法*@return*/@BeanpublicPasswordEncodergetPasswordEncoder(){returnnewBCryptPasswordEncoder();}}评论很详细。不明白的可以看看案例源码。回复关键词:9529搞定!测试1.首先测试登录界面。Postman访问http://localhost:2001/securit...,如下:可以看到成功返回了两个token。2、请求头不带token,直接请求http://localhost:2001/securit...,如下:可以看到,直接进入了处理器EntryPointUnauthorizedHandler。3、用token访问http://localhost:2001/securit...,如下:访问成功,token有效。4、刷新token接口测试,携带一个过期的token访问如下:5、刷新token接口测试,携带未过期的token测试,如下:可以看到,成功返回了两个新的token。源码追踪以上一系列配置完全基于过滤器UsernamePasswordAuthenticationFilter,是web服务表单登录的方式。SpringSecurity的原理是由一系列的过滤器组成的,登录过程也是一样的。首先,在org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter()方法中,进行认证匹配,如下:attemptAuthentication()方法的主要作用是获取客户端传递过来的用户名和密码,封装成UsernamePasswordAuthenticationToken,交给ProviderManager进行鉴权。源码如下:ProviderManager的主要流程是调用抽象类AbstractUserDetailsAuthenticationProvider#authenticate()方法,如下图所示:retrieveUser()方法是调用userDetailService查询用户信息。然后进行认证,一旦认证成功或失败,都会调用相应的失败和成功处理程序进行处理。总结SpringSecurity虽然比较重,但是确实很好用,尤其是实现Oauth2.0规范,非常简单方便。案例源码已上传至GitHub,关注公众号:码猿技术专栏,回复关键字:9529搞定!最后再说一句(不要当妓女,请注意)陈的每篇文章都是用心输出的。他写了3个专栏并将它们组织成PDF。获取方式如下:专栏】回复关键词SpringCloud进阶获取!《Spring Boot 进阶》PDF:关注公众号:【码猿技术专栏】回复关键词SpringBoot进阶get!《Mybatis 进阶》PDF:关注公众号:【码猿技术专栏】回复关键词Mybatis进阶获取!如果本文对您有帮助或启发,请点赞、观看、转发、收藏,您的支持是我坚持下去的最大动力!关注公众号:【码猿技术专栏】,公众号有超赞的粉丝福利,回复:进群,可以加入技术讨论群,和大家一起讨论技术,吹牛!