まず、Webアプリケーションのセキュリティは、我々が何をすべきか、物事確保するためには、何ですか?
虐待のWebリソース(資源:などのユーザ情報、ユーザプロパティ、ウェブデータ、)からの保護
の訪問者認証のため、承認、指定されたユーザーがリソースにアクセスすることができます
訪問者情報と操作は(XSS、CSRF、SQLインジェクションなど)で保護されています
用語の開発、我々は注意を払う必要があります:
1. [ハイリスク]ネットワークのデータ伝送の暗号化キー
1. [ハイリスク]サイトが展開され、HTTPSを使用して
2. [ハイリスク]ファイルの種類にかかわらず、ファイル転送はフィルタリングや交通とき
3. [ハイリスク]インタフェース開発、機密データの開示を防止すべき
パラメータを持つ4. [ハイリスク]防止のURL URLジャンプ
[危険にさらさ] 5. CSRF攻撃防止
[危険で] 6.悪意のあるSMS再送防ぐ
ブルート画像の予防に7 [リスク]をPIN
8] [低リスク盗難防止クッキー情報HttpOnlyのXSS
9低リスク] [HTTPプロトコル設けられたセキュリティ属性ヘッダ
......
第二には、なぜ春のセキュリティを話すべき?
多くのセキュリティで春のセキュリティは簡単に実装するための
抽象春のセキュリティは、オブジェクト指向の考え方を開発するのに役立ちます理解して
理解春のセキュリティのOAuth2への道を開くことができます
第三に、二つの概念を見つけるために:認証、許可、
(あなたがする許可されている?)、認証(あなたは誰ですか?)と認可(摘自春官网:アプリケーションのセキュリティはさらに2つの以下の独立問題に沸く。「春のセキュリティアーキテクチャ」)
ビュー、認証と認可の検証の単純なポイントは、ログインとパーミッションとして理解することができます
認証
自分が最初に一般的に何をすべきか、着陸を実現想像?
1.コードはバックエンドに提出し、ユーザー名とパスワードを入力し
、データベース2.ユーザー名とパスワードの検証で一致し
たユーザ認証情報が最初のユーザ名によって知ることができる、などがユーザーの状態かどうかを確認するために使用することができます前に3
4に伝達されます。バックエンドのパスワードは暗号化し、データベースのパスワードが一致した後に必要になることがあり
、データベースのパスワードは、我々は暗号化されたパスワードではなく、塩のために渡されるように、塩で格納されていてもよい5を、食卓塩は、一般ユーザーのですユーザー名で(ユーザー名塩は、あらかじめライブラリにチェックすることはできません)で、また、事前のユーザ情報にチェックする必要があり、データ、
ログイン成功後6は、ユーザ情報がクッキーに保存することができ、クッキー情報は、(直接、次のログを抽出すなわちremember-ログ私の)わずかな電気の供給業者のウェブサイトなどの低安全係数は、また、クッキーにより受注の一覧を表示するには、ログインすることができますが、注文が支払われたとき、または再度ログインし
、サイトには、複数のログエントリ、複数のログイン、ユーザ名・パスワードモードを有することができる7。 SMSの検証コードやその他の方法
8成功したログインが自動的に言及して失敗に成功、ログイン失敗ページ、またはジャンプを示すホームページや情報にジャンプした後、失敗メッセージショー
9.ログ機能は、セッションを空にする
シンプルな抽象
認定インターセプタ、ユーザ情報サービス
[証明]我々は、ユーザ名とパスワードの認証がフィルタにすることができるインターセプタもサーブレット行うことができます
。1.抽出ユーザー名パスワードパラメータ情報は、
ユーザー名を介してユーザ情報を取得する2.、ユーザーが利用可能であるかどうかを判断する、など
3.パスワードの暗号化暗号化パラメータを暗号化するかどうかを決定することによって、認証をその後、マッチしたパスワードを発見し
、その後の処理の認証成功または認証失敗4
ユーザー名を介してユーザ情報を取得するには、[ユーザー]情報サービスが、これは唯一の方法であり、あなたがUserServiceのを保存する必要があります
問題:
ここでは、単純にユーザー名・パスワードを検討し、電話番号の確認コードの場合は、それに署名する今何?認定インターセプタと相まって?
このように将来はどのように作られた公衆が使用するミドルウェアを提供する場合、サーブレット実装は、複数のサーブレットが存在することになる場合、フィルタの実装ならば、次に、複数のフィルタが存在するであろう、インターセプタの数に由来します最良の方法はありますか?
認定プロバイダー
ミドルウェアが行われた場合には、ユーザーの複数のアドレスによって占められるが、間違いなく欠点も
[]認定プロバイダ認証インターセプターが使用する唯一の、そしてその後、この事業をパッケージ化する認定、抽象的な場合は、[そのサブクラスを持っていますユーザ名パスワード認証プロバイダ]私たちの使用するために、[電話番号]認証コード認証プロバイダは、電話番号認証コード認証プロバイダは、[]、[ユーザー認証情報サービス・プロセッサーの障害] [証明書]は正常にプロセッサを使用することができ
、このかどうかをそれは良いでしょうか?
しかし、ここで認証インターセプタはリクエストパラメータを使用した認証プロバイダを判断するために、判断を行うには
認証マネージャ
:私達はちょうど、次のようである[証明書]管理者は、この事をやらせる、この事は抽象入れ
認定インターセプターのみ認定マネージャを必要とする、認証マネージャは0-nの認証プロバイダを持つことができ、そしてなぜそれが0かもしれ認定管理者として認証プロバイダも、これを自分のことを行うことができ、自身がこの事ドライ認定することができますが、彼が手渡しすることができます
そして、春のセキュリティ非難
承認
承認(許可の確認)とは何ですか?
授权即判断用户是否有访问某资源的权限,资源对于我们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
]
四、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的跳转到登录成功页
想要关闭访问缓存?可以
一、全局配置里禁用掉
http.requestCache().requestCache(new NullRequestCache())
二、设置成功处理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.攻击者可以以用户正常认证方式进行操作和窃取用户信息
五、源码相关
重要的还是搞清楚如何进行抽象,为什么这样去抽象?
如下是spring bean加载流程(右键新标签打开可查看大图)