Spring Security

1. 概要

Spring Security

Spring是非常流行和成功的Java应用开发框架,Spring Security正是Spring家族中的成员。Spring Security基于Spring框架,提供一套Web应用安全性的完整解决方案。

正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是Spring Security重要核心功能。

(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录。

(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。

Spring Security特点:

  • 和Spring无缝整合
  • 全面的权限控制
  • 专门为Web开发而设计
    • 旧版本不能脱离Web环境使用
    • 新版本对整个框架进行了分层抽取,分成了核心模块和Web模块。单独引入核心模块就可以脱离Web环境。
  • 重量级

Shiro

Apache旗下的轻量级权限控制框架。

Shiro特点:

  • 轻量级。Shiro主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
  • 通用性
    • 好处:不局限于Web环境,可以脱离Web环境使用。
    • 缺陷:在Web环境下一些特定的需求需要手动编写代码定制。

Spring Security与Shiro的关系

Spring Security是Spring家族中的一个安全管理框架,实际上,在Spring Boot出现之前,Spring Security就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是Shiro的天下。

相对于Shiro,在SSM中整合Spring Security都是比较麻烦的操作,所以,Spring Securtiy虽然功能比Shiro强大,但是使用反而没有Shiro多(Shiro虽然功能没有Spring Security多,但是对于大部分项目而言,Shiro也够用了)。

自从有了Spring Boot之后,Spring Boot对于Spring Security提供了自动化配置方案,可以使用更少的配置来使用Spring Security。

  • SSM + Shiro
  • Spring Boot / Spring Cloud + Spring Security

以上只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。


2. Spring Security入门案例

  • 其中需要添加一个配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()                 // 表单登录
            .and()       
            .authorizeRequests()         // 认证配置
            .anyRequest()                // 任何请求
            .authenticated();            // 都需要身份验证
    }
}

3. 代码重点底层流程

  • 重点看三个过滤器:

    • FilterSecurityInterceptor:是一个方法级的权限过滤器,基本位于过滤链的最底部。
      • super.beforeInvocation(fi)表示查看之前的filter是否通过
      • fi.getChain().doFilter(fi.getRequest(),fi.getResponse());表示真正的调用后台的服务
    • ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
    • UsernamePasswordAuthenticationFilter:对/login的POST请求做拦截,校验表单中用户名,密码
  • UserDetailsService接口详解

    当什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。

    如果需要自定义逻辑时,只需要实现UserDetailsService接口即可。接口定义如下:

- 返回值UserDetails

    这个类是系统默认的用户“主体”
// 表示获取登录用户所有权限
Collection<? extends GrantedAutority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证(密码)是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean iisEnabled();
- 方法参数username

    表示用户名。此值是客户端表单传递过来的数据。**默认情况下必须叫username,否则无法接收。**
  • PasswordEncoder接口详解
// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);

// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回true;
// 如果不匹配,则返回false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);

// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回true,否则返回false,默认返回false
default boolean upgradeEncoding(String encodedPassword){
    return false;
}
- 接口实现类

![](https://img2022.cnblogs.com/blog/2701464/202201/2701464-20220122110028504-999761353.png)

    BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个解析器。

    BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认10。
- 查用方法演示
@Test
public void test01(){
    // 创建密码解析器
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    // 对密码进行加密
    String atguigu = bCryptPasswordEncoder.encode("atguigu");
    // 打印加密之后的数据
    System.out.println("加密之后数据:\t" + atguigu);
    
    // 判断原字符加密后和加密之前是否匹配
    boolean result = bCryptPasswordEncoder.matches("atguigu",atguigu);
    // 打印比较结果
    System.out.println("比较结果是:\t" + result);
}

4. Spring Security方法

  1. 页面提交方式必须为post请求,所以上面的页面不能使用,用户名,密码必须为username,password。(原因是在执行登录的时候会走一个过滤器UsernamePasswordAuthenticationFilter

  2. 如果修改配置可以调用usernameParameter()和passwordParameter()方法。

  3. hasAuthority():如果当前的主体具体指定的权限,则返回true,否则返回false。

  4. hasAnyAuthority():如果当前的主体有任何提供的角色(给定的作为一个逗号分割的字符串列表)的话,返回true。

  5. hasRole()

    如果用户具备给定角色就允许访问,否则出现403;

    如果当前主体具有指定的角色,则返回true。

  6. hasAnyRole()

    表示用户具备任何一个条件都可以访问。


5. 注解使用

  1. @Secured

    • 判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_”。
    • 使用注解先要开启注解功能!@EnableGlobalMethodSecurity(securedEnabled = true)
  2. @PreAuthorize

    • 先开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled = true)
    • @PreAuthorize:注解适合进入方法前的权限验证,@PreAuthorize可以将登录用户的roles / permissions参数传到方法中。
  3. @PostAuthorize

    • 先开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled = true)
    • @PostAuthorize注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限。
  4. @PostFilter

    @PostFilter:权限验证之后对数据进行过滤,留下用户名是admin1的数据

    表达式中的filterObject引用的是方法返回值List中的某一个元素

  5. @PreFilter

    @PreFilter:进入控制器之前对数据进行过滤


6. CSRF

  1. CSRF理解

    跨站请求伪造(Cross-site request forgery),也被称为one-click attack或者session riding,通常缩写为CSRF或者XSRF,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。

    跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

    从Spring Security4.0开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用程序,Spring Security CSRF会针对PATCH,POST,PUT和DELETE方法进行防护。

  2. 案例

    在登录页面添加一个隐藏域:

<input type="hidden" th:if="${_csrf}!=null" th:value="${_csrf.token}" 
name="_csrf"/>
关闭安全配置的类中的csrf
http.csrf().disable();
Spring Security实现CSRF的原理:

1. 生成csrfToken保存到HttpSession或者Cookie中。
2. 请求到来时,从请求中提取csrfToken,和保存的csrfToken作比较,进而判断当前请求是否合法。主要通过CsrfFilter过滤器来完成。

7. Spring Security微服务权限方案

什么是微服务

  1. 微服务的由来

    微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进城中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能够构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。

  2. 微服务优势

    (1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。

    (2)微服务每个模块都可以使用不同的存储方式(比如有的用Redis,有的用mysql等),数据库也是单个模块对应自己的数据库。

    (3)微服务每个模块都可以使用不同的开发技术,开发模式灵活。

  3. 微服务本质

    (1)微服务,关键不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。

    (2)微服务的目的是有效的拆分应用,实现敏捷开发和部署。


微服务认证与授权实现思路

  1. 认证授权过程分析

    (1)如果是基于Session,那么Spring-Security会对cookie里的sessionid进行解析,找到服务器存储的session信息,然后判断当前用户是否符合请求的要求。

    (2)如果是token,则是解析出token,然后将当前请求加入到Spring-Security管理的权限信息中去。

    如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于token的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为key,权限列表为value的形式存入redis缓存中,根据用户名相关信息生成token返回,浏览器将token记录到cookie中,每次调用api接口都默认将token携带到header请求头中,Spring-Security解析header头获取token信息,解析token获取当前用户名,根据用户名就可以从redis中获取权限列表,这样Spring-Security就能够判断当前请求是否有权限访问。

  2. jwt介绍

    1. 访问令牌的类型
      • By reference token(透明令牌)

        随机生成的字符串标识符,无法简单猜测授权服务器如何颁发和存储

        资源服务器必须通过后端渠道发送OAuth2授权服务器的令牌检查端点,才能校验令牌是否有效,并获取claims / scopes等额外信息。

      • ** By value token(自包含令牌)**

        授权服务器颁发的令牌,包含关于用户或者客户的元数据和声明(claims)

        通过检查签名,期望的颁发者(issuer),期望的接收人aud(audience),或者scope,资源服务器可以在本地校验令牌通常实现为签名的JSON Web Tokens(JWT)。

    2. JWT的组成

    该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。

    每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名。

    - **JWT头**

        JWT头部分是一个描述JWT元数据的JSON对象,通常如下所示。
{
  "alg": "HS256",
  "typ": "JWT"
}
        在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存。
    - **有效载荷**

        有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。JWT指定七个默认字段供选择。

        iss:发行人

        exp:到期时间

        sub:主题

        aud:用户

        nbf:在此之前不可用

        iat:发布时间

        jti:JWT ID用于标识该JWT

        除以上默认字段外,我们还可以自定义私有字段,如下例:
{
  "sub": "1234567890",
  "name": "Helen",
  "admin": true
}
        请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防信息泄露。

        JSON对象也使用Base64 URL算法转换为字符串保存。
    - **签名哈希**

        签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

        首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。

        $HMAC SHA256(base64URLEncode(header) + "." + base64URLEncode(claims),secret)$

        在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象。
    - **Base64URL算法**

        如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL算法。该算法和常见Base64算法类似,稍有差别。

        作为令牌的JWT可以放在URL中(例如api.example / ?token=xxx)。Base64中用的三个字符是 "+","/" 和 "=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"=" 去掉,"+" 用 "-" 替换,"/" 用 "_" 替换,这就是Base64URL算法。
1. **编写核心配置类**

    Spring Security的核心配置就是继承WebSecurityConfigurerAdapter并注解@EnableWebSecurity的配置。这个配置指明了用户名密码的处理方式、请求路径、登录登出控制等和安全相关的配置。
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter{
    // 自定义查询数据库用户名密码和权限信息
    private UserDetailsService UserDetailsService;
    // token管理工具类(生成token)
    private TokenManager tokenManager;
    // 密码管理工具类
    private DefaultPasswordEncoder defaultPasswordEncoder;
    // redis操作工具类
    private RedisTemplate redisTemplate;
    @Autowired
    public TokenWebSecurityConfig(UserDetailsService userDetailsService,
    DefaultPasswordEncoder defaultPasswordEncoder,TokenManager tokenManager,
    RedisTemplate redisTemplate){
        this.userDetailsService = userDetailsService;
        this.defaultPasswordEncoder = defaultPasswordEncoder;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }
    /**
    * 配置设置
    */
    // 设置退出的地址和token,redis操作
    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.exceptionHandling()
            .authenticationEntryPoint(new UnauthorizedEntryPoint())
            .and().csrf().disable()
            .authorizeRequests()
            .anyRequest().authenticated()
            .and().logout().logoutUrl("/admin/acl/index/logout")
            .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate))
            .and().addFilter(new TokenLoginFilter(authenticationManager(),tokenManager,redisTemplate))
            .addFilter(new TokenAuthenticationFilter(authenticationManager(),tokenManager,redisTemplate))
            .httpBasic();
    }
    /**
    * 密码处理
    */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(defaultPasswordEncoder);
    }
    /**
    * 配置哪些请求不拦截
    */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/api/**","/swagger-ui.html/**");
    }
}

8. Spring Security原理总结

  1. Spring Security的过滤器介绍

    Spring Security采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的15个过滤器进行说明:

    (1)WebAsyncManagerIntegrationFilter:将Security上下文与Spring Web中用于处理异步请求映射的WebAsyncManager进行集成。

    (2)SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。

    (3)HeaderWriterFilter:用于将头信息加入响应中。

    (4)CsrfFilter:用于处理跨站请求伪造。

    (5)LogoutFilter:用于处理退出登录。

    (6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为username和password,这两个值可以通过设置这个过滤器的usernameParameter和passwordParamter两个参数的值进行修改。

    (7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

    (8)BasicAuthenticationFilter:检测和处理http basic认证。

    (9)RequestCacheAwareFilter:用来处理请求的缓存。

    (10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。

    (11)AnonymousAuthenticationFilter:检测SecurityContextHolder中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication。

    (12)SessionManagementFilter:管理session的过滤器。

    (13)ExceptionTranslationFilter:处理AccessDeniedException和Authentication异常。

    (14)FilterSecurityInterceptor:可以看作过滤器链的出口。

    (15)RememberMeAuthenticationFilter:当用户没有登陆而直接访问资源时,从cookie里找出用户的信息,如果Spring Security能够识别出用户提供的remember me cookie,用户将不必填写用户名和密码,而是直接登录进入系统,该过滤器默认不开启。

  2. Spring Security基本流程

    Spring Security采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:

绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用Spring Security提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重点介绍以下三个过滤器:

- **UsernamePasswordAuthenticationFilter**过滤器:该过滤器会拦截前端提交的POST方式的登录表单请求,并进行身份认证。
- **ExceptionTranslationFilter**过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
- **FilterSecurityInterceptor**过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由ExceptionTranslationFilter过滤器进行捕获和处理。
  1. Spring Security认证流程

    认证流程是在UsernamePasswordAuthenticationFilter过滤器中处理的,具体流程如下所示:

- **UsernamePasswordAuthenticationFilter源码**

    当前端提交的是一个POST方式的登录表单请求,就会被该过滤器拦截,并进行身份认证。该过滤器的doFilter()方法实现在其抽象父类。
  1. SpringSecurity权限访问流程

    • ExceptionTranslationFilter过滤器

      该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。

    • FilterSecurityInterceptor过滤器

      FilterSecurityInterceptor是过滤器链的最后一个过滤器,该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器ExceptionTranslationFilter进行捕获和处理。

      需要注意,Spring Security的过滤器链是配置在Spring MVC的核心组件DispatcherServlet运行之前。也就是说,请求通过Spring Security的所有过滤器,不意味着能够正常访问资源,该请求还需要通过Spring MVC的拦截器链。

  2. Spring Security请求间共享认证信息

    一般认证成功后的用户信息是通过Session在多个请求之间共享,那么Spring Security中是如何实现将已认证的用户信息对象Authentication与Session绑定的进行具体分析。


6. SecurityContextPersistenceFilter过滤器

前面提到过,在UsernamePasswordAuthenticationFilter过滤器认证成功之后,会在认证成功的处理方法中将已认证的用户信息对象Authentication封装进SecurityContext,并存入SecurityContextHolder。

之后,响应会通过SecurityContextPersistenceFilter过滤器,该过滤器的位置在所有过滤器的最前面,请求到来先进它,响应返回最后一个通过它,所以在该过滤器中处理已认证的用户信息对象Authentication与Session绑定。

认证成功的响应通过SecurityContextPersistenceFilter过滤器时,会从SecurityContextHolder中取出封装了已认证用户信息对象Authentication的SecurityContext,放进Session中。当请求再次到来时,请求首先经过该过滤器,该过滤器会判断当前请求的Session是否存有SecurityContext对象,如果有则将该对象取出再次放入SecurityContextHolder中,之后该请求所在的线程获取认证用户信息,后续的资源访问不需要进行身份认证;当响应再次返回时,该过滤器同样从SecurityContextHolder取出SecurityContext对象,放入Session中。

posted @ 2022-01-22 11:04  ArosyCat  阅读(98)  评论(0编辑  收藏  举报