SpringSecruity

Time: 2024-10-21 Monday 08:42:01
Author: Jackasher

SpringSecruity

基本使用

总体框架

IMG_1321

先导入 jar 包, SpringSecruity-starterde

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-spring-boot3-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>


</dependencies>

然后自定义UserDetailService,用于获取用户信息的,如果不重写,则会返回一个 user账户和默认密码

IMG_1322

重写后,需要返回一个UserDetail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class UserDetailServiceImpl implements UserDetailsService {


@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne( new LambdaQueryWrapper<User>().eq(User::getUsername, username));
if (Objects.isNull(user)) {
throw new RuntimeException("user查询为 Null");
}
return new LoginUser(user);
}
}

该 UserDetail 会得到账号和密码,这个 UserDetail 会被拿来认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {

private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}

@Override
public String getPassword() {
return null;
}

@Override
public String getUsername() {
return null;
}
}

如果没有配置密码加密会提示

1
java.lang.IllegalArgumentException: You have entered a password with no PasswordEncoder. If that is your intent, it should be prefixed with `{noop}`.

还有一种是直接创建用户

直接通过 User 来创建用户,而且是用 SpringBean 的依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.powernode.config;

import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.stereotype.Service;

/**
* 定义一个bean,用户详情服务接口
*/
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailsService(){
//创建了2个用户,系统的用户
UserDetails user1= User.builder()
.username("eric")
.password(passwordEncoder().encode("123456"))
.roles("student") //角色的前面加上ROLE_student 就变成了权限
.authorities("stduent:delete","student:add") //配置了权限
.build();
UserDetails user2= User.builder()
.username("thomas")
.password(passwordEncoder().encode("123456"))
.authorities("teacher:delete","teacher:add") //配置了权限
.roles("teacher") //ROLE_teacher
.build();
InMemoryUserDetailsManager manager=new InMemoryUserDetailsManager();
manager.createUser(user1);
manager.createUser(user2);

return manager;
}

/**
* 自定义用户必须配置密码编码器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

}

Idea导入 SpringBoot 项目大量爆红

你敢相信是因为加了<relativePath/>导致找不到父工程,所以报错…

权限配置

一个权限对应一个路径, 我们可以把用户与权限封装在一个类, 然后在写方法时指定什么权限可以访问什么方法, 这个颗粒度非常细, 而不是对整个用户进行分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
public class MySecurityUserConfig {
@Bean
public UserDetailsService userDetailsService(){
//创建了2个用户,系统的用户
UserDetails user1= User.builder()
.username("eric")
.password(passwordEncoder().encode("123456"))
.roles("student") //角色的前面加上ROLE_student 就变成了权限
.authorities("stduent:delete","student:add") //配置了权限
.build();
UserDetails user2= User.builder()
.username("thomas")
.password(passwordEncoder().encode("123456"))
.authorities("teacher:delete","teacher:add") //配置了权限
.roles("teacher") //ROLE_teacher
.build();
InMemoryUserDetailsManager manager=new InMemoryUserDetailsManager();
manager.createUser(user1);
manager.createUser(user2);

return manager;
}

SpringSecruity 思想

就我目前的理解, 这种校验目前分为认证和授权, 认证是在 UserDetailService, 返回 UserDetail, 传入的对象对被封装在 Authentication, 然后认证过的对象放在 ContextHolder 里面,而授权是在

简单的认证流程

很多人在刚开始学习 Web 应用程序的编写的时候,应该都使用过下面这种认证方式:

  1. 前端页面获取到用户的账号密码等信息后通过 POST 请求发送给后端
  2. 后端拿到用户的账号密码等信息到数据库查询服务端保存的用户信息
  3. 对比数据库中的用户信息和前端传递过来的账号密码信息
  4. 相同就生成一个 Token 保存到 Seesion 并将 Token 返回给客户端
  5. 前端保存拿到的 Token 后在后续的请求中携带这一 Token 来证明自己的身份

这样的认证方式是很简单的,但是,Spring Security 中的认证流程又何尝不是这样的呢?只不过,Spring Security 通过更加统一的抽象接口实现了这样的认证流程。

3 Spring Security

前面的简单的认证流程中,是可以将一些东西抽象出来作为一个单独的实体,这些实体都可以在 Spring Security 中找到相应的对象,包括:

  1. 用户输入的账号密码等信息,这些东西其实就是用户的认证信息,对应到 Spring Security 中的话就是 Authentication 对象,只不过,Spring Security 中的 Authentication 对象除了保存用户的认证信息以外,还可以用来保存用户认证成功后从数据库中拿到的用户的详细信息。

    1
    2
    3
    4
    5
    6
    7
    8
    public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities(); // 用户权限
    Object getCredentials(); // 用户认证信息
    Object getDetails(); // 用户详细信息
    Object getPrincipal(); // 用户身份信息
    boolean isAuthenticated(); // 当前 Authentication 是否已认证
    void setAuthenticated(boolean isAuthenticated);
    }
  2. 只凭用户提供的认证信息往往是不足以用来判断该用户是否合法的,因此,我们通常还需要某种手段来获取保存在服务端的用户信息,同时,也需要某种手段来保存用户信息,这些对应到 Spring Security 中的话就是 UserDetailsServiceUserDetails 这两个对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
    }

    public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUsername();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
    }
  3. 在拥有了用户提供的认证信息和保存在服务端的用户信息后,我们就需要通过某种方式来比较这两份信息,而这种来效验用户认证信息的对象对应到 Spring Security 中便是 AuthenticationManager 对象了。

    1
    Authentication authenticate(Authentication authentication)throws AuthenticationException;

    而鉴于各种各样的用户认证信息和层出不穷的效验方式,Spring Security 提供了更易于我们扩展的接口 AuthenticationProviderProviderManager 这个默认的 AuthenticationManager 实现。使用时,我们往往就只需要实现 AuthenticationProvider 就足够了。

    1
    2
    3
    4
    public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    boolean supports(Class<?> authentication);
    }

    可以看到,AuthenticationProvider 中的方法 authenticate 会返回一个 Authentication 对象,当通过认证后,这个对象往往会保存用户的详细信息。

  4. 当用户的认证信息通过效验后,我们往往还需要在服务端保存通过的认证信息或生成的令牌,这个保存通过的认证信息的对象在 Spring Security 中就是 SecurityContextSecurityContextHolder 这两个对象, SecurityContext 保存已通过认证的 Authentication 对象,SecurityContextHolder 保存 SecurityContext 到当前线程的上下文,方便我们的使用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public interface SecurityContext extends Serializable {
    Authentication getAuthentication();
    void setAuthentication(Authentication authentication);
    }

    public class SecurityContextHolder {
    public static SecurityContext getContext();
    public void SecurityContext setContext();
    }

    Spring Security 是基于 Filter 来实现的,而每个请求往往也会分配一个线程,因此,Spring Security 在请求到达具体的处理逻辑之前,就可以在 Filter 中完成用户信息的认证,生成 SecurityContext 方面后续的使用。

Session 其实就是 Cookie 的一种存储在浏览器的 Cookie 里面, 在发送请求时, 携带 SessionID, 这个 ID 是 Tomcat 生成的, 返回给浏览器, 浏览器会自动存储, 下一次请求时, 会把 SessionID 放在 Cookie 里面, 而 Cookie 是浏览器自动携带的, Session 方案,服务器存储信息, 浏览器存储钥匙, Cookie 方案, 应该是直接浏览器存储用户信息, 这不安全, 而 JWT 则是加密后的 字符串,本身就代表信息, 然后在服务端解密 ,LocalStorage 里面的东西是不会自动提交的, 需要手动取出来,这是与 Cookie 的不同之处

认证流程 2.0

认证流程,我们可以看到请求进来会有一个 AuthenticationManager 的类, 这个类会把前端输入的信息封装在 Authentication 里面, 然后调用 UserDetailService, 而这个UserDetailService 就是认证流程, 会返回一个 UserDetail,与 Authentication 进行比对账号密码, 如果匹配就放入 SecruityContext 里面

用户登录前,默认生成的Authentication对象处于未认证状态,登录时会交由AuthenticationManager负责进行认证。

AuthenticationManager会将Authentication中的用户名/密码与UserDetails中的用户名/密码对比,完成认证工作,认证成功后会生成一个已认证状态的Authentication对象;

最后把认证通过的Authentication对象写入到SecurityContext中,在用户有后续请求时,可从Authentication中检查权限。

image-20241020124832393Spring Security中默认执行的过滤器顺序如下:

  1. WebAsyncManagerIntegrationFilter
  2. SecurityContextPersistenceFilter
  3. HeaderWriterFilter
  4. CsrfFilter
  5. LogoutFilter
  6. UsernamePasswordAuthenticationFilter
  7. DefaultLoginPageGeneratingFilter
  8. DefaultLogoutPageGeneratingFilter
  9. RequestCacheAwareFilter
  10. SecurityContextHolderAwareRequestFilter
  11. AnonymousAuthenticationFilter:如果之前的认证机制都没有更新 SecurityContextHolder 拥有的 Authentication,那么一个 AnonymousAuthenticationToken 将会设给 SecurityContextHolder。
  12. SessionManagementFilter
  13. ExceptionTranslationFilter:用于处理在 FilterChain 范围内抛出的 AccessDeniedException 和 AuthenticationException,并把它们转换为对应的 Http 错误码返回或者跳转到对应的页面。
  14. FilterSecurityInterceptor:负责保护 Web URI,并且在访问被拒绝时抛出异常。
image-20241020125903541

SpringSecruity
http://example.com/2024/10/21/SpringSecruity/
作者
Jack Asher
发布于
2024年10月21日
许可协议