0%

Spring Security——权限管理概念、基本原理、关键的接口

权限管理中的相关概念

主体 principal

  • 使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系 统谁就是主体。

认证 authentication

  • 权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证 明自己是谁。 笼统的认为就是以前所做的登录操作。

授权 authorization

  • 将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功 能的能力。 所以简单来说,授权就是给用户分配权限。

SpringSecurity的基本原理

640?wx_fmt=png

  • SpringSecurity 本质是一个过滤器链,它采用的是责任链的设计模式,就是通过一个又一个过滤器最终通过认证的,下面对各个过滤器进行说明:
    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 和 passwordParameter 两个参数的值进行修改。
    7. DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
    8. BasicAuthenticationFilter:检测和处理 http basic 认证。
    9. RequestCacheAwareFilter:用来处理请求的缓存。
    10. SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
    11. AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在
    12. Authentication 对象,如果不存在为其提供一个匿名 Authentication。
    13. SessionManagementFilter:管理 session 的过滤器
    14. ExceptionTranslationFilter:处理 AccessDeniedException 和AuthenticationException 异常。
    15. FilterSecurityInterceptor:可以看做过滤器链的出口。
    16. RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
  • 比较关键的有三个过滤器:
    1. FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。 fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。
    2. ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常。
    3. UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户 名,密码。这里要求登录发出的请求必须是Post请求,并且请求的参数一个是username,一个是password,参数的键值不允许更改,否则后端将接受不到用户名和密码。

关键的接口

UserDetailService

  • 当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中 账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。
  • 如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:
1
2
3
4
5
6
public interface UserDetailsService {
/**
* UserDetails即为用户主体,通过用户名来加载数据库中的用户,并进行认证
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
  • 使用时,我们只需要将我们自定义的UserService实现UserDetailService接口,并实现loadUserByUsername方法,该方法返回null时,代表认证失败,返回UserDetails实现类的对象时,代表认证成功。

UserDetail

  • UserDetails即为用户主体,它的定义如下:
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
public interface UserDetails extends Serializable {
/**
* 角色列表,GrantedAuthority是角色类接口,它有三个实现类,分别是SimpleGrantedAuthority、JaasGrantedAuthority和SwitchUserGrantedAuthority,一般情况下使用SimpleGrantedAuthority就可以了
*/
Collection<? extends GrantedAuthority> getAuthorities();

/**
* 密码
*/
String getPassword();

/**
* 用户名
*/
String getUsername();

/**
* 指示用户的帐户是否已过期。过期的帐户无法通过身份验证。
*/
boolean isAccountNonExpired();

/**
* 指示用户是锁定还是解锁。锁定的用户无法通过身份验证。
*/
boolean isAccountNonLocked();

/**
* 指示用户的凭据(密码)是否已过期。过期的凭据会阻止身份验证。
*/
boolean isCredentialsNonExpired();

/**
* 指示启用还是禁用用户。禁用的用户无法通过身份验证。
*/
boolean isEnabled();
}
  • 使用UserDetails有三种方式:
    1. 我们可以直接使用它的实现类User
    2. 我们可以自定义User来实现UserDetails,然后使用自定义的User。
    3. 我们可以使用自定义的User,然后将自定义的User转换为已有的User。

PasswordEncoder

  • 该接口表示密码的加密器,即使用某种编码对密码进行加密,它的定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface PasswordEncoder {

/**
* 对密码进行加密处理
*/
String encode(CharSequence rawPassword);

/**
* 对密码进行匹配处理,匹配成功返回true
*/
boolean matches(CharSequence rawPassword, String encodedPassword);

default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
  • PasswordEncoder的一个常用的实现类是BCryptPasswordEncoder,它使用BCrypt强哈希函数的PasswordEncoder实现。下面是测试的一个例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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);
}

例子

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

创建数据库

  • 准备用户表、角色表和用户角色关联表。配置数据源这部分忽略。
1
2
3
4
5
6
create table sys_user(
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL UNIQUE COMMENT '用户名',
`password` varchar(128) NOT NULL COMMENT '密码',
primary key (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户表';

编写配置类

  • 下面的各种页面,可以根据需要自行编写,有一个地方需要注意的是:当url同时制定了权限认证和角色认证的时候,优先使用角色认证,如果角色认证不通过,即使权限是对的,也会认证失败。
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Configuration
//表示该配置是Security的配置类
@EnableWebSecurity
//开启授权注解
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;

@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//antMatchers用来指定一系列的url,permitAll表示放行动作,即将指定的url放行
.antMatchers("/login.jsp", "/img/**", "/failer.jsp", "/css/**", "/plugins/**").permitAll()
//anyRequest表示其他的请求资源,authenticated表示认证动作,即其他资源必须认证才能访问
.anyRequest()
.authenticated()
//表示一个新的配置开始
.and()
//登录表单的配置
.formLogin().loginPage("/login.html") //指定登录页
.loginProcessingUrl("/login") //指定登录的url
.defaultSuccessUrl("/test/index") //defaultSuccessUrl是用来指定登录成功后跳转的url
.failureUrl("/failure.html").permitAll() //failureUrl是用来指定失败后跳转到页面的
.permitAll() //指定以上几个页面放行
.and()
.authorizeRequests()
//指定某个api需要认证的权限,当有admin权限时,就可以访问/test/hello
.antMatchers("/test/hello").hasAuthority("admin")
//指定某个api需要认证的权限集合,当有其中一个权限时,就可以访问/test/hello
// .antMatchers("/test/hello").hasAnyAuthority("admin","abc")
//指定某个api需要认证的角色,当有admin角色时,就可以访问/test/hello
.antMatchers("/test/hello").hasRole("admin")
//指定某个api需要认证的角色集合,当有其中一个角色时,就可以访问/test/hello
// .antMatchers("/test/hello").hasAnyRole("")
//.and()
//登出配置
//.logout()
//.logoutUrl("/logout") //指定登出的url
//.logoutSuccessUrl("/login.html") //指定登出后跳转的页面
//登出是否清空session
//.invalidateHttpSession(true)
//.permitAll()
.and()
//配置403没有权限的页面
.exceptionHandling().accessDeniedPage("/unauth.html")
.and()
//关闭csrf拦截
.csrf().disable();
}


@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中设置一个用户
//auth.inMemoryAuthentication()
// .withUser("zhangsan")
// .password("$2a$10$VXEGCuO40DmEZy1I93qfq.kA5Ypyo4WCUuhfkxbPbmhuQcOZZZaA6")
// .roles("ADMIN");
//配置一个Service作为Security认证逻辑模块,并设置密码加密器
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
}

编写html界面

  • 这里需要注意的是,提交表单的请求必须是post请求,提交表单的参数名必须是username和password。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--login.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login", method="post">
用户名:<input type="text", name="username">
<br>
密码:<input type="text", name="password">
<br>
<input type="submit", value="登录">
</form>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
<!--failure.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>登录失败</p>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
<!--unauth.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>没有权限</p>
</body>
</html>

编写UserDetailsService的实现类

  • 这里可以根据需要结合不同的数据库访问技术,比如JDBC、MyBatis等,这里使用的是MyBatisPlus。
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
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
//密码加密器
@Autowired
private BCryptPasswordEncoder passwordEncoder;

@Override
public boolean insertUser(User user, List<Long> roles) {
//当添加用户时,对密码进行加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
...
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
//这里查数据库
User user = getUserByName(username);
if (user == null) {
//返回null表示验证失败
return null;
}
//若用户存在
//使用AuthorityUtils中的方法来临时创建一个权限列表,权限直接写,角色需要在前面加上ROLE_
List<GrantedAuthority> roles = AuthorityUtils.commaSeparatedStringToAuthorityList("abc,ROLE_admin");

return new User(user.getUsername(), user.getPassword(), roles);
} catch (Exception e) {
e.printStackTrace();
//出现异常验证失败
return null;
}
}
}

编写一个测试的控制器

1
2
3
4
5
6
7
8
9
10
11
@RestfulController("/test")
class TestController{
@GetMapping("/index")
public String index(){
return "index";
}
@GetMapping("hello")
public String hello(){
return "hello,security";
}
}
  • 最后运行测试。

-------------本文结束感谢您的阅读-------------