前言 WebAPI接口服务场景里,用户的认证和鉴权是很常见的需求,SpringSecurity据说是这个领域里事实上的标准,实践下来整体设计上确实有不少可圈可点之处,也在一定程度上印证了小伙们经常提到的太复杂了的说法也是很有道理的。 本文以一个简单的SpringBootWeb以应用为例,重点介绍以下内容:演示SpringSecurity接口认证和鉴权的配置方法;以内存和数据库为例,介绍认证和鉴权数据的存储和读取机制;若干模块的自定义实现,包括:认证过滤器、认证或鉴权失败处理器等。SpringBoot示例 创建SpringBoot示例,用于演示SpringSecurity在SpringBoot环境下的应用,简要介绍四部分内容:pom。xml、application。yml、IndexController和HelloController。SpringBootpom。xml。。。bootexampleartifactIddependenciesdependencygroupIdorg。springframework。bootgroupIdspringbootstarterwebartifactIddependencydependencygroupIdorg。springframework。bootgroupIdspringbootstarterloggingartifactIddependencydependencies bootexample是用于演示的SpringBoot项目子模块(Module)。 注:依赖项的版本已在项目pom。xmldependencyManagement中声明。SpringBootapplication。ymlspring:application:name:exampleserver:port:9999logging:level:root:info SpringBoot应用名称为example,实例端口为9999。SpringBootIndexControllerRestControllerRequestMapping()publicclassIndexController{GetMappingpublicStringindex(){}} IndexController实现一个接口:。SpringBootHelloControllerRestControllerRequestMapping(hello)publicclassHelloController{GetMapping(world)publicStringworld(){}GetMapping(name)publicStringname(){}} HelloController实现两个接口:helloworld和helloname。 编译启动SpringBoot应用,通过浏览器请求接口,请求路径和响应结果:http:localhost:9999indexhttp:localhost:9999helloworldhelloworldhttp:localhost:9999hellonamehelloname SpringBoot示例准备完成。SpringBoot集成SpringSecurity SpringBoot集成SpringSecurity仅需要在pom。xml中添加相应的依赖:springbootstartersecurity,如下:dependencies。。。dependencygroupIdorg。springframework。bootgroupIdspringbootstartersecurityartifactIddependencydependencies 编译启动应用,相对于普通的SpringBoot应用,我们可以在命令行终端看到特别的两行日志:2022010916:05:57。437INFO87581〔main〕。s。s。UserDetailsServiceAutoConfiguration:Usinggeneratedsecuritypassword:3ef27867e9384fa4b5da5015f0deab7b2022010916:05:57。525INFO87581〔main〕o。s。s。web。DefaultSecurityFilterChain:Willsecureanyrequestwith〔org。springframework。security。web。context。request。async。WebAsyncManagerIntegrationFilter11e355ca,org。springframework。security。web。context。SecurityContextPersistenceFilter5114b7c7,org。springframework。security。web。header。HeaderWriterFilter24534cb0,org。springframework。security。web。csrf。CsrfFilter77c233af,org。springframework。security。web。authentication。logout。LogoutFilter5853ca50,org。springframework。security。web。authentication。UsernamePasswordAuthenticationFilter6d074b14,org。springframework。security。web。authentication。ui。DefaultLoginPageGeneratingFilter3206174f,org。springframework。security。web。authentication。ui。DefaultLogoutPageGeneratingFilter70d63e05,org。springframework。security。web。authentication。www。BasicAuthenticationFilter5115f590,org。springframework。security。web。savedrequest。RequestCacheAwareFilter767f6ee7,org。springframework。security。web。servletapi。SecurityContextHolderAwareRequestFilter7b6c6e70,org。springframework。security。web。authentication。AnonymousAuthenticationFiltere11ecfa,org。springframework。security。web。session。SessionManagementFilter106d77da,org。springframework。security。web。access。ExceptionTranslationFilter7b66322e,org。springframework。security。web。access。intercept。FilterSecurityInterceptor3e5fd2b1〕 表示SpringSecurity已在SpringBoot应用中生效。默认情况下,SpringSecurity自动化地帮助我们完成以下三件事件:开启FormLogin登录认证模式;我们使用浏览器请求接口:http:localhost:9999会发现请求会被重定向至页面login:http:localhost:9999login提示使用用户名和密码登录:生成用于登录的用户名和密码;用户名为user,密码会输出到应用的启动日志:Usinggeneratedsecuritypassword:3ef27867e9384fa4b5da5015f0deab7b每一次应用启动,密码都会重新随机生成。注册用于认证和鉴权的过滤器;SpringSecurity本质就是通过过滤器或过滤器(链)实现的,每一个接口请求都会按顺序经过这些过滤器的过滤,每个过滤器承担的各自的职责,组合起来共同完成认证和鉴权。根据配置的不同,注册的过滤器也会有所不同,默认情况下,加载的过滤器列表可以参考启动日志:WebAsyncManagerIntegrationFilterSecurityContextPersistenceFilterHeaderWriterFilterCsrfFilterLogoutFilterUsernamePasswordAuthenticationFilterDefaultLoginPageGeneratingFilterDefaultLogoutPageGeneratingFilterBasicAuthenticationFilterRequestCacheAwareFilterSecurityContextHolderAwareRequestFilterAnonymousAuthenticationFilterSessionManagementFilterExceptionTranslationFilterFilterSecurityInterceptor 使用SpringSecurity默认为我们生成的用户名和密码进行登录(Signin),成功之后会自动重定向至:index 之后我们就可以通过浏览器正常请求helloworld和helloname。 默认情况下,SpringSecurity仅支持基于FormLogin方式的认证,只能使用固定的用户名和随机生成的密码,且不支持鉴权。如果想要使用更丰富的安全特性:其他认证方式,如:HttpBasic自定义用户名和密码鉴权 则需要我们自定义配置SpringSecurity。自定义配置可以通过两种方式实现:JavaConfiguration:使用Java代码的方式配置SecurityNameSpaceConfiguration:使用XML文件的方式配置 本文以JavaConfiguration的方式为例进行介绍,需要我们提供一个继承自WebSecurityConfigurerAdapter配置类,然后通过重写若干方法进而实现自定义配置。importorg。springframework。context。annotation。Cimportorg。springframework。security。config。annotation。web。builders。HttpSimportorg。springframework。security。config。annotation。web。configuration。WebSecurityConfigurerAConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{}} SecurityConfig使用Configuration注解(配置类),继承自WebSecurityConfigurerAdapter,本文通过重写configure方法实现自定义配置。 需要注意:WebSecurityConfigurerAdapter中有多个名称为configure重载方法,这里使用的是参数类型为HttpSecurity的方法。 注:SpringSecurity默认自动化配置参考SpringBootAutoConfiguration。SpringSecurity使用HttpBasic认证protectedvoidconfigure(HttpSecurityhttp)throwsException{http。authorizeHttpRequests(authorizeauthorize。anyRequest()。authenticated())。httpBasic();}http。authorizeHttpRequests() 用以指定哪些请求需要什么样的认证或授权,这里使用anyRequest()和authenticated()表示所有的请求均需要认证。http。authorizeHttpRequests() 表示我们使用HttpBasic认证。 编译启动应用,会发现终端仍会输出密码:Usinggeneratedsecuritypassword:e2c774678c464fe1ab32eb87558b8c0e 因为,我们仅仅改变的是认证方式。 为方便演示,我们使用CURL直接请求接口:curlhttp:localhost:9999{timestamp:20220110T02:47:20。82000:00,status:401,error:Unauthorized,path:} 会提示我们Unauthorized,即:没有认证。 我们按照HttpBasic要求添加请求头部参数Authorization,它的值:BasicBase64(user:e2c774678c464fe1ab32eb87558b8c0e) 即:BasicdXNlcjplMmM3NzQ2Ny04YzQ2LTRmZTEtYWIzMi1lYjg3NTU4YjhjMGU 再次请求接口:curlHAuthorization:BasicdXNlcjplMmM3NzQ2Ny04YzQ2LTRmZTEtYWIzMi1lYjg3NTU4YjhjMGUhttp:localhost:9999index 认证成功,接口正常响应。SpringSecurity自定义用户名和密码 使用默认用户名和随机密码的方式不够灵活,大部分场景都需要我们支持多个用户,且分别为他们设置相应的密码,这就涉及到两个问题:用户名和密码如何读取(查询)用户名和密码如何存储(增加删除修改) 对于读取,SpringSecurity设计了UserDetailsService接口:publicinterfaceUserDetailsService{UserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundE}loadUserByUsername 实现按照用户名(username)从某个存储介质中加载相对应的用户信息(UserDetails)。username 用户名,客户端发送请求时写入的用于用户名。UserDetails 用户信息,包括用户名、密码、权限等相关信息。 注意:用户信息不只用户名和用户密码。 对于存储,SpringSecurity设计了UserDetailsManager接口:publicinterfaceUserDetailsManagerextendsUserDetailsService{voidcreateUser(UserDetailsuser);voidupdateUser(UserDetailsuser);voiddeleteUser(Stringusername);voidchangePassword(StringoldPassword,StringnewPassword);booleanuserExists(Stringusername);}createUser 创建用户信息updateUser 修改用户信息deleteUser 删除用户信息changePassword 修改当前用户的密码userExists 检查用户是否存在 注意:UserDetailsManager继承自UserDetailsService。 也就是说,我们可以通过提供一个已实现接口UserDetailsManager的类,并重写其中的若干方法,基于某种存储介质,定义用户名、密码等信息的存储和读取逻辑;然后将这个类的实例以Bean的形式注入SpringSecurity,就可以实现用户名和密码的自定义。 实际上,SpringSecurity仅关心如何读取,存储可以由业务系统自行实现;相当于,只实现接口UserDetailsService即可。 SpringSecurity已经为我们预置了两种常见的存储介质实现:InMemoryUserDetailsManager,基于内存的实现JdbcUserDetailsManager,基于数据库的实现 InMemoryUserDetailsManager和JdbcUserDetailsManager均实现接口UserDetailsManager,本质就是对于UserDetails的CRUD。我们先介绍UserDetails,然后再分别介绍基于内存和数据库的实现。UserDetails UserDetails是用户信息的抽象接口:publicinterfaceUserDetailsextendsSerializable{C?extendsGrantedAuthoritygetAuthorities();StringgetPassword();StringgetUsername();booleanisAccountNonExpired();booleanisAccountNonLocked();booleanisCredentialsNonExpired();booleanisEnabled();}getUsername 获取用户名。getPassword 获取密码。getAuthorities 获取权限,可以简单理解为角色名称(字符串),用于实现接口基于角色的授权访问,详情见后文。其他 获取用户是否可用,或用户密码是否过期或锁定。 SpringSecurity提供了一个UserDetails的实现类User,用于用户信息的实例表示。另外,User提供Builder模式的对象构建方式。UserDetailsuserUser。builder()。username(user)。password({bcrypt}2a10GRLdNijSQMUvlau9ofL。eDwmoohzzS7。rmNSJZ。0FxOBTk76klW)。roles(USER)。build();username 设置用户名称。password 设置密码,SpringSecurity不建议使用明文字符串存储密码,密码格式:{id}encodedPassword 其中,id为加密算法标识,encodedPassword为密码加密后的字符串。这里以加密算法bcrypt为例,详细内容可参考PasswordStorage。roles 设置角色,支持多个。 UserDetails实例创建完成之后,就可以使用UserDetailsManager的具体实现进行存储和读取。InMemory InMemoryUserDetailsManager是SpringSecurity为我们提供的基于内存实现的UserDetailsManager。ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{。。。BeanpublicUserDetailsManagerusers(){UserDetailsuserUser。builder()。username(userA)。password({bcrypt}2a10CrPsv1X3hM。giwVZyNsrKuaRvpJZyGQycJg78xT7Dm68K4DWNlxS)。roles(USER)。build();InMemoryUserDetailsManagermanagernewInMemoryUserDetailsManager();manager。createUser(user);}}创建用户信息实例user,用户名为userA,密码为123456(使用Bcrypt算法加密);认证并需要角色参与,但roles必须被设置,这里指定为USER;创建InMemoryUserDetailsManager实例使用createUser方法将user存储至相当于把用户信息存储至内存介质中;返回 使用Bean将InMemoryUserDetailsManager实例注入SpringSecurity。 创建InMemoryUserDetailsManager实例之后,并不是必须立即调用createUser添加用户信息,也可以在业务系统的其它地方获取已注入的InMemoryUserDetailsManager动态存储UserDetails实例。 编译启动应用,使用我们自己创建的用户名和密码(userA123456)访问接口:curlHAuthorization:BasicdXNlckE6MTIzNDU2http:localhost:9999index 基于内存介质自定义的用户名和密码已生效,接口正常响应。JDBC JdbcUserDetailsManager是SpringSecurity为我们提供的基于数据库实现的UserDetailsManager,相较于InMemoryUserDetailsManager使用略复杂,需要我们创建数据表,并准备好数据库连接需要的数据源(DataSource),JdbcUserDetailsManager实例的创建依赖于数据源。 JdbcUserDetailsManager可以与业务系统共用一个数据库数据源实例,本文不讨论数据源的相关配置。 以MySQL为例,创建数据表语句:createtableusers(usernamevarchar(50)notnullprimarykey,passwordvarchar(500)notnull,enabledbooleannotnull);createtableauthorities(usernamevarchar(50)notnull,authorityvarchar(50)notnull,constraintfkauthoritiesusersforeignkey(username)referencesusers(username));createuniqueindexixauthusernameonauthorities(username,authority); 其他数据库语句可参考UserSchema。 JdbcUserDetailsManager实例的创建与注入,除获取已注入的数据源实例dataS创建实例时需要传入数据源实例dataS 之外,整体流程与InMemoryUserDetailsManager类似,不再赘述。ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{。。。。。。AutowiredprivateDataSourcedataSBeanpublicUserDetailsManagerusers(){UserDetailsuserUser。builder()。username(user)。password({bcrypt}2a10CrPsv1X3hM。giwVZyNsrKuaRvpJZyGQycJg78xT7Dm68K4DWNlxS)。roles(USER)。build();JdbcUserDetailsManagermanagernewJdbcUserDetailsManager(dataSource);manager。createUser(user);}} 在业务系统中获取已注入的JdbcUserDetailsManager实例,可以动态存储UserDetails实例。 编译启动应用,使用我们自己创建的用户名和密码(userA123456)访问接口:curlHAuthorization:BasicdXNlckE6MTIzNDU2http:localhost:9999index 基于数据库介质自定义的用户名和密码已生效,接口正常响应。SpringSecurity鉴权 SpringSecurity可以提供基于角色的权限控制:不同的用户可以属于不同的角色不同的角色可以访问不同的接口 假设,存在两个角色USER(普通用户)和ADMIN(管理员), 角色USER可以访问接口helloname, 角色ADMIN可以访问接口helloworld, 所有用户认证后可以访问接口。 我们需要按上述需求重新设置HttpSecurity:protectedvoidconfigure(HttpSecurityhttp)throwsException{http。authorizeHttpRequests(authorizeauthorize。mvcMatchers(helloname)。hasRole(USER)。mvcMatchers(helloworld)。hasRole(ADMIN)。anyRequest()。authenticated())。httpBasic();}mvcMatchers(helloname)。hasRole(USER) 设置角色USER可以访问接口helloname。mvcMatchers(helloworld)。hasRole(ADMIN) 设置角色ADMIN可以访问接口helloworld。anyRequest()。authenticated() 设置其他接口认证后即可访问。 mvcMatchers支持使用通配符。 创建属于角色USER和ADMIN的用户: 用户名:userA,密码:123456,角色:USER 用户名:userB,密码:abcdef,角色:ADMINBeanpublicUserDetailsManagerusers(){UserDetailsuserAUser。builder()。username(userA)。password({bcrypt}2a10CrPsv1X3hM。giwVZyNsrKuaRvpJZyGQycJg78xT7Dm68K4DWNlxS)。roles(USER)。build();UserDetailsuserBUser。builder()。username(userB)。password({bcrypt}2a10PES8fUdtRrQ9OxLqf4CofOfcXBLQ3lkY2TSIcs1E9A0z2wECmZigG)。roles(ADMIN)。build();JdbcUserDetailsManagermanagernewJdbcUserDetailsManager(dataSource);manager。createUser(userA);manager。createUser(userB);} 对于用户userA: 使用用户userA的用户名和密码访问接口:curlHAuthorization:BasicdXNlckE6MTIzNDU2http:localhost:9999index 认证通过,可正常访问。 使用用户userA的用户名和密码访问接口helloname:curlHAuthorization:BasicdXNlckE6MTIzNDU2http:localhost:9999hellonamehelloname 认证通过,鉴权通过,可正常访问。 使用用户userA的用户名和密码访问接口helloworld:curlHAuthorization:BasicdXNlckE6MTIzNDU2http:localhost:9999helloworld{timestamp:20220110T13:11:18。03200:00,status:403,error:Forbidden,path:helloworld} 认证通过,用户userA不属于角色ADMIN,禁止访问。 使用用户userA的用户名和密码访问接口:curlHAuthorization:BasicdXNlckE6MTIzNDU2http:localhost:9999index 认证通过,可正常访问。 对于用户userB: 使用用户userB的用户名和密码访问接口:curlHAuthorization:BasicdXNlckI6YWJjZGVmhttp:localhost:9999index 认证通过,可正常访问。 使用用户userB的用户名和密码访问接口helloworld:curlHAuthorization:BasicdXNlckI6YWJjZGVmhttp:localhost:9999helloworldhelloworld 认证通过,鉴权通过,可正常访问。 使用用户userB的用户名和密码访问接口helloname:curlHAuthorization:BasicdXNlckI6YWJjZGVmhttp:localhost:9999helloname{timestamp:20220110T13:18:29。46100:00,status:403,error:Forbidden,path:helloname} 认证通过,用户userB不属于角色USER,禁止访问。 这里可能会有一点奇怪,一般情况下我们会认为管理员应该拥有普通用户的全部权限,即普通用户可以访问接口helloname,那么管理员应该也是可以访问接口helloname的。如何实现呢? 方式一,设置用户userB同时拥有角色USER和ADMIN;UserDetailsuserBUser。builder()。username(userB)。password({bcrypt}2a10PES8fUdtRrQ9OxLqf4CofOfcXBLQ3lkY2TSIcs1E9A0z2wECmZigG)。roles(USER,ADMIN)。build(); 这种方式有点不够优雅。 方式二,设置角色ADMIN包含USER; SpringSecurity有一个HierarchicalRoles的特性,可以支持角色之间的包含操作。 使用这个特性要特别注意两个地方:authorizeRequestsOverrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http。authorizeRequests(authorizeauthorize。mvcMatchers(helloname)。hasRole(USER)。mvcMatchers(helloworld)。hasRole(ADMIN)。mvcMatchers()。authenticated())。httpBasic();} 前文使用的是HttpSecurity。authorizeHttpRequests方法,此处需要变更为HttpSecurity。authorizeRequests方法。RoleHierarchyBeanRoleHierarchyhierarchy(){RoleHierarchyImplhierarchynewRoleHierarchyImpl();hierarchy。setHierarchy(ROLEADMINROLEUSER);} 使用RoleHierarchy以Bean的方式定义角色之间的层级关系;其中,ROLE是SpringSecurity要求的固定前缀。 编译启动应用,使用用户userB的用户名和密码访问接口helloname:curlHAuthorization:BasicdXNlckI6YWJjZGVmhttp:localhost:9999hellonamehelloname 认证通过,鉴权通过,可正常访问。 如果开启SpringSecurity的debug日志级别,访问接口时可以看到如下的日志输出:Fromtheroles〔ROLEADMIN〕onecanreach〔ROLEUSER,ROLEADMIN〕inzeroormoresteps。 可以看出,SpringSecurity可以从角色ADMIN推导出用户实际拥有USER和ADMIN两个角色。特别说明 HierarchicalRoles文档中的示例有明显错误:BeanAccessDecisionVoterhierarchyVoter(){RoleHierarchyhierarchynewRoleHierarchyImpl();hierarchy。setHierarchy(ROLEADMINROLESTAFFROLESTAFFROLEUSERROLEUSERROLEGUEST);returnnewRoleHierarcyVoter(hierarchy);} 接口RoleHierarchy中并不存在方法setHierarchy。前文所述authorizeRequests和RoleHierarchy结合使用的方法是结合网络搜索和自身实践得出的,仅供参考。 另外,authorizeHttpRequests和RoleHierarchy结合是没有效果的,authorizeRequests和authorizeHttpRequests两者之间的区别可以分别参考AuthorizeHttpServletRequestswithAuthorizationFilter和AuthorizeHttpServletRequestwithFilterSecurityInterceptor。 鉴权的前提需要认证通过;认证不通过的状态码为401,鉴权不通过的状态码为403,两者是不同的。SpringSecurity异常处理器 SpringSecurity异常主要分为两种:认证失败异常和鉴权失败异常,发生异常时会分别使用相应的默认异常处理器进行处理,即:认证失败异常处理器和鉴权失败异常处理器。 使用的认证或鉴权实现机制不同,可能使用的默认异常处理器也不相同。认证失败异常处理器 SpringSecurity认证失败异常处理器:publicinterfaceAuthenticationEntryPoint{voidcommence(HttpServletRequestrequest,HttpServletResponseresponse,AuthenticationExceptionauthException)throwsIOException,ServletE} 如前文所述,认证失败时,SpringSecurity使用默认的认证失败处理器实现返回:{timestamp:20220110T02:47:20。82000:00,status:401,error:Unauthorized,path:} 如果想要自定义返回内容,则可以通过自定义认证失败处理器实现:AuthenticationEntryPointauthenticationEntryPoint(){return(request,response,authException)response。getWriter()。print(401);}Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http。。。。httpBasic()。authenticationEntryPoint(authenticationEntryPoint());} authenticationEntryPoint()会创建返回一个自定义的AuthenticationEntryPoint实例;其中,使用HttpServletResponse。getWriter()。print()写入我们想要返回的内容:401。 httpBasic()。authenticationEntryPoint(authenticationEntryPoint())使用我们自定义的AuthenticationEntryPoint替换HttpBasic默认的BasicAuthenticationEntryPoint。 编译启动应用,使用不正确的用户名和密码访问接口:curlHAuthorization:Basicerrorhttp:localhost:9999401 认证不通过,使用我们自定义的内容401返回。鉴权失败异常处理器 SpringSecurity鉴权失败异常处理器:publicinterfaceAccessDeniedHandler{voidhandle(HttpServletRequestrequest,HttpServletResponseresponse,AccessDeniedExceptionaccessDeniedException)throwsIOException,ServletE} 如前文所述,认证失败时,SpringSecurity使用默认的认证失败处理器实现返回:{timestamp:20220110T13:18:29。46100:00,status:403,error:Forbidden,path:helloname} 如果想要自定义返回内容,则可以通过自定义鉴权失败处理器实现:AccessDeniedHandleraccessDeniedHandler(){return(request,response,accessDeniedException)response。getWriter()。print(403);}Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http。。。。httpBasic()。authenticationEntryPoint(authenticationEntryPoint())。and()。exceptionHandling()。accessDeniedHandler(accessDeniedHandler());} 自定义鉴权失败处理器与认证失败处理器过程类似,不再赘述。 编译启动应用,使用用户userA的用户名和密码访问接口helloworld:curlHAuthorization:BasicdXNlckE6MTIzNDU2http:localhost:9999helloworld403 鉴权不通过,使用我们自定义的内容403返回。特别注意 exceptionHandling()也是有一个authenticationEntryPoint()方法的;对于HttpBasic而言,使用exceptionHandling()。authenticationEntryPoint()设置自定义认证失败处理器是不生效的,具体原因需要大家自行研究。SpringSecurity自定义认证 前文介绍两种认证方式:FormLogin和HttpBasic,SpringSecurity还提供其他若干种认证方式,详情可参考AuthenticationMechanisms。 如果我们想实现自己的认证方式,也是比较简单的。SpringSecurity本质就是过滤器,我们可以实现自己的认证过滤器,然后加入到SpringSecurity中即可。FilterpreAuthenticatedFilter(){return(servletRequest,servletResponse,filterChain){。。。UserDetailsuserUser。builder()。username(xxx)。password(xxx)。roles(USER)。build();UsernamePasswordAuthenticationTokentokennewUsernamePasswordAuthenticationToken(user,user。getPassword(),user。getAuthorities());SecurityContextcontextSecurityContextHolder。createEmptyContext();context。setAuthentication(token);SecurityContextHolder。setContext(context);filterChain。doFilter(servletRequest,servletResponse);};} 认证过滤器核心实现流程:利用Http请求(servletRequest)中的信息完成自定义认证过程(省略),可能的情况:检查请求中的用户名和密码是否匹配检查请求中的Token是否有效其他 如果认证成功,则继续下一步;认证失败,则可以抛出异常,或者跳过后续步骤;从Http请求中提取username(用户名),使用已注入的UserDetailsService实例,加载UserDetails(用户信息)(省略);简单起见,模拟创建一个用户信息实例因为到这一步时,用户已是认证成功的,用户名和密码可以随意设置,实际只有角色是必须的,我们设置已认证用户的角色为USER。创建用户认证标识;SpringSecurity内部是依靠Authentication。isAuthenticated()来判断用户是否已认证过的,UsernamePasswordAuthenticationToken是Authentication的一种具体实现,需要注意创建实例时使用的构造方法和参数,构造方法内部会调用Authentication。setAuthenticated(true)。创建并设置环境上下文SecurityC环境上下文中保存着用户认证标识:context。setAuthentication(token)。特别注意 除去抛出异常的情况外,filterChain。doFilter(servletRequest,servletResponse);是必须保证被执行的。 理解认证过滤器涉及的概念会比较多,详情参考ServletAuthenticationArchitecture。 认证过滤器创建完成之后,就可以加入到SpringSecurity中:Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http。。。。。。。addFilterBefore(preAuthenticatedFilter(),ExceptionTranslationFilter。class)。exceptionHandling()。authenticationEntryPoint(authenticationEntryPoint())。accessDeniedHandler(accessDeniedHandler());} SpringSecurity根据我们配置的不同,会为我们自动按照一定的次序组装一条过滤器链,通过这条链上的若干过滤器完成认证鉴权的。我们需要把自定义的认证过滤器加到这个链的合适位置,这是选取的位置是在ExceptionTranslationFilter的前面。 过滤器链的顺序可以参考SecurityFilters。 ExceptionTranslationFilter的作用可以参考HandlingSecurityExceptions。特别注意 使用自定义认证过滤器时,自定义认证失败异常处理器和鉴权失败异常处理器的设置方法。 编译启动应用,我们会发现可以在不填入任何认证信息的情况下直接访问接口和helloname,因为模拟用户已认证且角色为USER;访问接口helloworld时会出现提示403。结语 SpringSecurity自身包含的内容很多,官方文档也不能很好的讲述清楚每个功能特性的使用方法,很多时候需要我们自己根据文档、示例、源码以及他人的分享,尽可能多的实践,逐步加深理解。