详解Spring Boot Security

时间:2021-07-07 15:53:50

简介

spring security,这是一种基于 spring aop 和 servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 web 请求级和方法调用级处理身份确认和授权。

工作流程

从网上找了一张spring security 的工作流程图,如下。

详解Spring Boot Security

图中标记的myxxx,就是我们项目中需要配置的。

快速上手

建表

表结构

详解Spring Boot Security

建表语句

?
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
drop table if exists `user`;
drop table if exists `role`;
drop table if exists `user_role`;
drop table if exists `role_permission`;
drop table if exists `permission`;
create table `user` (
`id` bigint(11) not null auto_increment,
`username` varchar(255) not null,
`password` varchar(255) not null,
primary key (`id`)
);
create table `role` (
`id` bigint(11) not null auto_increment,
`name` varchar(255) not null,
primary key (`id`)
);
create table `user_role` (
`user_id` bigint(11) not null,
`role_id` bigint(11) not null
);
create table `role_permission` (
`role_id` bigint(11) not null,
`permission_id` bigint(11) not null
);
create table `permission` (
`id` bigint(11) not null auto_increment,
`url` varchar(255) not null,
`name` varchar(255) not null,
`description` varchar(255) null,
`pid` bigint(11) not null,
primary key (`id`)
);
insert into user (id, username, password) values (1,'user','e10adc3949ba59abbe56e057f20f883e');
insert into user (id, username , password) values (2,'admin','e10adc3949ba59abbe56e057f20f883e');
insert into role (id, name) values (1,'user');
insert into role (id, name) values (2,'admin');
insert into permission (id, url, name, pid) values (1,'/user/common','common',0);
insert into permission (id, url, name, pid) values (2,'/user/admin','admin',0);
insert into user_role (user_id, role_id) values (1, 1);
insert into user_role (user_id, role_id) values (2, 1);
insert into user_role (user_id, role_id) values (2, 2);
insert into role_permission (role_id, permission_id) values (1, 1);
insert into role_permission (role_id, permission_id) values (2, 1);
insert into role_permission (role_id, permission_id) values (2, 2);

pom.xml

?
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-security</artifactid>
</dependency>
<dependency>
 <groupid>org.springframework.boot</groupid>
 <artifactid>spring-boot-starter-thymeleaf</artifactid>
</dependency>
<dependency>
 <groupid>org.springframework.boot</groupid>
 <artifactid>spring-boot-starter-web</artifactid>
</dependency>
<dependency>
 <groupid>org.thymeleaf.extras</groupid>
 <artifactid>thymeleaf-extras-security4</artifactid>
</dependency>

user

?
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
public class user implements userdetails , serializable {
 private long id;
 private string username;
 private string password;
 private list<role> authorities;
 public long getid() {
 return id;
 }
 public void setid(long id) {
 this.id = id;
 }
 @override
 public string getusername() {
 return username;
 }
 public void setusername(string username) {
 this.username = username;
 }
 @override
 public string getpassword() {
 return password;
 }
 public void setpassword(string password) {
 this.password = password;
 }
 @override
 public list<role> getauthorities() {
 return authorities;
 }
 public void setauthorities(list<role> authorities) {
 this.authorities = authorities;
 }
 /**
 * 用户账号是否过期
 */
 @override
 public boolean isaccountnonexpired() {
 return true;
 }
 /**
 * 用户账号是否被锁定
 */
 @override
 public boolean isaccountnonlocked() {
 return true;
 }
 /**
 * 用户密码是否过期
 */
 @override
 public boolean iscredentialsnonexpired() {
 return true;
 }
 /**
 * 用户是否可用
 */
 @override
 public boolean isenabled() {
 return true;
 }
}

上面的 user 类实现了 userdetails 接口,该接口是实现spring security 认证信息的核心接口。其中 getusername 方法为 userdetails 接口 的方法,这个方法返回 username,也可以是其他的用户信息,例如手机号、邮箱等。getauthorities() 方法返回的是该用户设置的权限信息,在本实例中,从数据库取出用户的所有角色信息,权限信息也可以是用户的其他信息,不一定是角色信息。另外需要读取密码,最后几个方法一般情况下都返回 true,也可以根据自己的需求进行业务判断。

role

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class role implements grantedauthority {
 private long id;
 private string name;
 public long getid() {
 return id;
 }
 public void setid(long id) {
 this.id = id;
 }
 public string getname() {
 return name;
 }
 public void setname(string name) {
 this.name = name;
 }
 @override
 public string getauthority() {
 return name;
 }
}

role 类实现了 grantedauthority 接口,并重写 getauthority() 方法。权限点可以为任何字符串,不一定是非要用角色名。

所有的authentication实现类都保存了一个grantedauthority列表,其表示用户所具有的权限。grantedauthority是通过authenticationmanager设置到authentication对象中的,然后accessdecisionmanager将从authentication中获取用户所具有的grantedauthority来鉴定用户是否具有访问对应资源的权限。

myuserdetailsservice

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@service
public class myuserdetailsservice implements userdetailsservice {
 @autowired
 private usermapper usermapper;
 @autowired
 private rolemapper rolemapper;
 @override
 public userdetails loaduserbyusername(string username) throws usernamenotfoundexception {
 //查数据库
 user user = usermapper.loaduserbyusername( username );
 if (null != user) {
  list<role> roles = rolemapper.getrolesbyuserid( user.getid() );
  user.setauthorities( roles );
 }
 return user;
 }
}

service 层需要实现 userdetailsservice 接口,该接口是根据用户名获取该用户的所有信息, 包括用户信息和权限点。

myinvocationsecuritymetadatasourceservice

?
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
@component
public class myinvocationsecuritymetadatasourceservice implements filterinvocationsecuritymetadatasource {
 
 @autowired
 private permissionmapper permissionmapper;
 
 /**
 * 每一个资源所需要的角色 collection<configattribute>决策器会用到
 */
 private static hashmap<string, collection<configattribute>> map =null;
 
 
 /**
 * 返回请求的资源需要的角色
 */
 @override
 public collection<configattribute> getattributes(object o) throws illegalargumentexception {
 if (null == map) {
  loadresourcedefine();
 }
 //object 中包含用户请求的request 信息
 httpservletrequest request = ((filterinvocation) o).gethttprequest();
 for (iterator<string> it = map.keyset().iterator() ; it.hasnext();) {
  string url = it.next();
  if (new antpathrequestmatcher( url ).matches( request )) {
  return map.get( url );
  }
 }
 
 return null;
 }
 
 @override
 public collection<configattribute> getallconfigattributes() {
 return null;
 }
 
 @override
 public boolean supports(class<?> aclass) {
 return true;
 }
 
 /**
 * 初始化 所有资源 对应的角色
 */
 public void loadresourcedefine() {
 map = new hashmap<>(16);
 //权限资源 和 角色对应的表 也就是 角色权限 中间表
 list<rolepermisson> rolepermissons = permissionmapper.getrolepermissions();
 
 //某个资源 可以被哪些角色访问
 for (rolepermisson rolepermisson : rolepermissons) {
 
  string url = rolepermisson.geturl();
  string rolename = rolepermisson.getrolename();
  configattribute role = new securityconfig(rolename);
 
  if(map.containskey(url)){
  map.get(url).add(role);
  }else{
  list<configattribute> list = new arraylist<>();
  list.add( role );
  map.put( url , list );
  }
 }
 }
}

myinvocationsecuritymetadatasourceservice 类实现了 filterinvocationsecuritymetadatasource,filterinvocationsecuritymetadatasource 的作用是用来储存请求与权限的对应关系。

filterinvocationsecuritymetadatasource接口有3个方法:

boolean supports(class<?> clazz):指示该类是否能够为指定的方法调用或web请求提供configattributes。

collection

 getallconfigattributes():spring容器启动时自动调用, 一般把所有请求与权限的对应关系也要在这个方法里初始化, 保存在一个属性变量里。

collection

 getattributes(object object):当接收到一个http请求时, filtersecurityinterceptor会调用的方法. 参数object是一个包含url信息的httpservletrequest实例. 这个方法要返回请求该url所需要的所有权限集合。

myaccessdecisionmanager

?
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
/**
 * 决策器
 */
@component
public class myaccessdecisionmanager implements accessdecisionmanager {
 
 private final static logger logger = loggerfactory.getlogger(myaccessdecisionmanager.class);
 
 /**
 * 通过传递的参数来决定用户是否有访问对应受保护对象的权限
 *
 * @param authentication 包含了当前的用户信息,包括拥有的权限。这里的权限来源就是前面登录时userdetailsservice中设置的authorities。
 * @param object 就是filterinvocation对象,可以得到request等web资源
 * @param configattributes configattributes是本次访问需要的权限
 */
 @override
 public void decide(authentication authentication, object object, collection<configattribute> configattributes) throws accessdeniedexception, insufficientauthenticationexception {
 if (null == configattributes || 0 >= configattributes.size()) {
  return;
 } else {
  string needrole;
  for(iterator<configattribute> iter = configattributes.iterator(); iter.hasnext(); ) {
  needrole = iter.next().getattribute();
 
  for(grantedauthority ga : authentication.getauthorities()) {
   if(needrole.trim().equals(ga.getauthority().trim())) {
   return;
   }
  }
  }
  throw new accessdeniedexception("当前访问没有权限");
 }
 
 }
 
 /**
 * 表示此accessdecisionmanager是否能够处理传递的configattribute呈现的授权请求
 */
 @override
 public boolean supports(configattribute configattribute) {
 return true;
 }
 
 /**
 * 表示当前accessdecisionmanager实现是否能够为指定的安全对象(方法调用或web请求)提供访问控制决策
 */
 @override
 public boolean supports(class<?> aclass) {
 return true;
 }}

myaccessdecisionmanager 类实现了accessdecisionmanager接口,accessdecisionmanager是由abstractsecurityinterceptor调用的,它负责鉴定用户是否有访问对应资源(方法或url)的权限。

myfiltersecurityinterceptor

?
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
@component
public class myfiltersecurityinterceptor extends abstractsecurityinterceptor implements filter {
 @autowired
 private filterinvocationsecuritymetadatasource securitymetadatasource;
 @autowired
 public void setmyaccessdecisionmanager(myaccessdecisionmanager myaccessdecisionmanager) {
 super.setaccessdecisionmanager(myaccessdecisionmanager);
 }
 @override
 public void dofilter(servletrequest servletrequest, servletresponse servletresponse, filterchain filterchain) throws ioexception, servletexception {
 filterinvocation fi = new filterinvocation(servletrequest, servletresponse, filterchain);
 invoke(fi);
 }
 public void invoke(filterinvocation fi) throws ioexception, servletexception {
 interceptorstatustoken token = super.beforeinvocation(fi);
 try {
  //执行下一个拦截器
  fi.getchain().dofilter(fi.getrequest(), fi.getresponse());
 } finally {
  super.afterinvocation(token, null);
 }
 }
 @override
 public class<?> getsecureobjectclass() {
 return filterinvocation.class;
 }
 @override
 public securitymetadatasource obtainsecuritymetadatasource() {
 return this.securitymetadatasource;
 }
}

每种受支持的安全对象类型(方法调用或web请求)都有自己的拦截器类,它是abstractsecurityinterceptor的子类,abstractsecurityinterceptor 是一个实现了对受保护对象的访问进行拦截的抽象类。

abstractsecurityinterceptor的机制可以分为几个步骤:

1. 查找与当前请求关联的“配置属性(简单的理解就是权限)”
2. 将 安全对象(方法调用或web请求)、当前身份验证、配置属性 提交给决策器(accessdecisionmanager)
3. (可选)更改调用所根据的身份验证
4. 允许继续进行安全对象调用(假设授予了访问权)
5. 在调用返回之后,如果配置了afterinvocationmanager。如果调用引发异常,则不会调用afterinvocationmanager。

abstractsecurityinterceptor中的方法说明:

beforeinvocation()方法实现了对访问受保护对象的权限校验,内部用到了accessdecisionmanager和authenticationmanager;

finallyinvocation()方法用于实现受保护对象请求完毕后的一些清理工作,主要是如果在beforeinvocation()中改变了securitycontext,则在finallyinvocation()中需要将其恢复为原来的securitycontext,该方法的调用应当包含在子类请求受保护资源时的finally语句块中。

afterinvocation()方法实现了对返回结果的处理,在注入了afterinvocationmanager的情况下默认会调用其decide()方法。
了解了abstractsecurityinterceptor,就应该明白了,我们自定义myfiltersecurityinterceptor就是想使用我们之前自定义的 accessdecisionmanager 和 securitymetadatasource。

securityconfig

@enablewebsecurity注解以及websecurityconfigureradapter一起配合提供基于web的security。自定义类 继承了websecurityconfigureradapter来重写了一些方法来指定一些特定的web安全设置。

?
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
@configuration
@enablewebsecurity
public class securityconfig extends websecurityconfigureradapter {
 @autowired
 private myuserdetailsservice userservice;
 @autowired
 public void configureglobal(authenticationmanagerbuilder auth) throws exception {
  //校验用户
  auth.userdetailsservice( userservice ).passwordencoder( new passwordencoder() {
   //对密码进行加密
   @override
   public string encode(charsequence charsequence) {
    system.out.println(charsequence.tostring());
    return digestutils.md5digestashex(charsequence.tostring().getbytes());
   }
   //对密码进行判断匹配
   @override
   public boolean matches(charsequence charsequence, string s) {
    string encode = digestutils.md5digestashex(charsequence.tostring().getbytes());
    boolean res = s.equals( encode );
    return res;
   }
  } );
 }
 @override
 protected void configure(httpsecurity http) throws exception {
  http.authorizerequests()
    .antmatchers("/","index","/login","/login-error","/401","/css/**","/js/**").permitall()
    .anyrequest().authenticated()
    .and()
    .formlogin().loginpage( "/login" ).failureurl( "/login-error" )
    .and()
    .exceptionhandling().accessdeniedpage( "/401" );
  http.logout().logoutsuccessurl( "/" );
 }
}

 

maincontroller

?
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
@controller
public class maincontroller {
 @requestmapping("/")
 public string root() {
  return "redirect:/index";
 }
 @requestmapping("/index")
 public string index() {
  return "index";
 }
 @requestmapping("/login")
 public string login() {
  return "login";
 }
 @requestmapping("/login-error")
 public string loginerror(model model) {
  model.addattribute( "loginerror" , true);
  return "login";
 }
 @getmapping("/401")
 public string accessdenied() {
  return "401";
 }
 @getmapping("/user/common")
 public string common() {
  return "user/common";
 }
 @getmapping("/user/admin")
 public string admin() {
  return "user/admin";
 }
}

页面

login.html

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<head>
 <meta charset="utf-8">
 <title>首页</title>
</head>
<body>
 <h2>page list</h2>
 <a href="/user/common" rel="external nofollow" rel="external nofollow" >common page</a>
 <br/>
 <a href="/user/admin" rel="external nofollow" rel="external nofollow" >admin page</a>
 <br/>
 <form th:action="@{/logout}" method="post">
  <input type="submit" class="btn btn-primary" value="注销"/>
 </form>
</body>
</html>

index.html

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<head>
 <meta charset="utf-8">
 <title>首页</title>
</head>
<body>
 <h2>page list</h2>
 <a href="/user/common" rel="external nofollow" rel="external nofollow" >common page</a>
 <br/>
 <a href="/user/admin" rel="external nofollow" rel="external nofollow" >admin page</a>
 <br/>
 <form th:action="@{/logout}" method="post">
  <input type="submit" class="btn btn-primary" value="注销"/>
 </form>
</body>
</html>

admin.html

?
1
2
3
4
5
6
7
8
9
<!doctype html>
<head>
 <meta charset="utf-8">
 <title>admin page</title>
</head>
<body>
 success admin page!!!
</body>
</html>

common.html

?
1
2
3
4
5
6
7
8
9
<!doctype html>
<head>
 <meta charset="utf-8">
 <title>common page</title>
</head>
<body>
 success common page!!!
</body>
</html>

401.html

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!doctype html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <title>401 page</title>
</head>
<body>
 <div>
  <div>
   <h2>权限不够</h2>
   <p>拒绝访问!</p>
  </div>
 </div>
</body>
</html>

最后运行项目,可以分别用 user、admin 账号 去测试认证和授权是否正确。

总结

以上所述是小编给大家介绍的spring boot security,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对服务器之家网站的支持!