SpringBoot安全(SpringSecurity、Shiro)

springboot安全

一、SpringSecurity

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

Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求

1、认识SpringSecurity

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

  • WebSecurityConfigurerAdapter:自定义Security策略
  • AuthenticationManagerBuilder:自定义认证策略
  • @EnableWebSecurity:开启WebSecurity模式

Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。

“认证”(Authentication)

身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。

身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

“授权” (Authorization)

授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

这个概念是通用的,而不是只在Spring Security 中存在。

2、SpringSecurity实战

实战是最好的学习方式,简单写个案例。

1、引入 SpringSecurity 依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

2、新建config文件夹,创建SecurityConfig配置文件,并且继承 WebSecurityConfigurerAdapter,添加@EnableWebSecurity注解

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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 首页所有人可以访问,功能也只有对应有权限的人才能访问
// 请求授权的规则
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasAnyRole("vip1")
.antMatchers("/level2/**").hasAnyRole("vip2")
.antMatchers("/level3/**").hasAnyRole("vip3");
// 没有权限默认会到登录页面
// 默认为:/login
// 定制登录页 loginPage("/toLogin");
http.formLogin().loginPage("/toLogin");

http.csrf().disable(); // 关闭csrf功能
// 注销 开启了注销功能,跳转到首页
http.logout().logoutSuccessUrl("/");

// 开启记住我功能 cookie 默认保存两周
http.rememberMe().rememberMeParameter("remember");
}
}

注意这里用到了链式,当我们测试时发现没有权限的时候,会跳转到登录的页面

image-20221216175516122

3、查看刚才登录页的注释信息;我们可以定义认证规则,继续编写SecurityConfig,重写configure(AuthenticationManagerBuilder auth)方法,也可以去jdbc中去取,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 认证  springboot 2.1.x 可以直接使用
// 密码编码:PasswordEncoder
// 在Spring Security 5.0+ 新增了很多加密方式
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 这些数据正常应该从数据库中获得 本例从内存中获得
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("lotus").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}

这样一来,每个角色只能访问自己认证下的规则。并且开启了注销、记住我的功能

image-20221216211111086

这个cookie的保存日期默认为14天,注销的时候,spring security 会帮我们自动删除这个 cookie

二、Shiro

Apache Shiro是一个强大且易用的Java安全框架,可以完成身份验证、授权、密码和会话管理

官网: http://shiro.apache.org/

1、认识Shiro

三个核心组件:Subject,SecurityManager 和 Realms

image-20221216225323402

1、Subject

即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。

Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
2、SecurityManager

它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

3、Realm

Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。

Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。

也就是说对于我们而言,最简单的一个Shiro应用:

应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。
从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入

2、基本功能点

image-20221216225753526

  • Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
  • Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
  • Web Support:Web 支持,可以非常容易的集成到 Web 环境;
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
  • Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
  • Testing:提供测试支持;
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了

3、Shiro实战

1、引入依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.6.0</version>
</dependency>

下面是编写配置文件
Shiro 三大要素

subject -> ShiroFilterFactoryBean —-当前用户
securityManager -> DefaultWebSecurityManager —-管理所有用户
Realm
实际操作中对象创建的顺序 : realm -> securityManager -> subject —-连接数据

2、定义配置类ShiroConfig

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
69
70
71
72
73
74
75
@Configuration
public class ShiroConfig {
/**
* 1、Subject:
* 即“当前操作用户”。它仅仅意味着“当前跟软件交互的东西”。
* Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
*
* 2、SecurityManager:
* 它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
*
* 3、Realm:
* Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
*/

// 第三步:ShiroFilterFactoryBean
// 配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
// 如什么样的请求可以访问,什么样的请求不可以访问等等
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
// 创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器,设置管理的同时也会指定某个Realm 用来完成我们权限分配
bean.setSecurityManager(defaultWebSecurityManager);

// 添加shiro的内置过滤器
/**
anon:无需认证就可以访问
authc:必须认证才可访问
user:必须拥有 记住我才能用
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问
**/

// 拦截
// 定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问,什么样的请求不可以访问
LinkedHashMap<String, String> filterMap = new LinkedHashMap<>();

// 授权 正常的情况下,没有授权会跳转到未授权的页面
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");

filterMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterMap);

// 设置登录的请求
bean.setLoginUrl("/toLogin");
// 未授权页面
bean.setUnauthorizedUrl("/noauth");

return bean;
}

// 第二步:DefaultWebSecurityManager
@Bean(name = "securityManager" )
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}

// 第一步:创建 realm对象 , 需要自定义类
// 配置一个自定义的Realm的bean,最终将使用这个bean返回的对象来完成我们的认证和授权
@Bean(name = "userRealm")
public UserRealm userRealm(){
return new UserRealm();
}


// 整合ShiroDoalect :用来整合 Shiro Thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
}

3、自定义MyRealm类

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
// 自定义的UserRealm extends AuthorizingRealm
public class UserRealm extends AuthorizingRealm {

@Autowired
UserService userService;


//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了授权方法");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();


// 拿到当前登录的这个对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User)subject.getPrincipal(); // 拿到User对象
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了认证方法");


UsernamePasswordToken usertoken = (UsernamePasswordToken) token;

// 链接真实数据库
User user = userService.queryUserByName(usertoken.getUsername());

if (user == null){ // 没有这个人 抛出 UnknownAccountException异常
return null;
}

// 密码认证 shiro来做
return new SimpleAuthenticationInfo(user,user.getPwd(),"");

}
}

4、定义UserController类

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
@Controller
public class MyController {

@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello,Shiro");
return "index";
}

@RequestMapping("/user/add")
public String add(){
return "user/add";
}

@RequestMapping("/user/update")
public String update(){
return "user/update";
}

@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}

@RequestMapping("/login")
public String login(String username,String password,Model model){
// 获取当前用户
Subject subject = SecurityUtils.getSubject();
// 封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);

try {
subject.login(token); // 执行登录的方法,如果没有异常就说明ok了
return "index";
}catch (UnknownAccountException e){ //用户名不存在
model.addAttribute("msg","用户名错误");
return "login";
}catch (IncorrectCredentialsException e){ // 密码错误
model.addAttribute("msg","密码错误");
return "login";
}
}

@RequestMapping("/noauth")
@ResponseBody
public String unauthorized(){
return "未授权无法访问此页面";
}
}

5、整合Mybatis

需要导入的依赖:①mysql-connect ②druid ③log4j ④mybtis-spring-boot-starter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

编写配置文件application.properties或者application.yaml

编写lotus文件下的pojo,controller,service,mapper文件夹参考前面的springboot整合mybatis代码

image-20221217014324475

在mapper中添加了一个queryUserByName的方法

1
2
3
4
5
6
7
8
9
10
@Service
public class UserServiceImpl implements UserService {

@Autowired
UserMapper userMapper;
@Override
public User queryUserByName(String name) {
return userMapper.queryUserByName(name);
}
}

这样一来,我们就简单的体验了shiro的授权和认证功能,值得一提的是,前端页面可以和thymeleaf整合,写出如一下代码的判断

1
2
3
4
<!-- 如果没有user:add权限  则不显示该元素  -->
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">add</a>
</div>

index页面

image-20221217015109477

由于目前登录的用户没有add权限,拥有update字段,所以显示update模块

image-20221217015224985

三、Shiro 和 Spring Security区别

先说个人结论:Shiro 首选 ,上手快 ,也足够用,自由度高,Spring Security中有的,Shiro也基本都有(项目没有使用Spring这一套,不用考虑,直接Shiro)

如果开发项目使用Spring这一套,用Spring Security可能更合适一些;虽然Spring Security 比较复杂,但与Spring 家族结合能力更强,是一个可以放心选择的框架结构

  • Shiro比Spring更容易使用,实现和最重要的理解
  • Spring Security更加知名的唯一原因是因为品牌名称
  • “Spring”以简单而闻名,但讽刺的是很多人发现安装Spring Security很难
  • 然而,Spring Security却有更好的社区支持
  • Apache Shiro在Spring Security处理密码学方面有一个额外的模块
  • Spring-security 对spring 结合较好,如果项目用的springmvc ,使用起来很方便。但是如果项目中没有用到spring,那就不要考虑它了。
  • Shiro 功能强大、且 简单、灵活。是Apache 下的项目比较可靠,且不跟任何的框架或者容器绑定,可以独立运行

个人认为现阶段需求,权限的操作粒度能控制在路径及按钮上,数据粒度通过sql实现。Shrio简单够用。

至于OAuth,OpenID 站点间统一登录功能,现租户与各个产品间单点登录已经通过cookies实现,所以Spring Security的这两个功能可以不考虑。

SpringSide网站的权限也是用Shrio做的。

shiro有很多地方都比spring security方便简单直接,比起spring security的庞大模式更容易理解和切入一些,而spring security比shiro功能上要多一点,再就是和spring框架的无缝对接,比如支持spel等,有时候比shiro更方便灵活。不过spring security的很多源代码我看了感觉可插拔性设计的不够优化,想自己扩展的话要做很多无谓的工作。