前言

在Web开发中,安全是第一位!

在做网站的过程中,安全应该在设计之初就进行考虑。

在以往的开发中,我们可以通过配置一些过滤器与拦截器来实现一些简单的安全操作,但是过滤器和拦截器的使用需要编写大量的原生代码,使得项目过于繁琐,产生冗余

接下来我们学习两个常用的安全框架:SpringSecurity、Shiro

笔记作于跟随 狂神说Java b站视频学习过程中

SpringSecurity

简介

  • 官网:https://spring.io/projects/spring-security

    官方文档:https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/

  • Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。 它是用于保护基于Spring的应用程序的实际标准。 Spring Security是一个框架,致力于为Java应用程序提供身份验证和授权。 与所有Spring项目一样,Spring Security的真正强大之处在于可以轻松扩展以满足自定义要求(翻译~)

    Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

    Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

  • 几个重要的类:

    • WebSecurityConfigurerAdapter:自定义Security策略
    • AutnenticationManagerBuilder:自定义认证模式
    • @EnableWebSecurity:开启WebSecurity模式
  • SpringSecurity的主要任务:

    • 认证(Authentication)
    • 授权(Authorization)
  • 本质:

    采用 AOP 将安全组件横切进去,不影响业务逻辑!

认证与授权

1. 环境搭建

image-20200518102459647

image-202005181025421332. 导入依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

image-20200518101124356

3. 编写配置类
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
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

/**
* 授权
* 使用链式编程
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//首页所有人可以访问,功能页只有对应有权限的人可以访问
//请求授权的规则
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");

//没有权限自动跳转到登陆页面
http.formLogin();
// .loginPage("/toLogin"); 设置登陆页面,默认为 "/login"

//注销:开启注销功能
http.logout();
// .deleteCookies("remove") 清空Cookie
// .invalidateHttpSession(true) 清空Session
// .logoutSuccessUrl(""); 设置注销后跳转的页面,默认是登陆页面

//开启 记住我 功能
//本质:cookie,默认保存两周
http.rememberMe();
}

/**
* 认证
* 密码编码问题:java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
* 在 Spring 5.0+ 中加入了很多加密方法
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

//正常应该去数据库中读
//auth.jdbcAuthentication(). ...

//从内存中读取
auth.inMemoryAuthentication()
//必须要添加加密方式
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("wxshhh")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("vip1", "vip2")
//通过 and 方法 可添加多个用户
.and()
.withUser("root")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("vip1", "vip2", "vip3")
.and()
.withUser("guest")
.password(new BCryptPasswordEncoder().encode("123456"))
.roles("vip1");
}
}
4. 运行测试
  1. 初次访问各 level页面 时会跳转到登陆页面,需要进行登陆操作

image-20200518113922103

  1. 登陆用户 “wxshhh”

    • level1的所有页面可以正常访问

    image-20200518104944098

    • level2的所有页面也可以正常访问

    image-20200518105043839

    • level3的页面报错:403

    image-20200518105109990

    ​ 结果与配置结果一致,配置成功!

  2. 注销用户

    1
    2
    3
    4
    <!-- 设置注销页面的请求路径:/logout -->
    <a class="item" th:href="@{/logout}">
    <i class="sign-out icon"></i> 注销
    </a>

    image-20200518110231858

    跳转到注销页面:

    image-20200518110248734

    确认注销之后跳转到登陆页面:

    image-20200518110320875

Thymeleaf 与 SpringSecurity 整合

  1. 导入依赖

    1
    2
    3
    4
    5
    6
    <!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 -->
    <dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
    <version>3.0.2.RELEASE</version>
    </dependency>
  2. 修改 index.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!--登录注销-->
    <div class="right menu">

    <!--未登录,只显示登陆按钮-->
    <div sec:authorize="!isAuthenticated()">
    <a class="item" th:href="@{/toLogin}">
    <i class="address card icon"></i> 登录
    </a>
    </div>

    <!--已登录,显示用户名和注销按钮-->
    <div sec:authorize="isAuthenticated()">
    <a class="item">
    用户名:<span sec:authentication="name"></span>
    </a>
    <a class="item" th:href="@{/logout}">
    <i class="sign-out icon"></i> 注销
    </a>
    </div>

    </div>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!-- 根据用户角色的不同显示不同的菜单 -->
    <div class="ui three column stackable grid">
    <div class="column" sec:authorize="hasRole('vip1')">
    ……
    </div>

    <div class="column" sec:authorize="hasRole('vip2')">
    ……
    </div>

    <div class="column" sec:authorize="hasRole('vip3')">
    ……
    </div>
    </div>

    ==注意!要使用较低版本的SpringBoot==

Shiro

简介

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

Apache Shiro™是一个功能强大且易于使用的Java安全框架,它执行身份验证,授权,加密和会话管理。 使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序-从最小的移动应用程序到最大的Web和企业应用程序。

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.

Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。

其基本功能如下图所示:

image-20200520094749416

  • Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;

  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

  • Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;

  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

  • Web Support:Web 支持,可以非常容易的集成到 Web 环境;

  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;

  • Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

  • Testing:提供测试支持;

  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

Shiro外部:

image-20200520095526775

  • Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;

  • SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;

  • Realm:域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。

快速开始

  1. 导入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <dependencies>
    <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
    </dependency>
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.21</version>
    </dependency>
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.26</version>
    </dependency>
    <dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
    </dependency>
    </dependencies>
  2. 配置文件 shiro.ini

    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
    # -----------------------------------------------------------------------------
    # Users and their assigned roles
    #
    # Each line conforms to the format defined in the
    # org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc
    # -----------------------------------------------------------------------------
    [users]
    # user 'root' with password 'secret' and the 'admin' role
    root = secret, admin
    # user 'guest' with the password 'guest' and the 'guest' role
    guest = guest, guest
    # user 'presidentskroob' with password '12345' ("That's the same combination on
    # my luggage!!!" ;)), and role 'president'
    presidentskroob = 12345, president
    # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
    darkhelmet = ludicrousspeed, darklord, schwartz
    # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
    lonestarr = vespa, goodguy, schwartz

    # -----------------------------------------------------------------------------
    # Roles with assigned permissions
    #
    # Each line conforms to the format defined in the
    # org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
    # -----------------------------------------------------------------------------
    [roles]
    # 'admin' role has all permissions, indicated by the wildcard '*'
    admin = *
    # The 'schwartz' role can do anything (*) with any lightsaber:
    schwartz = lightsaber:*
    # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
    # license plate 'eagle5' (instance specific id)
    goodguy = winnebago:drive:eagle5
  3. HelloWord(部分重要代码)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // get the currently executing user:
    Subject currentUser = SecurityUtils.getSubject();

    Session session = currentUser.getSession();

    if (!currentUser.isAuthenticated()) {
    ……
    }

    currentUser.getPrincipal();

    if (currentUser.hasRole("schwartz")) {
    ……
    }

    if (currentUser.isPermitted("lightsaber:wield")) {
    ……
    }

    currentUser.logout();

    SpringSecurity 中都有,只是名字变了!

集成SpringBoot

1. 环境准备

image-20200520160459569

2. 导入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- spring-shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>

<!-- thymeleaf -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
3. 编写配置类

从下往上编写

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
package com.wxshhh.config;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {

//3. DefaultWebSecurityManager
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}


//2. DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm);
return securityManager;
}

//1. 创建 Realm 对象
@Bean(name = "userRealm")
public UserRealm userRealm() {
return new UserRealm();
}
}
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
package com.wxshhh.config;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

/**
* 自定义的 realm
*/
public class UserRealm extends AuthorizingRealm {

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("=======执行了授权=======");
return null;
}

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("=======执行了认证=======");
return null;
}
}
4. 实现登陆拦截
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);

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

Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/*", "authc");

//设置登录请求
bean.setLoginUrl("/toLogin");

bean.setFilterChainDefinitionMap(filterChainDefinitionMap);

return bean;
}
5. 实现用户认证

IndexController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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);
return "/index";
} catch (UnknownAccountException e) { //用户名不存在
model.addAttribute("msg", "用户名错误!");
return "/login";
} catch (IncorrectCredentialsException e) { //密码不存在
model.addAttribute("msg", "密码错误!");
return "/login";
}
}

UserRealm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("=======执行了认证=======");
//伪造数据库
User user = new User("root","123456");

UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

if (!userToken.getUsername().equals(user.getUsername())) {
return null; //抛出异常 UnKnownAccountException
}
//密码认证,shiro来做!
return new SimpleAuthenticationInfo(user,user.getPassword(),"");
}
6. 实现权限控制

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
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);

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

Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//设置权限
filterChainDefinitionMap.put("/user/add", "perms[user:add]");
filterChainDefinitionMap.put("/user/*", "authc");

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

bean.setFilterChainDefinitionMap(filterChainDefinitionMap);

return bean;
}

UserRealm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("=======执行了授权=======");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission("user:add");

//拿到当前登录的这个对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();

//User中有属性 perms ,该结果可从数据库中查出
info.addStringPermission(currentUser.getPerms());

return info;
}

完结!!!

狂神,永远滴神!!!

参考

狂神说Java:https://www.bilibili.com/video/BV1NE411i7S8

w3cschool:https://www.w3cschool.cn/shiro/