一、介绍Security 官方原话SpringSecurityisaframeworkthatprovidesauthentication,authorization,andprotectionagainstcommonattacks即SpringSecurity是一个提供身份验证、授权和防止常见攻击的框架。它是Spring提供的一个安全框架,可以根据使用者需要定制相关验证授权操作,配合SpringBoot可以快速开发一套完善的权限系统。二、快速上手创建一个SpringBoot项目并导入如下依赖或点击下载示例代码dependencygroupIdorg。springframework。bootgroupIdspringbootstartersecurityartifactIddependencydependencygroupIdorg。springframework。bootgroupIdspringbootstarterwebartifactIddependency复制代码运行SpringBoot应用程序 若是正确启动了,可以看到SpringSecurity生成了一段默认密码。。。。2022091323:56:07。841WARN19924〔main〕。s。s。UserDetailsServiceAutoConfiguration:Usinggeneratedsecuritypassword:70a36ac670c14f72822c71165988c56e。。。复制代码访问http:localhost:8080会跳转到login登录页面,输入账号(user)密码(控制台自动生成的密码)以继续访问。 SrpingSecurity主要解决的问题是安全访问控制,其实现原理是通过Filter对进入系统的请求进行拦截。当初始化SpringSecurity时,它创建了一个名为springSecurityFilterChain的Servlet过滤器,负责程序中的所以安全控制。三、基本原理DelegatingFilterProxy 从必要知识里我们知道了Filter的工作原理,在Spring中使用自定义的Filter有个问题那就是Filter必须在Servlet容器启动前就注册好,但是Spring使用ContextLoaderListener来加载SpringBean,于是设计了DelegatingFilterProxy。本质上来说DelegatingFilterProxy就是一个Filter,其间接实现了Filter接口,它嵌入在ServletFilterChain中,但是在doFilter中其实调用的从Spring容器中获取到的代理Filter的实现类delegate。 FilterChainProxy和SecurityFilterChain FilterChainProxy是SpringSecurity提供的一个特殊Filter,DelegatingFilterProxy并不是直接实例化和调用SpringSecurityFilter,而是构建了一个FilterChainProxy,当有请求进来就会去执行doFilter方法调用SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它是SpringSecurity使用的核心。 此外,SecurityFilterChain提供了更大的灵活性,Servlet容器中,仅根据URL调用过滤器。但是,FilterChainProxy可以利用RequestMatcher接口,根据HttpServletRequest中的任何内容确定调用,比原生的Servlet更灵活,此外,FilterChainProxy可以构建多条SecurityFilterChain,你的应用程序可以为不同的情况提供完全独立的配置,如下图所示。 过滤器链中主要的几个过滤器及其作用SecurityContextPersistenceFilter:这个Filter是整个拦截过程的入口,会在请求开始时从配置好的SecurityContextRepository中获取SecurityContext,然后把它设置给SecurityContextHolder。在请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository,同时清除securityContextHolder所持有的SecurityContext。UsernamePasswordAuthenticationFilter:用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的AuthenticationSuccessHandler和AuthenticationFailureHandler,这些都可以根据需求做相关改变;。LogoutFilter:用来处理实现用户登出和清除认证信息工作,登出成功后执行LogoutSuccessHandler,这里可以自定义实现一些功能。FilterSecurityInterceptor:是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问ExceptionTranslationFilter:能够捕获来自FilterChain所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException和AccessDeniedException,其它的异常它会继续抛出。异常处理 首先,ExceptionTranslationFilter调用FilterChain。doFilter(request,response)来调用应用程序的其余部分。如果用户未通过身份验证或者是AuthenticationException,则启动身份验证。SecurityContextHolder被清除HttpServletRequest保存在RequestCache中。当用户成功认证后,RequestCache用于重放原始请求。AuthenticationEntryPoint用于启动身份验证。例如,它可能重定向到登录页面或BASIC认证等。否则,如果是AccessDeniedException,则拒绝访问。调用AccessDeniedHandler来处理拒绝访问。表单登录 以上示例在未授权的情况下访问会经过以下安全过滤器:Securityfilterchain:〔DisableEncodeUrlFilterWebAsyncManagerIntegrationFilterSecurityContextPersistenceFilterHeaderWriterFilterCsrfFilterLogoutFilterUsernamePasswordAuthenticationFilterDefaultLoginPageGeneratingFilterDefaultLogoutPageGeneratingFilterBasicAuthenticationFilterRequestCacheAwareFilterSecurityContextHolderAwareRequestFilterAnonymousAuthenticationFilterSessionManagementFilterExceptionTranslationFilterFilterSecurityInterceptor〕复制代码 当没有登录的时候默认是anonymousUser匿名用户,经过一些列过滤器处理后,最后由FilterSecurityInterceptor进行权限校验授权,AccessDecisionManager进行授权投票,匿名用户不允许访问该接口,请求被拒绝重定向到登录页面,接着由DefaultLoginPageGeneratingFilter(自定义表单则不会初始化这个Filter)生成默认登录界面输出到浏览器。登录时经过UsernamePasswordAuthenticationFilter,只要用户请求满足该过滤器要求,则认证成功,接着是授权成功访问通过。 每个过滤器都有不同的功能,组织在一起形成了强大的安全体系,你可以在过滤链中自定义过滤器,里面的逻辑我就不一一细说了没啥好讲的,官方文档中都有介绍。下面讲讲我自己的一些实现吧。四、我实现思路是什么,我是怎么实现的 背景:拓展SpringSecurity实现基于Token的API认证授权基础程序 采用的广为熟知的RBAC模型,基于角色的访问控制(RoleBasedAccessControl) 拓展点:禁用CSRF(有个过滤器校验会报错)、会话管理设置为无状态STATELESS(因为我们要自定义处理登录注销逻辑)自定义UserDetailsService重写loadUserByUsername方法,从数据库中读取账号信息添加自定义Token认证过滤器自定义登录成功和失败处理器successHandler与failureHandler自定义注销处理器LogoutSuccessHandler自定义异常处理器AuthenticationEntryPoint与AccessDeniedHandler自定义AuthorizationManager 开发调试可以设置一下日志输出级别,这样能助于我们更快地分析和排查问题:logging:level:org。springframework。web:traceorg。springframework。security:trace复制代码 另外EnableWebSecurity这个注解debug属性设置为true也能看到更多的日志信息,这对我们很有帮助。SecurityConfiguration核心配置类EnableWebSecurity(debugfalse)publicclassSecurityConfiguration{privatefinalAppUserDetailsServiceuserDetailsService;privatefinalAbstractStringCacheStorecacheStore;privatefinalAuthenticationTokenFilterauthenticationTokenFilter;privatefinalPermissionAuthorizationManagerRequestAuthorizationContextpermissionAuthorizationManager;publicSecurityConfiguration(AppUserDetailsServiceuserDetailsService,AbstractStringCacheStorecacheStore,PermissionAuthorizationManagerRequestAuthorizationContextpermissionAuthorizationManager){this。userDetailsServiceuserDetailsService;this。cacheStorecacheStore;this。permissionAuthorizationManagerpermissionAuthorizationManager;this。authenticationTokenFilternewAuthenticationTokenFilter(cacheStore,userDetailsService);}BeanpublicWebSecurityCustomizerwebSecurityCustomizer(){return(web)web。ignoring()SpringSecurityshouldcompletelyignoreURLsstartingwithresources。antMatchers(resources);}BeanpublicSecurityFilterChainsecurityFilterChain(HttpSecurityhttp)throwsException{http。authorizeHttpRequests()。antMatchers(HttpMethod。OPTIONS,)。permitAll()。antMatchers()。permitAll()。antMatchers(userinfo)。authenticated()需要认证。anyRequest()。access(permissionAuthorizationManager)动态权限认证。and()。userDetailsService(userDetailsService)。formLogin()。permitAll()。successHandler(newCustomizeAuthenticationSuccessHandler(cacheStore))。failureHandler(newAuthenticationEntryPointFailureHandler(newCustomizeAuthenticationEntryPoint()))。and()。logout()。logoutSuccessHandler(newCustomizeLogoutSuccessHandler(cacheStore))。and()。exceptionHandling()。authenticationEntryPoint(newCustomizeAuthenticationEntryPoint())。accessDeniedHandler(newCustomizeAccessDeniedHandler())。and()。csrf()。disable()。sessionManagement()。sessionCreationPolicy(SessionCreationPolicy。STATELESS)。and()。addFilterBefore(authenticationTokenFilter,UsernamePasswordAuthenticationFilter。class)。addFilterBefore(authenticationTokenFilter,LogoutFilter。class);returnhttp。build();}复制代码 释义:AuthenticationTokenFilterToken认证过滤器(除了自定义开放的接口外都会被调用)PermissionAuthorizationManager动态权限授权管理器(基于角色与资源权限表)CustomizeAuthenticationSuccessHandler登录处理器(登录成功后被调用用于生成Token) CustomizeLogoutSuccessHandler注销处理器(注销成功后被调用用于清除Toekn)CustomizeAuthenticationEntryPoint认证失败处理器(认证出现异常被调用)CustomizeAccessDeniedHandler授权失败处理器(授权出现异常被调用,如权限不足以访问某接口)AbstractStringCacheStore缓存类(用于缓存Token)CustomizeAuthenticationSuccessHandler登录处理器Slf4jpublicclassCustomizeAuthenticationSuccessHandlerimplementsAuthenticationSuccessHandler{privatefinalAbstractStringCacheStorecacheStore;Expiredseconds。privatestaticfinalintACCESSTOKENEXPIREDSECONDS243600;privatestaticfinalintREFRESHTOKENEXPIREDDAYS30;publicCustomizeAuthenticationSuccessHandler(AbstractStringCacheStorecacheStore){this。cacheStorecacheStore;}OverridepublicvoidonAuthenticationSuccess(HttpServletRequestrequest,HttpServletResponseresponse,Authenticationauthentication)throwsIOException{AppUserDetailsuserDetails(AppUserDetails)SecurityContextHolder。getContext()。getAuthentication()。getPrincipal();GeneratenewtokenAuthTokentokennewAuthToken();token。setAccessToken(BottleUtils。randomUUIDWithoutDash());token。setExpiredIn(ACCESSTOKENEXPIREDSECONDS);token。setRefreshToken(BottleUtils。randomUUIDWithoutDash());Cachethosetokens,justforclearingcacheStore。putAny(SecurityUtils。buildAccessTokenKey(userDetails),token。getAccessToken(),ACCESSTOKENEXPIREDSECONDS,TimeUnit。SECONDS);cacheStore。putAny(SecurityUtils。buildRefreshTokenKey(userDetails),token。getRefreshToken(),REFRESHTOKENEXPIREDDAYS,TimeUnit。DAYS);CachethosetokenswithuseridcacheStore。putAny(SecurityUtils。buildTokenAccessKey(token。getAccessToken()),userDetails。getUserId(),ACCESSTOKENEXPIREDSECONDS,TimeUnit。SECONDS);cacheStore。putAny(SecurityUtils。buildTokenRefreshKey(token。getRefreshToken()),userDetails。getUserId(),REFRESHTOKENEXPIREDDAYS,TimeUnit。DAYS);response。setCharacterEncoding(utf8);response。setStatus(HttpServletResponse。SCOK);response。setContentType(MediaType。APPLICATIONJSONVALUE);response。getWriter()。write(JsonUtils。objectToJson(BaseResponse。ok(登录成功!,token)));}复制代码LogoutSuccessHandler注销处理器Slf4jpublicclassCustomizeLogoutSuccessHandlerimplementsLogoutSuccessHandler{privatefinalAbstractStringCacheStorecacheStore;publicCustomizeLogoutSuccessHandler(AbstractStringCacheStorecacheStore){this。cacheStorecacheStore;}OverridepublicvoidonLogoutSuccess(HttpServletRequestrequest,HttpServletResponseresponse,Authenticationauthentication)throwsIOException,ServletException{if(Objects。isNull(authentication)){return;}AppUserDetailsuserDetails(AppUserDetails)authentication。getPrincipal();ClearaccesstokencacheStore。getAny(SecurityUtils。buildAccessTokenKey(userDetails),String。class)。ifPresent(accessToken{DeletetokencacheStore。delete(SecurityUtils。buildTokenAccessKey(accessToken));cacheStore。delete(SecurityUtils。buildAccessTokenKey(userDetails));});ClearrefreshtokencacheStore。getAny(SecurityUtils。buildRefreshTokenKey(userDetails),String。class)。ifPresent(refreshToken{cacheStore。delete(SecurityUtils。buildTokenRefreshKey(refreshToken));cacheStore。delete(SecurityUtils。buildRefreshTokenKey(userDetails));});response。setCharacterEncoding(utf8);response。setStatus(HttpServletResponse。SCOK);response。setContentType(MediaType。APPLICATIONJSONVALUE);response。getWriter()。write(JsonUtils。objectToJson(BaseResponse。ok(登出成功!,null)));log。info(Youhavebeenloggedout,lookingforwardtoyournextvisit!);}}复制代码AuthenticationTokenFilterToken认证过滤器Slf4jpublicclassAuthenticationTokenFilterextendsOncePerRequestFilter{privatestaticfinalStringAUTHENTICATIONSCHEMEBEARERBearer;privatefinalAbstractStringCacheStorecacheStore;privatefinalAppUserDetailsServiceappUserDetailsService;publicAuthenticationTokenFilter(AbstractStringCacheStorecacheStore,AppUserDetailsServiceappUserDetailsService){this。cacheStorecacheStore;this。appUserDetailsServiceappUserDetailsService;}OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{GettokenfromrequestheaderStringaccessTokenrequest。getHeader(HttpHeaders。AUTHORIZATION);if(!StringUtils。hasText(accessToken)){DofilterfilterChain。doFilter(request,response);return;}if(!StringUtils。startsWithIgnoreCase(accessToken,AUTHENTICATIONSCHEMEBEARER)){thrownewBadCredentialsException(Token必须以bearer开头);}if(accessToken。equalsIgnoreCase(AUTHENTICATIONSCHEMEBEARER)){thrownewBadCredentialsException(Token不能为空);}GettokenbodyaccessTokenaccessToken。substring(AUTHENTICATIONSCHEMEBEARER。length()1);OptionalLongoptionalUserIdcacheStore。getAny(SecurityUtils。buildTokenAccessKey(accessToken),Long。class);if(!optionalUserId。isPresent()){log。debug(Token已过期或不存在〔{}〕,accessToken);filterChain。doFilter(request,response);return;}UserDetailsuserDetailsappUserDetailsService。loadUserById(optionalUserId。get());UsernamePasswordAuthenticationTokenauthenticationnewUsernamePasswordAuthenticationToken(userDetails,null,userDetails。getAuthorities());SecurityContextHolder。getContext()。setAuthentication(authentication);DofilterfilterChain。doFilter(request,response);}}复制代码CustomizeAuthenticationEntryPoint认证异常处理器Slf4jpublicclassCustomizeAuthenticationEntryPointimplementsAuthenticationEntryPoint{Overridepublicvoidcommence(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationExceptionauthException)throwsIOException,ServletException{BaseResponseObjecterrorDetailhandleBaseException(authException);errorDetail。setData(Collections。singletonMap(uri,request。getRequestURI()));response。setCharacterEncoding(utf8);response。setStatus(HttpStatus。UNAUTHORIZED。value());response。setContentType(MediaType。APPLICATIONJSONVALUE);response。getWriter()。write(JsonUtils。objectToJson(errorDetail));}privateBaseResponseObjecthandleBaseException(Throwablet){Assert。notNull(t,Throwablemustnotbenull);BaseResponseObjecterrorDetailnewBaseResponse();errorDetail。setStatus(HttpStatus。UNAUTHORIZED。value());if(log。isDebugEnabled()){errorDetail。setDevMessage(ExceptionUtils。getStackTrace(t));}if(tinstanceofAccountExpiredException){errorDetail。setMessage(账户过期);}elseif(tinstanceofDisabledException){errorDetail。setMessage(账号被禁用);}elseif(tinstanceofLockedException){errorDetail。setMessage(账户被锁定);}elseif(tinstanceofAuthenticationCredentialsNotFoundException){errorDetail。setMessage(用户身份凭证未找到);}elseif(tinstanceofAuthenticationServiceException){errorDetail。setMessage(用户身份认证服务异常);}elseif(tinstanceofBadCredentialsException){errorDetail。setMessage(t。getMessage());}else{errorDetail。setMessage(访问未授权);}returnerrorDetail;}}复制代码CustomizeAccessDeniedHandler授权异常publicclassCustomizeAccessDeniedHandlerimplementsAccessDeniedHandler{Overridepublicvoidhandle(HttpServletRequestrequest,HttpServletResponseresponse,AccessDeniedExceptionaccessDeniedException)throwsIOException,ServletException{BaseResponseObjecterrorDetailnewBaseResponse();errorDetail。setStatus(HttpStatus。FORBIDDEN。value());errorDetail。setMessage(禁止访问);response。setCharacterEncoding(utf8);response。setStatus(HttpServletResponse。SCFORBIDDEN);response。setContentType(MediaType。APPLICATIONJSONVALUE);response。getWriter()。write(JsonUtils。objectToJson(errorDetail));}}复制代码PermissionAuthorizationManager动态权限授权管理Slf4jComponentpublicclassPermissionAuthorizationManagerTimplementsAuthorizationManagerT{privatefinalAuthenticationTrustResolvertrustResolvernewAuthenticationTrustResolverImpl();privatefinalPermissionServicepermissionService;publicPermissionAuthorizationManager(PermissionServicepermissionService){this。permissionServicepermissionService;}OverridepublicAuthorizationDecisioncheck(Supplierauthentication,Tobject){DeterminesifthecurrentuserisauthorizedbyevaluatingifthebooleangrantedisGranted(authentication。get());if(!granted){returnnewAuthorizationDecision(false);}Collectionlt;?extendsGrantedAuthorityauthoritiesauthentication。get()。getAuthorities();SetStringauthorityauthorities。stream()。map(GrantedAuthority::getAuthority)。collect(Collectors。toSet());log。debug(username〔{}〕havroles:〔{}〕,authentication。get()。getName(),authority);RequestAuthorizationContextrequestAuthorizationContext(RequestAuthorizationContext)object;StringservletPathrequestAuthorizationContext。getRequest()。getRequestURI();log。debug(accessurl:{},servletPath);AppUserDetailsuserDetails(AppUserDetails)authentication。get()。getPrincipal();ListLongroleIdsuserDetails。getRoles()。stream()。map(Role::getId)。collect(Collectors。toList());ListPermissionpermissionspermissionService。listByRoleIds(roleIds);booleanagreeFlagpermissions。stream()。anyMatch(permissionisRouter(permission)permission。getUrl()。equals(servletPath));log。debug(checkresult:{},agreeFlag);returnnewAuthorizationDecision(agreeFlag);}privatebooleanisGranted(Authenticationauthentication){returnauthentication!nullisNotAnonymous(authentication)authentication。isAuthenticated();}privatebooleanisNotAnonymous(Authenticationauthentication){return!this。trustResolver。isAnonymous(authentication);}privatebooleanisRouter(Permissionpermission){return1。equals(permission。getType());}}复制代码五、示例 登录成功POSTlogin?usernameuserpassword123456Host:localhost:8080response:{status:200,message:登录成功!,devMessage:null,data:{accesstoken:8430064e7d9b497c8b786a33b0524bc5,expiredin:86400,refreshtoken:8d2c6fb3489b47389a65cbf79f732f9a}}复制代码登录失败POSTlogin?usernameuserpassword123Host:localhost:8080response:{status:401,message:用户名或密码错误,devMessage:org。springframework。security。authentication。BadCredentialsException:用户名或密码错误。。。,data:{uri:login}}复制代码登录注销POSTlogoutHost:localhost:8080Authorization:Bearerb6422e3462224126a67f876b5f1b3a1eresponse:{status:200,message:登出成功!,devMessage:null,data:null}复制代码未登录或Token过期POSTlogoutHost:localhost:8080Authorization:Bearerb6422e3462224126a67f876b5f1b3a1eresponse:{status:401,message:访问未授权,devMessage:org。springframework。security。authentication。InsufficientAuthenticationException:Fullauthenticationisrequiredtoaccessthisresource。。。,data:{uri:userinfo}}复制代码权限不足GETadminHost:localhost:8080Authorization:Bearerf7a542c4899a4e6ea5039002a8f19110response:{status:403,message:禁止访问,devMessage:null,data:null}复制代码六、小结 好了,就分享到这里了,希望对大家有所帮助,另外如有理解错误的地方请多多指教。SpringSecurity还有很多值得探索的功能,继续学习吧