sa-token相关资料地址
官网: https://sa-token.cc/
gitee: https://gitee.com/dromara/sa-token
github: https://github.com/dromara/sa-token
快速开始: https://sa-token.cc/doc.html#/
sa-token典型应用
这里我直接拿SpringBoot_v2(springboot的开源后台脚手架)来作为应用案例进行演示了。
SpringBoot_v2介绍:SpringBoot_v2项目是努力打造springboot框架的极致细腻的脚手架。包括一套漂亮的前台。无其他杂七杂八的功能,原生纯净。
可以作为练手项目的脚手架,或快速开发小型系统的后台脚手架。
需要稍微注意点是,他自带一套前端简单界面,不过他是用thymeleaf方式的,不是nodejs生态,不太建议同时当作前端脚手架来使用。
相关资料:
git地址: https://gitee.com/bdj/SpringBoot_v2
github地址: https://github.com/fuce1314/Springboot_v2
以下简称SpringBoot_v2为应用。
a.应用初始化
建库导库。create database `springbootv2`;
将doc下的springbootv2.sql导入你的数据库。
b.修改数据库地址、用户名密码
c.启动服务
登录地址:
http://localhost:8080/admin/login
具体的功能就不介绍了,可以自己尝试。
下面对sa-token相关依赖、配置进行说明:
引入依赖:
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot-starter</artifactId><version>1.26.0</version></dependency><!-- Sa-Token 整合 Redis (使用jackson序列化方式) --><dependency><groupId>cn.dev33</groupId><artifactId>sa-token-dao-redis-jackson</artifactId><version>1.26.0</version></dependency>
进行配置:
定义用户、角色、权限信息获取
这个类里就直接访问系统中的dao了,也就是与系统原角色权限体系打通了。
配置权限控制注解方法拦截处理
SaTokenConfigure
/*** 注册 Sa-Token 的注解拦截器,打开注解式鉴权功能 */@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**"); }
上面这个SaAnnotationInterceptor是控制方法权限的,类似这种:
主要就是配合这几个注解来实现方法级的权限控制的。@SaCheckLogin @SaCheckPermission @SaCheckRole。
简单说明下:
检查是否登录:一般除了登录接口,其他接口都需要登录后访问
检查角色:用户可以关联角色,通过检查有没有指定的角色决定是否允许
检查权限:每个角色可以关联多个权限,权限是最小粒度,类似于前端按钮权限
访问地址跳转登录页面
Tips:这个是针对前后端不分离的情况来说的,如果你的项目前后端分离,这一步一般是前端直接做的。
定义了SaServletFilter,指定某些url允许直接访问(一般就是登录地址、静态资源等)。
其他的url全都检查是否已登录。如果是未登录的异常,跳转登录页面;如果是其他异常,也就是认证操作本身的异常。
同时在每次请求进来时,可以通过指定beforeAuth钩子函数来写一下通用处理,设置响应头之类的。(当然,这个filter所有的功能可以直接自己定义一个filter来实现,只是借用一下StpUtil.checkLogin()就行了,只不过sa-token提取了一个SaServletFilter把整个拦截过程拆分为了认证前、认证、错误处理,以及覆盖的url包含、排除匹配等)。
完成
对于大部分情况来说,这就算完成了,能够检查是否登录,能够根据用户角色配置控制controller方法访问权限,已经足够满足需要了。整体上只需要实现一个用户信息获取、添加一个filter,一个handlerInterceptor,足够简单。
springboot服务后端安全痛点
考虑一般的安全需求:
1.用户登录状态的保持
2.用户会话过期控制
3.token创建、保存及传递
4.方法级后台权限控制
5.登录页面跳转(不是必要)
在很多情况下,可能我们更多的只是想要第4点,其他的用系统原有的filter体系就足够了。当然对于新建系统,还是建议统一使用sa-token提供的机制,让sa-token帮你处理底层的繁杂细节处理。
站在个人的思考角度,我能想到的也就这些情况了,对于这样的安全需求,我们该如何实现,如何选择呢?
spring-security的复杂性
大家大都使用的spring全家桶,碰到问题,一般首先会想到从spring生态中去寻找现成的解决方案,认为这种方式集成起来更加可靠,也更容易操作上手,但就安全这块来说,在我个人看来,spring-security还是相当复杂的。虽然对于配置量而言,使用他的代码不是很多,但对于理解难度来说,特别是对于想探究源码层面原理的同学来说,还是相当复杂、相当难以理顺关系的。
配置举例
如果是spring-security,大概的配置方式为:
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {// 搜寻匿名标记 url: @AnonymousAccessRequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();// 获取匿名标记Map<String, Set<String>> anonymousUrls = getAnonymousUrl(handlerMethodMap);httpSecurity// 禁用 CSRF.csrf().disable().addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)// 授权异常.exceptionHandling().authenticationEntryPoint(authenticationErrorHandler).accessDeniedHandler(jwtAccessDeniedHandler)// 防止iframe 造成跨域.and().headers().frameOptions().disable()// 不创建会话.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 静态资源等等.antMatchers(HttpMethod.GET,"/*.html","/**/*.html","/**/*.css","/**/*.js","/webSocket/**").permitAll()// swagger 文档.antMatchers("/swagger-ui.html").permitAll().antMatchers("/swagger-resources/**").permitAll().antMatchers("/webjars/**").permitAll().antMatchers("/*/api-docs").permitAll()// 文件.antMatchers("/avatar/**").permitAll().antMatchers("/file/**").permitAll()// 阿里巴巴 druid.antMatchers("/druid/**").permitAll().antMatchers("/actuator/**").permitAll().antMatchers("/instances/**").permitAll()// 放行OPTIONS请求.antMatchers(HttpMethod.OPTIONS, "/**").permitAll().antMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()// 所有请求都需要认证.anyRequest().authenticated().and().apply(securityConfigurerAdapter());}
}
自动配置
通过这个配置,能看出来啥呢?他给我们暴露出来的可操作对象为HttpSecurity,而这只是spring-security体系中的一环,整体是怎么样的呢?我这里简单的梳理一下,仅供大家参考对比。
1.SecurityAutoConfiguration 这个是security的自动配置类,作为分析的起点
2.SpringBootWebSecurityConfiguration 这个是1引入的配置类,配置了一个默认的SecurityFilterChain
3.WebSecurityEnablerConfiguration 这个也是1引入的,是为了引入4
4.@EnableWebSecurity这个是3引入的,相当于不用咱们自己加了。
5.WebSecurityConfiguration这个是4引入的,下面详细描述
6.HttpSecurityConfiguration这个是4引入的,他主要就是创建了一个HttpSecurity,他创建的HttpSecurity对象其实是给了2注入用的,作为系统默认配置。也就是说完全使用springboot的yml配置来动态修改某些属性,而不添加自定义的配置类。
WebSecurityConfiguration
下面对5进行简单分析:
1.WebSecurityConfiguration的目标就是springSecurityFilterChain()函数生成一个filter,而这个filter最终的类型看WebSecurity的performBuild()方法,看到了,是FilterChainProxy对象。
2.1的目标filter,是通过webSecurity来生成的,这算是创建型设计模式里的建造者模式(Builder),不过他比一般而言的Builder模式要复杂的多,这里不做具体分析。
3.简单看下springSecurityFilterChain方法的代码,他这里直接操作了一个list,就是securityFilterChains,通过webSecurity.addSecurityFilterChainBuilder加进去了
4.还有注意的是标记了@Autowired的方法setFilterChainProxySecurityConfigurer,他注入了SecurityConfigurer的list,然后挨个webSecurity.apply应用给webSecurity了
5.第4步SecurityConfigurer因为泛型的缘故,其实他注入的就是WebSecurityConfigurer对象,也就是最经典的配置方式:用户继承WebSecurityConfigurerAdapter实现个性化配置。
6.WebSecurityConfigurerAdapter重点关注init(WebSecurity)方法,这个方法是WebSecurityConfigurer这个配置器的第一阶段(一共是两个阶段:init和configure,看SecurityConfigurer),这里调用web.addSecurityFilterChainBuilder方法(即webSecurity.addSecurityFilterChainBuilder)方法,也给webSecurity加进去了,效果类似3。
7.由此,用户自定义的继承自WebSecurityConfigurerAdapter的配置类,就相当于给webSecurity配置了一个filterChain过滤器链,而这个链具体是在HttpSecurity对象中配置的,WebSecurityConfiguerAdapter主要是配置AuthenticationManager、UserDetailsService等的。
WebSecurity建造
下面对2进行简单分析:
1.webSecurity看下他的继承关系,他的父类是AbstractConfiguredSecurityBuilder<Filter, WebSecurity>
2.建造者模式,最大的步骤,在顶层父类SecurityBuilder中,是build方法
3.下一层步骤,在SecurityBuilder的子类AbstractSecurityBuilder中,扩展了个doBuild方法。
4.下一层步骤,在类AbstractConfiguredSecurityBuilder中
protected final O doBuild() throws Exception {synchronized(this.configurers) {this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;this.beforeInit();this.init();this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;this.beforeConfigure();this.configure();this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;O result = this.performBuild();this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;return result;}}
可以看到这里又拆分成了很多子步骤,包括beforeInit,init,beforeConfiguer,configure,同时引入一个新步骤:performBuild。同时这个类主要是辅助子类(这里是WebSecurity)进行动态化配置的,他提供了两个方法add(C configurer)和removeConfigurer来添加删除配置器。
5.在WebSecurity类中,也就是实现了performBuild方法。
SecurityConfigurer配置器
再对上面的4里面的configurer进行简单分析:
1.这里的configurer是SecurityConfigurer<O, B>,这里的O表示目标,就是Filter,B表示构造器本身,这里是WebSecurity。
2.看WebSecurityConfigurerAdapter的类型,他实现了接口WebSecurityConfigurer<WebSecurity>
3.WebSecurityConfigurer继承了SecurityConfigurer<Filter, T>,这里的T是WebSecurity
4.所以归根结底,WebSecurityConfigurerAdapter就是个SecurityConfigurer
5.SecurityConfigurer的方法包括两个:init,configure,也就是说,我们可以通过添加一个个的SecurityConfigurer对webSecurity进行配置,最终通过webSecurity的build来产出我们想要的FilterChainProxy。
6.也就是说,一条security过滤器链,是由一组WebSecurityConfigurerAdapter具体配置的(当然这里只需要一个这样的configurer)。
7.但是呢,这里还不是最终的地方。可以看到,init方法里,web.addSecurityFilterChainBuilder(http),这里又出现了HttpSecurity对象。
HttpSecurity建造
下面对HttpSecurity简单分析:
1.HttpSecurity也是个建造者模式,AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>,与WebSecurity类似,他为了产出DefaultSecurityFilterChain
2.他依赖的配置器组configurers,类型是SecurityConfigurer<O,B>,O是DefaultSecurityFilterChain,B是HttpSecurity。这里重点关注HttpSecurity的getOrApply方法
3.这里与之相关的configurer有什么呢?大概有这么些:
AnonymousConfigurer
AuthorizeHttpRequestsConfigurer
ChannelSecurityConfigurer
CorsConfigurer
CsrfConfigurer
DefaultLoginPageConfigurer
ExceptionHandlingConfigurer
ExpressionUrlAuthorizationConfigurer
FormLoginConfigurer
HeadersConfigurer
HttpBasicConfigurer
JeeConfigurer
LogoutConfigurer
PermitAllSupport
PortMapperConfigurer
RememberMeConfigurer
RequestCacheConfigurer
SecurityContextConfigurer
ServletApiConfigurer
SessionManagementConfigurer
UrlAuthorizationConfigurer
X509Configurer
也就是我们在个性化配置的时候,重写configure(HttpSecurity httpSecurity)方法时做的那些事情。
而HttpSecurity也提供了辅助方法,方便我们函数式添加configurer。
4.这些configurer之间还能通过HttpSecurity的setSharedObject和getSharedObject之类的方法实现相互间的共享,还有更多的细节,无法一一详述。
CorsConfigurer举例
以CorsConfigurer举例,直接看他的configure方法:
public void configure(H http) {ApplicationContext context = (ApplicationContext)http.getSharedObject(ApplicationContext.class);CorsFilter corsFilter = this.getCorsFilter(context);Assert.state(corsFilter != null, () -> {return "Please configure either a corsFilter bean or a corsConfigurationSourcebean.";});http.addFilter(corsFilter);}
简单来说,就是添加了一个corsFilter。
总结
说了这么多,只是想说spring-security真的挺复杂。当然你可以说站在使用者的角度,我们配置的代码量并不大。那确实,不过对于比较想看源码、想进行比较底层一点定制的人的来说,理解难度太大了。
安全框架的选型折中
如果你的水平较高,或者公司基础平台已经选型确定了spring-security,或者你不认为spring-security存在理解难、理解费劲的问题,那可以依然使用他。如果你水平有限,或者不想把安全这块做的那么重,那就完全可以考虑使用sa-token,或者借鉴sa-token的思路,通过加几个类,在Filter层面、Servlet层面、HandlerInterceptor层面操作一下,也能实现想要的效果。
当然这里是推荐使用sa-token,至少可以写个demo或直接下载SpringBoot_v2的代码体验一下子,研究一下子。
从sa-token想到的破题法
不必纠结于框架,够用好用永远是最终目标。
如果项目有明确要求,或有明确的技术栈标准,那该用还得用,技术毕竟要为需要服务的。
扩展:如何根据需要自己写个小而美的框架
如何手动实现一个小框架并与springboot整合的基本思路流程。
在与springboot整合方面,我个人感觉,sa-token的spring-boot-starter写的一般,他没有加一个SaTokenAutoConfiguration(个人见解)。
一般我认为遵循以下步骤:
1.建模,甭管是画个示意图、流程图,还是用文字描述清楚,把要解决的问题点列出来、把具体的方案模型描述出来
2.提取接口、实体,提取关键逻辑类。一开始,我们可以不将其抽取出去,而是直接放到项目中,只是作为一个包独立放置。
3.在项目中实际调试、使用,注意这个独立包不要依赖项目,而是项目依赖他。
4.代码重构。将项目结构化优化,让步骤清晰明了。并提供给用户充分的便利,提供Customizer、Configurer之类的口子,让用户能方便的进行个性化微调、配置;如果可能,用户甚至可以自定义替换某块整个处理环节逻辑。
5.默认配置。提供某些接口的默认实现,减少用户的工作量;对于某些场景(如持久化到内存、redis、数据库),可以预置多个实现,供用户选择。
6.将代码抽取到独立工程中去,并提供starter自动配置。
7.项目中依赖starter,整合测试。
8.发布到私服或开源。