spring security之web应用安全

1、什么是web应用安全,为了安全咱们要作哪些事情?

保护web资源不受侵害(资源:用户信息、用户财产、web数据信息等)
对访问者的认证、受权,指定的用户才能够访问资源
访问者的信息及操做获得保护(xss csrf sql注入等)html

开发中咱们须要注意的项:
1. 【高危】网络关键数据传输加密
1. 【高危】站点使用https方式部署
2. 【高危】文件传输时,过滤与业务无关的文件类型
3. 【高危】接口开发,应预防泄露敏感数据
4. 【高危】预防url中带url跳转参数
5. 【中危】预防CSRF攻击
6. 【中危】预防短信恶意重发
7. 【中危】预防暴力破解图片验证码
8. 【低危】经过httponly预防xss盗取cookie信息
9. 【低危】设置http协议安全的报文头属性前端

......nginx

2、为何要聊spring security?

spring security在不少安全防御上很容易实现
理解spring security的抽象有助于养成面向对象思惟
能够为理解spring security oauth2作铺垫web

3、先搞清楚两大概念:认证、受权

Application security boils down to two more or less independent problems: authentication (who are you?) and authorization (what are you allowed to do?).(摘自spring官网 《Spring Security Architecture》ajax

简单点出发,认证和受权能够理解为登陆和权限验证redis

认证

首先试想一下咱们本身实现登录,一般须要作些什么?

1.输入用户名密码、验证码提交给后端
2.用户名密码与数据库的进行匹配验证
3.验证前能够先把用户信息经过用户名查出来,看看用户状态是否可用等
4.传递到后端的密码可能须要加密后再与数据库里的密码进行匹配
5.数据库里的密码多是加盐存储的,这样咱们传递进来的密码还要进行加盐加密,加盐通常都是用户表里的数据,即仍是可能要提早经过用户名查询用户信息(用户名加盐能够不用提早查库)
6.登陆成功后,能够把用户信息存储到cookie,下次直接提取cookie信息进行登陆(即remember-me登陆)安全系数稍低,例如电商网站也可经过cookie登陆查看订单列表,可是下单支付时仍是要从新登陆
7.可能网站有多个登陆入口,多种登陆方式,用户名密码方式、短信验证码方式等
8.登陆成功后自动跳转到主页或者提示成功信息,登陆失败跳转到失败页或者提示失败信息
9.退出登陆功能,清空sessionspring

简单的进行抽象

 

认证拦截器、用户信息服务

【认证拦截器】咱们用户名密码的认证能够作成一个filter也能够作成一个servlet
1.提取用户名密码参数信息
2.经过用户名获取用户信息,判断用户是否可用等
3.经过密码加密器把密码参数进行加密而后与查出来的密码进行匹配,判断是否定证经过
4.认证成功或者认证失败的后续处理sql

【用户信息服务】经过用户名获取用户信息,可是这个只是一个方法,须要提供一个userservice来存放数据库

 

问题:后端

这里只是单单考虑用户名密码登陆,若是如今要作手机号验证码登陆呢?再加一个认证拦截器?
这样日后会衍生出不少个拦截器,若是是filter实现方式的话就会有多个filter,若是是servlet实现方式则会有多个servlet,若是从作成公用的中间件来提供使用的话,怎样才是最好的方式?

认证提供者

若是是作成中间件的话,占用用户的多个地址无疑是一个缺点
若是认证拦截器只使用一个,而后把认证这块的业务进行打包,抽象出一个【认证提供者】,其子类有【用户名密码认证提供者 】、【手机号验证码认证提供者】供咱们使用,手机号验证码认证提供者一样可使用【用户信息服务】【认证失败处理器】【认证成功处理器】
这样是否会更好?

 

可是认证拦截器这里要作判断,要经过请求参数来判断使用哪一种认证提供者

认证管理者

咱们干脆把这个事情也抽象出来,让【认证管理者】去作这个事情,以下:
认证拦截器只须要一个认证管理者,认证管理者能够有0-n个认证提供者,为何能够0个呢,由于认证管理者自己也能够干认证这个事情,只不过他能够交给对应的认证提供者也能够本身干这个事情

 

与spring security对号入座

受权

什么是受权(权限验证)?

 受权即判断用户是否有访问某资源的权限,资源对于咱们web应用来讲就是url,每个controller里的action对应的request mapping的url

资源:web应用的每个url

权限:用户可以访问某个资源的凭证,能够是一个变量字符,也能够是角色名,是用户与资源相关联的中间产物

 

试想一下咱们本身实现权限验证,一般须要作些什么?

1.【受权拦截器】拦截须要受权的资源【受保护的资源】
2.【公开资源】放行静态资源文件和不用受权的页面,例如登陆界面
3.有些资源能够某个角色可以访问,有些资源须要某个权限才能够访问
4.有些资源remember-me登陆的能访问,有些资源必须从新输入密码登陆才能访问,例5.如电商网站查看订单就不用从新登陆,下单就须要,即划分了不一样的安全级别
6.web界面上菜单按钮的显示与隐藏控制 

受权拦截器

拦截全部请求仍是只拦截受保护的资源请求?

方案一:只拦截受保护的资源请求
【受权拦截器】怎么拦截【受保护的资源】不拦截【公开资源】呢?
这还不简单,在项目启动时把受保护的资源动态添加到filter-mapping不就好了?
相似以下伪代码:

 

这样【公开资源】不被拦截、【受保护的资源】被拦截,一箭双雕!
可是问题来了,受权应用上线后运行正常,一段时间后,咱们须要增长受保护的资源,好比子应用上线了,子应用是物理上另一个单独的应用,经过nginx挂载在同域名/module1目录下,这时数据库增长资源配置后,可是filter不生效,由于filter在应用启动的时候已经注册了,这里无法增长urlpattern了,最简单的办法只能是重启受权应用

 

方案二:拦截全部资源请求
【受权拦截器】拦截全部请求/*,当请求过来时,只须要判断当前请求是不是【公开资源】(公开资源能够动态从配置取也能够从数据库去取),是则直接放行,不在公开资源范围则走受权流程
两个方案对比来讲,在不考虑性能消耗的状况下(也消耗不了多少性能),无疑方案二更安全更适合扩展

spring security也是采用的方案二,在拦截全部请求后,能够动态的加载受保护的资源配置,再进行处理

受权拦截器拦截到资源请求后,要作的就是受权
1.经过当前请求的资源获取权限列表
2.获取用户的权限,咱们须要从session持久化的地方去取用户的权限信息,有统一的地方去存取,后面咱们会讲到,不在这里展开

循环资源的权限
循环用户的权限

判断用户是否拥有该资源的权限

资源对应权限是1对1

那么咱们的系统就简单不少
直接经过资源获取到权限,而后判断用户是否有该权限则能够判断是否受权经过

资源对应权限是1对0

在用户登陆状况下,咱们是受权经过仍是拒绝呢,这个取决于咱们本身,能够经过配置去设定,spring security固然也是支持咱们这么作的
FilterSecurityInterceptor.setRejectPublicInvocations(true) 默认是false

资源对应权限是1对多

 

 

 那么受权这里咱们应该须要这么干:

int hasAuthorities=0 循环资源的权限(5个) 循环用户的权限,用户有该资源权限则hasAuthorities +1

最后获得的结果是:
资源对应权限个数是5
用户拥有权限个数是2
究竟是能访问呢仍是不能访问呢,即受权结果是经过仍是不经过?
仔细看上图其实发现ROLE_开头的有两个,是同一类型的权限,其余3个是不一样类型,按道理一个正经常使用户便是管理员又是新闻编辑角色的可能几乎不可能,正常来讲一个用户只有一个角色,其余类型的权限也同理,若是按权限类型来分,应该是4类权限,那么用户就拥有2类,应该是 4:2才对

所里这里涉及到两个问题:
资源的权限应该按分类来进行计数(即ROLE开头的归为一类,无论资源拥有几个,只要用户有一个都计数1)
权限的分类:角色、操做、IP、认证模式等
 
受权的决策
一票经过,即用户拥有一类权限即经过
全票经过,即用户拥有全部权限分类才经过
少数服从多数,即用户拥有的权限分类必须大于没有的分类
 
例如对应上面的4:2,
一票经过:经过
全票经过:拒绝
少数服从多数:2=2 咱们能够设置相同时的处理逻辑,经过或拒绝,spring security默认相同是经过

受权决策者、受权投票者

受权的决策咱们交给【受权决策者】,投票咱们交给【受权投票者】

 

与spring security对号入座

 

 恭喜,你已经搞清楚filter chain中最关键的两个filter了

Security filter chain: [
...
AuthenticationProcessingFilter
...
FilterSecurityInterceptor
]

4、spring security的filter chain

filter chain的概念

关于filter chain的概念咱们就不作多的解释,最下面的加载流程图里也有说明

 

 

 

 

@EnableWebSecurity(debug = true)

把debug日志打出来后,每次请求均可以看到完整的filterchain,方便咱们去理解和吸取

Security filter chain: [ WebAsyncManagerIntegrationFilter SecurityContextPersistenceFilter HeaderWriterFilter LogoutFilter AuthenticationProcessingFilter RequestCacheAwareFilter SecurityContextHolderAwareRequestFilter AnonymousAuthenticationFilter SessionManagementFilter ExceptionTranslationFilter FilterSecurityInterceptor ]

如何使用filter chain中的filter

主要仍是增长本身的实现,或者基于默认实现作一些配置 《Spring Security - Adding In Your Own Filters》

授之以渔比授之以鱼更加剧要,因此这里只是简单的列举一些使用的例子,具体的原理仍是要到源码中去本身品味摸索,每一个filter本身的奥妙须要读者本身去体会

AuthenticationProcessingFilter

登录(认证 Authentication)
AuthenticationProcessingFilter =》默认UsernamePasswordAuthenticationFilter 或者配置本身实现的filter,登陆成功后会存储到session,若是是使用的spring-session-redis则会存储到redis

FilterSecurityInterceptor

权限验证(受权Authorization)
FilterSecurityInterceptor=》替换成本身实现的filter 若是没有则使用该filter

RememberMeAuthenticationFilter

protected void configure(HttpSecurity http) throws Exception { http.addFilterAt(rememberMeAuthenticationFilter(), RememberMeAuthenticationFilter.class) } private String REMEMBER_ME_KEY = "3a87d426-0789-46b1-91d9-61d1f953db17"; private RememberMeServices rememberMeServices() { return new TokenBasedRememberMeServices(REMEMBER_ME_KEY, customUserDetailService()) {{ setAlwaysRemember(true);//不须要前端传递参数 remember-me=(true/on/yes/1 四个值均可以) }}; } //这里能够跟认证的filter公用一个认证管理者(认证管理者会判断当前authenticationRequest去判断适用哪一个provider),也能够建一个新的,而后只添加rememberme认证的provider
private RememberMeAuthenticationFilter rememberMeAuthenticationFilter() throws Exception { return new RememberMeAuthenticationFilter(this.customAuthenticationManager(), this.rememberMeServices()); } private AuthenticationManager customAuthenticationManager() throws Exception { CustomDaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider(); authenticationProvider.setUserDetailsService(this.customUserDetailService()); authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder()); List<AuthenticationProvider> providers = new ArrayList<>(); providers.add(authenticationProvider); providers.add(new RememberMeAuthenticationProvider(REMEMBER_ME_KEY)); return new ProviderManager(providers); }

 

RequestCacheAwareFilter

在security调用链中用户可能在没有登陆的状况下访问被保护的页面,这时候用户会被跳转到登陆页,登陆以后,springsecurity会自动跳转到以前用户访问的保护的页面

SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler

会先从requestCache去取,若是有上面的操做(例如未登陆访问某页面,会记录到session),就会取session得到url,而后跳转过去,若是requestcache取不到,就会执行

super.onAuthenticationSuccess,即SimpleUrlAuthenticationSuccessHandler的跳转到登陆成功页

若是有些业务数据是写在登陆成功页(例如写cookie),那么若是requestcache有数据,则不会重定向到登陆成功页,会直接跳转到上次未登陆访问的页面url,到目标页后则取不到登陆成功页写的数据,那么就会有问题

 想要关闭访问缓存?能够

1、全局配置里禁用掉

http.requestCache().requestCache(new NullRequestCache())

2、设置成功处理handler直接使用SimpleUrlAuthenticationSuccessHandler,

而不是SavedRequestAwareAuthenticationSuccessHandler

CustomUsernamePasswordAuthenticationFilter mu = new CustomUsernamePasswordAuthenticationFilter(); mu.setAuthenticationSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler(){{ setDefaultTargetUrl("/login/success"); }};);

把这里登陆成功的处理handler改成以下SimpleUrlAuthenticationSuccessHandler,simpleurl就不会去取requestCache

mu.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler(){{ setDefaultTargetUrl("/login/success"); }};);

ConcurrentSessionFilter

protected void configure(HttpSecurity http) throws Exception { http .addFilterAt(new ConcurrentSessionFilter(this.sessionRegistry()),ConcurrentSessionFilter.class) } @Bean public SessionRegistry sessionRegistry(){ //若是是分布式系统,多台机器,这里还要改为SpringSessionBackedSessionRegistry使用springsession存储,而不是存储在内存里
    return new SessionRegistryImpl(); } private CustomUsernamePasswordAuthenticationFilter loginFilter() throws Exception { CustomUsernamePasswordAuthenticationFilter mu = new CustomUsernamePasswordAuthenticationFilter(); List<SessionAuthenticationStrategy> list=new ArrayList(); //从内存取全部sessionid,并过时掉访问时间最先的
    list.add(new ConcurrentSessionControlAuthenticationStrategy(this.sessionRegistry()));//策略的前后顺序没有关系,spring会帮咱们作好逻辑 //保存当前sessionid至内存
    list.add(new RegisterSessionAuthenticationStrategy(this.sessionRegistry())); mu.setSessionAuthenticationStrategy(new CompositeSessionAuthenticationStrategy(list)); ... }

CsrfFilter

csrffilter默认不会拦截的请求类型:TRACE HEAD GET OPTIONS

protected void configure(HttpSecurity http) throws Exception { http.csrf().disable();//注释掉默认的
    http.addFilterAt(new CsrfFilter(csrfTokenRepository()),CsrfFilter.class); } //这里security默认开启csrf配置也是同样的,须要注意分布式环境时token的存储问题
@Bean public CsrfTokenRepository csrfTokenRepository() { return new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository()); } //本身的loginfilter默认SessionAuthenticationStrategy是null,因此本身实现filter须要注册上去,若是是security默认的认证filter则会自动注入进去strategy不用咱们操心
private CustomUsernamePasswordAuthenticationFilter loginFilter() throws Exception { CustomUsernamePasswordAuthenticationFilter mu = new CustomUsernamePasswordAuthenticationFilter(); List<SessionAuthenticationStrategy> list=new ArrayList(); //登陆成功后从新生成csrf token,不然登陆成功后token也不会变
    list.add(new CsrfAuthenticationStrategy(csrfTokenRepository())); mu.setSessionAuthenticationStrategy(new CompositeSessionAuthenticationStrategy(list)); ... }

首先得get请求一个页面,后台才会把token存到session供后面post时使用,不过这个csrftoken在访问第一个get页面后生成后都不会再改变了,须要注意这一点;

只有每次登陆成功后才会变!

AuthenticationProcessingFilter里面的SessionAuthenticationStrategy包含 CsrfAuthenticationStrategy. 会去设置新的csrftoken

 如何使用token

csrfToken=((CsrfToken)ApplicationContextUtil.getBean(CsrfTokenRepository.class)).loadToken(request) csrfToken.getHeaderName() csrfToken.getParameterName() csrfToken.getToken() 或 ((CsrfToken)request.getAttribute("_csrf")).getHeaderName() ((CsrfToken)request.getAttribute("_csrf")).getParameterName() ((CsrfToken)request.getAttribute("_csrf")).getToken() 或 <meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<script> var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $.ajaxSetup({ beforeSend: function (xhr) { if(header && token ){ xhr.setRequestHeader(header, token); } }} ); </script>

BasicAuthenticationFilter

该filter在ConcurrentSessionFilter后面,说明他不会走同时登陆次数限制的逻辑
构造UsernamePasswordAuthenticationToken而后调用authenticationManager进行身份认证
属性里有RememberMeServices,说明能够走rememberme cookie自动登陆逻辑

LogoutFilter

logoutfilter 注意,只能post请求才能够
该filter会调用logouthandlers.logout
把 remembermeservices里的cookie设置过时
把 csrftokenrepository token设置为null
把 session.invalidate SecurityContext.setAuthentication((Authentication)null) SecurityContextHolder.clearContext();

ExceptionTranslationFilter

关于受权的全部异常抛出统一都是在ExceptionTranslationFilter
包括认证异常、受权异常
认证异常:指的是匿名或者未认证的用户访问了须要认证的资源
受权异常:当前用户没有访问该资源的权限

protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint).accessDeniedHandler(customAccessDeniedHandler); } @Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { protected final Log logger = LogFactory.getLog(getClass()); @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException ex) throws IOException, ServletException { logger.warn("请从新登陆后访问,"+ex.getMessage()); logger.warn(JSONObject.toJSON(ex)); RespEntity respEntity = RespUtil.toRespEntity(RespUtil.ACCESS_DENIED, "请从新登陆后访问",null); response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.println( JSONObject.toJSONString(respEntity)); } } @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { protected final Log logger = LogFactory.getLog(getClass()); @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { logger.warn("请从新登陆后访问,"+accessDeniedException.getMessage()); logger.warn(JSONObject.toJSON(accessDeniedException)); RespEntity respEntity = RespUtil.toRespEntity(RespUtil.ACCESS_DENIED, "请从新登陆后访问", null); response.setCharacterEncoding("UTF-8"); response.setContentType("text/plain;charset=utf-8"); PrintWriter writer = response.getWriter(); writer.println( JSONObject.toJSONString(respEntity)); } }

关于session-fixation attacks

在登陆成功后要更换sessionid,默认的认证filter会帮咱们加进去

private CustomUsernamePasswordAuthenticationFilter loginFilter() throws Exception { CustomUsernamePasswordAuthenticationFilter mu = new CustomUsernamePasswordAuthenticationFilter(); List<SessionAuthenticationStrategy> list=new ArrayList(); //登陆成功后更换新的sessionid list.add(new ChangeSessionIdAuthenticationStrategy()); mu.setSessionAuthenticationStrategy(new CompositeSessionAuthenticationStrategy(list)); ... }

默认的认证filter的session认证strategy有4个(会随着开启csrf  concurrentsession而增长strategy,不开则不加)

模拟:

创建一个springboot站点(不使用spring security)

@RestController public class TestController { @GetMapping("/login") public String login(@RequestParam(name = "userName",required = false) String userName, HttpServletRequest request) { request.getSession().setAttribute("userName",userName); return "sessionid:"+request.getSession().getId()+";userName:"+userName; } @GetMapping("/user") public String getOrder( HttpServletRequest request) { return "sessionid:"+request.getSession().getId()+";userName:"+request.getSession().getAttribute("userName"); } }

1.攻击者先访问 login地址,获得sessionid

2.被攻击者访问地址

http://localhost:8080/login;jsessionid=520F92C885F099E997DA55D9D0F450BE

3.被攻击者访问地址后模拟get登陆(后面附带参数)

http://localhost:8080/login;jsessionid=520F92C885F099E997DA55D9D0F450BE?userName=tianjun

 4.攻击者能够以用户正常认证方式进行操做和窃取用户信息

 

5、源码相关

重要的仍是搞清楚如何进行抽象,为何这样去抽象?

以下是spring bean加载流程(右键新标签打开可查看大图)

相关文章
相关标签/搜索