SpringSecurity

学习源于三更草堂

SpringSecurity

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。这是保护基于Spring的应用程序的事实上的标准。

SpringSecurity 提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC、DI、AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

Spring Security是一个专注于为Java应用程序提供身份验证和授权的框架。像所有Spring项目一样,Spring Security的真正力量在于它有多容易被扩展以满足自定义要求。

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

配置

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

基础项目

SecurityApplication.java

1
2
3
4
5
6
7
8
9
10
package com.boyolo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}

HelloController.java

1
2
3
4
5
6
7
8
9
10
11
package com.boyolo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
@RequestMapping("/hello")
public String helloController() {
return "hello";
}
}

测试

访问 http://localhost:8080/hello

跳转页面

默认用户名:user

默认密码:后台运行复制

认证

SpringSecurity完整流程

SpringSecurity其实就是一个过滤器链,内部包含了各种功能的过滤器。

基础流程图

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。

ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

FilterSecurityInterceptor: 负责权限校验的过滤器。

SpringSecurity过滤器链中有哪些过滤器及它们的顺序:

认证流程详解

Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

AuthenticationManager接口:定义了认证Authentication的方法,在 Spring Security 中,用来处理身份认证的类是 AuthenticationManager,我们也称之为认证管理器。

UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

UserDetails接口(继承了 Serializable 序列化):提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

  1. 自定义登录验证

    1. 自定义登录接口

      调用ProviderManager的方法进行认证 如果认证通过生成jwt

      使用userId作为Key,用户信息作为Value,把用户信息存入redis中

    2. 自定义UserDetailsService

      在这个实现类中去查询数据库

  2. 校验

    定义Jwt认证过滤器,获取token,解析token获取其中的userid,从redis中获取用户信息,存入SecurityContextHolder

授权

授权基本流程

在 SpringSecurity 中,会使用默认的 FilterSecurityInterceptor 来进行权限校验。在 FilterSecurityInterceptor 中会从 SecurityContextHolder 获取其中的 Authentication ,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。

然后设置我们的资源所需要的权限即可。

授权实现

限制访问资源所需权限

可以使用注解去指定访问对应的资源所需的权限

  1. 需要先开启相关配置

    1
    @EnableGlobalMethodSecurity(prePostEnabled = true)
  2. 在方法中使用注解配置权限

    1
    @PreAuthorize("hasAuthority('权限')")

封装权限信息

自定义失败处理

希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。

在 SpringSecurity 中,如果我们在认证或者授权的过程中出现了异常会被 ExceptionTranslationFilter 捕获到。在 ExceptionTranslationFilter 中会去判断是认证失败还是授权失败出现的异常。

如果是认证过程中出现的异常会被封装成 AuthenticationException 然后调用 AuthenticationEntryPoint 对象的方法去进行异常处理。

如果是授权过程中出现的异常会被封装成 AccessDeniedException 然后调用 AccessDeniedHandler 对象的方法去进行异常处理。

跨域

浏览器出于安全的考虑,使用 XMLHttpRequest 对象发起 HTTP 请求时必须遵守同源策略,否则就是跨域的 HTTP 请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

  1. 先对SpringBoot配置,运行跨域请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
    // 设置允许跨域的路径
    registry.addMapping("/**")
    // 设置允许跨域请求的域名
    .allowedOriginPatterns("*")
    // 是否允许cookie
    .allowCredentials(true)
    // 设置允许的请求方式
    .allowedMethods("GET", "POST", "DELETE", "PUT")
    // 设置允许的header属性
    .allowedHeaders("*")
    // 跨域允许时间
    .maxAge(3600);
    }
    }
  2. 开启 SpringSecurity 的跨域访问

    1
    2
    3
    4
    5
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    //允许跨域
    http.cors();
    }

补充

CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。

SpringSecurit 去防止 CSRF 攻击的方式就是通过 csrf_token 。后端会生成一个 csrf_token ,前端发起请求的时候需要携带这个 csrf_token ,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。

CSRF 攻击依靠的是 cookie 中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是 token ,而 token 并不是存储中 cookie 中,并且需要前端代码去把token 设置到请求头中才可以,所以CSRF攻击也就不用担心了。


项目过程

前端输入用户密码传入后端,后端判断用户名密码是否正确,如果正确,会生成一个JWT令牌,返回给前端,如果不正确重新输入用户名密码。

前端拿到JWT令牌之后,会放在请求头中,之后每一次请求都会携带JWT令牌,后端 JWT令牌拦截器,每次有请求进入后端,先判断JWT令牌 是否存在以及令牌是否正确合法有效,如果不存在或者JWT令牌不合法有效,拦截请求,如果JWT令牌合法有效,允许请求访问。

引入依赖

1
2
3
4
5
6
7
8
9
10
11
<!--security 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--JWT 依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

JWT配置

1
2
3
4
5
6
7
8
9
jwt:
# JWT存储的请求头
tokenHeader: Authorization
# JWT 加解密使用的密钥
secret: yeb-secret
# JWT的超期限时间(60*60*24)
expiration: 604800
# JWT 负载中拿到开头
tokenHead: Bearer

编写JwtToken工具类

  1. 根据用户信息生成Token

    1. generateToken(UserDetails userDetails)
    2. generateToken(Map<String,Object> claims)
    3. generateExpirationDate()
  2. 从Token中获取信息

    1. 从Token中获取登录用户名

      public String getUserNameFromToken(String token)

    2. 从Token中获取荷载

      private Claims getClaimsFromToken(String token)

  3. 判断Token是否有效

    1. 判断 Token中的用户名和 用户信息中的用户名是否一致 并且 是否失效

      public boolean validateToken(String token , UserDetails userDetails)

    2. 判断 Token 是否失效

      private boolean isTokenExpired(String token)

    3. 获取 Token 失效时间

      private Date getExpiredDateFromToken(String token)

  4. 判断 Token 是否可以被刷新

    public boolean canRefreshToken(String token)

  5. 刷新 Token

    public String refreshToken(String token)

  1. 前端输入用户密码传入后端,后端判断用户名密码是否正确,如果正确,会生成一个JWT令牌,返回给前端,如果不正确重新输入用户名密码。

    1. 使用 Admin 实现 UserDetails , 重写其中的方法

    2. 使用 AdminServiceImpl 实现类 , 获取前端传入的 用户名、密码、图形验证码;

      1. 获取请求中的的图形验证码进行校验;

      2. 通过 userDetailsService 接口中 loadUserByUsername(username) 根据传入的用户名生成用户信息 userDetails

        校验 userDetails

        校验 前端传入密码是否与数据库中存储的密码进行校验

      3. SpringSecurity全局对象中 更新 用户登录信息

        1
        2
        3
        4
        //全局更新
        //更新security登录用户对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
      4. 生成 Token ,将 Token 以及 请求头放入 map 传入前端

    登录成功

  2. JWT登录授权过滤器

    存在token并且token格式正确,使其登录,同时判断token是否有效,有效重新 SpringSecurity全局对象中 更新 用户登录信息

  3. 自定义未授权和登录结果返回

    1. 未登录或token失效处理器
    2. 授权失败处理器

UserDetailsService接口

UserDetailsService接口 加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

1
UserDetails loadUserByUsername(String username) throw UsernameNotFoundException;

UserDetails接口:提供核心用户信息。通过 UserDetailsService 根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。


UserDetails接口的实现类 User 其中有两个核心构造方法,其中第一个User()调用第二个 User()方法

  • User()方法中的 username 是从 loadUserByUsername(String username) 中前端传入的username,然后 User()方法中的 password 是从数据库中 根据 username 查找对应的 用户,返回其密码(用来和前端传入的密码进行比较)。
1
2
3
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public User(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}

PasswordEncoder 接口 Spring已经实例化,需要自定义重新实现 PasswordEncoder 实例。

该接口共有两个主要方法

  1. 对 密码进行加密

    1
    String encode(CharSequence rawPassword);

    通过对 原始密码 rawPassword 进行加密,返回一个加密后的 字符串

  2. 匹配 原始密码 与 加密后的密码

    1
    boolean matches(CharSequence rawPassword, String encodedPassword);

官方推荐使用PasswordEncoder 接口的实现类 BCryptPasswordEncoder 基于 Hash算法 实现单向加密


UsernamePasswordAuthenticationFilter类 负责处理在登陆页面填写了用户名密码后的登陆请求


Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。


FormLoginConfigurer类 可以自定义登陆成功登陆失败跳转

ForwardAuthenticationSuccessHandler 实现了 AuthenticationSuccessHandler 接口 定义了成功跳转链接

ForwardAuthenticationFailureHandler 实现了 AuthenticationFailureHandler 接口 定义了失败跳转链接


WebSecurityConfigurerAdapter 是个适配器,是构建SecurityFilterChain的关键, 在配置的时候,需要我们自己写个配置类去继承他,然后编写自己所特殊需要的配置

重写的 configure(HttpSecurity http) 的方法就是用来配置 HttpSecurity

http.

anyRequest() 表示所有请求

antMatches() 表示匹配请求

regexMatches() 表示通过正则进行匹配

mvcMatches() 表示匹配 ServletPath 为特有方法


permitAll() 表示不需要被认证

authenticated() 表示需要被认证

hasAuthority() 表示需要某种权限

hasAnyAuthority() 表示需要多个权限其中之一

hasRole() 表示需要某种角色(初始化角色时 前缀 ROLE_ 匹配值不需要前缀)

hasAnyRole() 表示需要多个角色中的其中之一

hasIpAddress() 表示特定的 IP地址 才可以通过


http.csrf() 跨站请求伪造攻击


http.

exceptionHandling() 表示异常处理

accessDeniedHandler() 表示授权失败处理器

authenticationEntryPoint() 表示认证失败处理器


AccessDeniedHandler接口 该类用来统一处理 AccessDeniedException 异常,为授权过程中出现异常

自定义授权过程异常处理,实现AccessDeniedHandler接口,并重写handle() 方法

1
2
void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
throws IOException, ServletException;

基于注解的访问控制

需要通过 @EnableGlobalMethodSecurity 进行开启后使用

@Secured(方法或类) 专门用于判断是否具有角色 ,参数以 ROLE_ 开头

@PreAuthorize/@PostAuthorize(方法或类) 在访问方法或类再执行之前/之后判断权限


Token Auth

基于Token的身份验证方法,在服务端不需要存储用户的登录记录

  1. 客户端使用用户名跟密码请求登陆
  2. 服务端收到请求,验证用户名和密码
  3. 验证成功,服务器端签发一个 Token,将这个Token发送给客户端
  4. 客户端收到Token将其存储
  5. 客户端每次向服务器请求资源的时候携带该 Token
  6. 服务器端收到请求,验证请求中携带的Token,验证成功,返回请求数据

JWT JSON WEB Token ,定义了一种简介的、自包含的协议格式,用于在通信双方传递JSON对象,传递的信息经过数字签名可以被验证和信任。

  1. 基于JSON,方便解析;
  2. 可以在令牌中自定义丰富内容,易扩展;
  3. 通过非对称加密算法及数字签名技术,防止篡改,安全性高;
  4. 资源服务使用JWT可不依赖认证服务即可完成授权。

JWT令牌较长,占存储空间比较大

JWT组成

  1. 头部 Header 描述JWT最基本信息

    对头部的 JSON 字符串 进行 BASE64编码解码

  2. 负载 payload 存放有效信息(不要放敏感信息)

    1. 标准中注册的声明
    2. 公共的声明
    3. 私有的声明
  3. 签证、签名 signature

    1. header(BASE64编码之后的头部信息)
    2. payload (BASE64编码之后的载荷信息)
    3. secret (盐 ,必须保密,保存在服务器端)

JWT签发在服务器端,secret相当于私钥

JJWT:提供端到端的JWT创建和验证的Java库


OAUTH2 认证

优点

  1. 更安全,客户端不接触用户密码
  2. 短寿命和封装的Token
  3. 广泛传播、持续采用
  4. 资源服务器和授权服务器解耦
  5. 集中式授权,简化客户端
  6. 客户可以具有不同的信任级别

缺点

  1. 协议框架太宽泛,造成各种实现的兼容性和互操作性差
  2. 不是一个认证协议,本身不能告诉你任何用户信息

客户凭证(client Credentials) 客户端的clientId和密码用于认证客户

令牌(tokens) 授权服务器在接受到客户请求后,颁发的访问令牌

授权码 仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌

访问令牌 用于代表一个用户或服务直接去访问受保护的资源

刷新令牌 用于去授权服务器获取一个刷新访问令牌

beareaToken 不管谁拿到Token都可以访问资源

proof of Possession Token 可以校验client是否对Token有明确的拥有权

作用域(scopes) 客户请求访问令牌时,由资源拥有者外指定的细分权限

  1. 用户访问,此时没有Token,Oauth2RestTemplate报错,报错信息会被Oauth2ClientContextFilter捕获,并重定向到认证服务器;
  2. 认证服务器通过 Authorization Endpoint进行授权,并通过 AuthorizationServerTokenServices 生成授权码并返回给客户;
  3. 客户端拿到授权码去认证服务器通过 Token Endpoint 调用 AuthorizationServerTokenServices 生成Token 并返回给客户端
  4. 客户端拿到Token 去资源服务器访问资源,一般会通过 Oauth2AuthenticationManager 调用 ResourceServerTokenServices 进行校验,校验通过可获取资源
查看评论