image-20220212165414250

安全框架:

什么是安全框架? 解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制,非常麻烦。使用安全框架,我们可以通过配置的方式实现对资源的访问限制。

常用安全框架

Spring Security:Spring家族一员。是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决 方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了 Spring IoC , DI(控制 反转Inversion of Control,DI:Dependency Injection 依赖注入) 和 AOP(面向切面编程) 功能,为应用系 统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。

SpringSecurity

概述:

Spring Security是一个高度自定义的安全框架。利用 Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控 制功能,减少了为系统安全而编写大量重复代码的工作。使用 Spring Secruity 的原因有很多,但大部分都是发现了 javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同时认识到他们在 WAR 或 EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用 Spring Security解决了这些问题, 也为你提供许多其他有用的、可定制的安全功能。正如你可能知道的两个应用程序的两个主要区域是“认证”和“授 权”(或者访问控制)。这两点也是 Spring Security 重要核心功能。“认证”,是建立一个他声明的主体的过程(一 个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否 能登录。“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情。

历史:

历史 Spring Security 以“The Acegi Secutity System for Spring”的名字始于2003年年底。其前身为 acegi 项目。起因是 Spring 开发者邮件列表中一个问题,有人提问是否考虑提供一个基于 Spring 的安全实现。限制于时间问题,开发出 了一个简单的安全实现,但是并没有深入研究。几周后,Spring 社区中其他成员同样询问了安全问题,代码提供给 了这些人。2004 年 1 月份已经有 20 人左右使用这个项目。随着更多人的加入,在 2004 年 3 月左右在 sourceforge 中建立了一个项目。在最开始并没有认证模块,所有的认证功能都是依赖容器完成的,而 acegi 则注重授权。但是随 着更多人的使用,基于容器的认证就显现出了不足。acegi 中也加入了认证功能。大约 1 年后 acegi 成为 Spring子项 目。在 2006 年 5 月发布了 acegi 1.0.0 版本。2007 年底 acegi 更名为Spring 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
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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springSecurityConfig</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springSecurityConfig</name>
<description>springSecurityConfig</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

image-20220212152147169

1
2
3
4
5
6
7
8
9
10
    void contextLoads() {
// 对密码进行加密
PasswordEncoder pe = new BCryptPasswordEncoder();
String encode = pe.encode("123");
System.out.println("encode = " + encode);
// 是否进行匹配
boolean matches = pe.matches("123", encode);
System.out.println("matches = " + matches);

}

image-20220212152624055

自定义登录逻辑:

在容器中注入bean

1
2
3
4
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}

实现UserDetailsService的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pw;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

System.out.println("执行了loadUserByUsername方法");

//1.查询数据库判断用户名是否存在,如果不存在就会抛出UsernameNotFoundException异常
if (!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在!");
}
//2.把查询出来的密码(注册时已经加密过)进行解析,或者直接把密码放入构造方法
String password = pw.encode("123");
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc," +
"/main.html,/insert,/delete"));
}
}

自定义登录页面:

使用自身的登录页面:

1
2
3
4
5
6
7
8
9
10
11
12
13

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {

//自定义的表单提交
http.formLogin()
//自定义登录页面,不需要进行认证处理直接进行跳转
.loginPage("/login.html");
}
}

设置请求参数和密码的参数名:

image-20220212161047024

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
//	@Autowired
// private MyAccessDeniedHandler myAccessDeniedHandler;
// @Autowired
// private UserDetailsServiceImpl userDetailsService;


@Override
protected void configure(HttpSecurity http) throws Exception {

//自定义的表单提交
http.formLogin()
.usernameParameter("username123")
.passwordParameter("password123")
//当发现/login时认为是登录,必须和表单提交的地址一样,去执行UserDetailsServiceImpl
.loginProcessingUrl("/login")
//自定义登录页面,不需要进行认证处理接进行跳转
.loginPage("/showLogin")
//登录成功后跳转页面,Post请求
.successForwardUrl("/toMain")
//登录成功后处理器,不能和successForwardUrl共存
// .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
//登录失败后跳转页面,Post请求
.failureForwardUrl("/toError");
//登录失败后处理器,不能和failureForwardUrl共存
// .failureHandler(new MyAuthenticationFailureHandler("/error.html"));

//
//授权认证
http.authorizeRequests()
// //error.html不需要被认证
.antMatchers("/error.html").permitAll()
// .antMatchers("/error.html").access("permitAll()")
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/showLogin").access("permitAll()")
// //所有请求都必须被认证,必须登录之后被访问
.anyRequest().authenticated();
//关闭csrf防护
http.csrf().disable();
}
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

import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* @author zhoubin
* @since 1.0.0
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

private String url;

public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println(request.getRemoteAddr());
User user = (User) authentication.getPrincipal();
System.out.println(user.getUsername());
//输出null
System.out.println(user.getPassword());
System.out.println(user.getAuthorities());
response.sendRedirect(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
package com.example.springsecurityconfig.handle;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* @author zhoubin
* @since 1.0.0
*/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

private String url;

public MyAuthenticationFailureHandler(String url) {
this.url = url;
}

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(url);
}
}

anyRequest详解

访问控制url匹配

在前面讲解了认证中所有常用配置,主要是对 http.formLogin() 进行操作。而在配置类中http.authorizeRequests() 主要是对url进行控制,也就是我们所说的授权(访问控制)。 http.authorizeRequests() 也支持连缀写法,总体公式为:

url 匹配规则权限控制方法 通过上面的公式可以有很多 url 匹配规则和很多权限控制方法。这些内容进行各种组合就形成了Spring Security中 的授权。 在所有匹配规则中取所有规则的交集。配置顺序影响了之后授权效果,越是具体的应该放在前面,越是笼统的应该 放到后面。

anyRequest() 在之前认证过程中我们就已经使用过 anyRequest(),表示匹配所有的请求。一般情况下此方法都会使用,设置全 部内容都需要进行认证。

1
.anyRequest().authenticated();  

antMatchers详解

1
public C antMatchers(String... antPatterns) 

参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。

规则如下:

  • ? : 匹配一个字符

  • *:匹配 0 个或多个字符

  • ** :匹配 0 个或多个目录

在实际项目中经常需要放行所有静态资源,下面演示放行 js 文件夹下所有脚本文件。

1
.antMatchers("/js/**","/css/**").permitAll() 

还有一种配置方式是只要是.js 文件都放行

1
.antMatchers("/**/*.js").permitAll()

regexMathcers详解

介绍

使用正则表达式进行匹配。和 antMatchers() 主要的区别就是参数,

antMatchers() 参数是 ant 表达式,

regexMatchers() 参数是正则表达式

1
2
.regexMatchers( ".+[.]js").permitAll()

两个参数时使用方式

无论是 antMatchers() 还是 regexMatchers() 都具有两个参数的方法,其中第一个参数都是 HttpMethod ,表 示请求方式,当设置了 HttpMethod 后表示只有设定的特定的请求方式才执行对应的权限设置。

mvcMatchers()

mvcMatchers()适用于配置了 servletPath 的情况

servletPath 就是所有的 URL 的统一前缀。在 SpringBoot 整合SpringMVC 的项目中可以在 application.properties 中添加下面内容设置 ServletPath

1
2
spring.mvc.servlet.path=/xxxx

在 Spring Security 的配置类中配置 .servletPath() 是 mvcMatchers()返回值特有的方法,antMatchers()和
regexMatchers()没有这个方法。在 servletPath() 中配置了 servletPath 后,mvcMatchers()直接写 SpringMVC
中@RequestMapping()中设置的路径即可。

1
.mvcMatchers("/demo").servletPath("/xxxx").permitAll()

如果不习惯使用 mvcMatchers()也可以使用 antMatchers(),下面代码和上面代码是等效

1
.antMatchers("/xxxx/demo").permitAll()
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package com.xxxx.springsecuritydemo.config;

import com.xxxx.springsecuritydemo.handle.MyAccessDeniedHandler;
import com.xxxx.springsecuritydemo.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
* SpringSecurity配置类
*
* @author zhoubin
* @since 1.0.0
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private DataSource dataSource;
@Autowired
private PersistentTokenRepository persistentTokenRepository;

@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交
http.formLogin()
.passwordParameter("password123")
//当发现/login时认为是登录,必须和表单提交的地址一样,去执行UserDetailsServiceImpl
.loginProcessingUrl("/login")
//自定义登录页面
.loginPage("/showLogin")
//登录成功后跳转页面,Post请求
.successForwardUrl("/toMain")
//登录成功后处理器,不能和successForwardUrl共存,能够进行处理
// .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
//登录失败后跳转页面,Post请求
.failureForwardUrl("/toError");
//登录失败后处理器,不能和failureForwardUrl共存
// .failureHandler(new MyAuthenticationFailureHandler("/error.html"));


//授权认证
http.authorizeRequests()
//error.html不需要被认证
// .antMatchers("/error.html").permitAll()
.antMatchers("/error.html").access("permitAll()")
//login.html不需要被认证
// .antMatchers("/login.html").permitAll()
.antMatchers("/showLogin").access("permitAll()")
.antMatchers("/js/**","/css/**","/images/**").permitAll()
// .antMatchers("/**/*.png").permitAll()
//正则表达式匹配
.regexMatchers(".+[.]png").permitAll()
// 需要指定请求的路径
// .regexMatchers(HttpMethod.GET,"/demo").permitAll()
//mvc匹配servletPath为特有方法,其他2种匹配方式没有
// .mvcMatchers("/demo").servletPath("/xxxx").permitAll()
//和mvc匹配等效
// .antMatchers("/xxxx/demo").permitAll()
//权限判断
// .antMatchers("/main1.html").hasAuthority("admiN")
// .antMatchers("/main1.html").hasAnyAuthority("admin","admiN")
//角色判断
// .antMatchers("/main1.html").hasRole("abC")
// .antMatchers("/main1.html").access("hasRole('abc')")
// .antMatchers("/main1.html").hasAnyRole("abC,abc")
//IP地址判断
// .antMatchers("/main1.html").hasIpAddress("127.0.0.1")
//所有请求都必须被认证,必须登录之后被访问
.anyRequest().authenticated();
//access自定义方法
// .anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");

//关闭csrf防护
// http.csrf().disable();

//异常处理
// http.exceptionHandling()
// .accessDeniedHandler(myAccessDeniedHandler);

//记住我
http.rememberMe()
//失效时间,单位秒
.tokenValiditySeconds(60)
// .rememberMeParameter()
//自定义登录逻辑
.userDetailsService(userDetailsService)
//持久层对象
.tokenRepository(persistentTokenRepository);

//退出登录
http.logout()
.logoutUrl("/logout")
//退出登录跳转页面
.logoutSuccessUrl("/login.html");

}

@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}


@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动建表,第一次启动时候需要,第二次启动注释掉
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}

}

内置访问控制方法

Spring Security 匹配了 URL 后调用了 permitAll() 表示不需要认证,随意访问。在 Spring Security 中提供了多 种内置控制。

permitAll()

表示所匹配的 URL 任何人都允许访问。

authenticated()

authenticated()表示所匹配的 URL 都需要被认证才能访问。

anonymous()

表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter 链中

denyAll()

denyAll()表示所匹配的 URL 都不允许被访问。

rememberMe()

被“remember me”的用户允许访问

fullyAuthenticated()

如果用户不是被 remember me 的,才可以访问。

角色权限判断

除了之前讲解的内置权限控制。Spring Security 中还支持很多其他权限控制。这些方法一般都用于用户已经被认 证后,判断用户是否具有特定的要求。

hasAuthority(String)

判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。下图中 admin和 normal 就是用户的权限。admin和normal 严格区分大小写。

在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问

1
.antMatchers("/main1.html").hasAuthority("admin")

hasAnyAuthority(String …)

如果用户具备给定权限中某一个,就允许访问。 下面代码中由于大小写和用户的权限不相同,所以用户无权访问

1
.antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")

hasRole(String)

如果用户具备给定角色就允许访问。否则出现 403。 参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授权。 在给用户赋予角色时角色需要以: ROLE_开头 ,后面添加角色名称。例如:ROLE_abc 其中 abc 是角色名,ROLE_ 是固定的字符开头。 使用 hasRole()时参数也只写 abc 即可。否则启动报错。 给用户赋予角色:

在配置类中直接写 abc 即可

1
2
.antMatchers("/main1.html").hasRole("abc")
智者乐山仁者乐水●程序员乐字节

hasIpAddress(String)

如果请求是指定的 IP 就运行访问。 可以通过 request.getRemoteAddr() 获取 ip 地址。 需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。 当浏览器中通过 localhost 进行访问时控制台打印的内容

1
2
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")

自定义403处理方案

使用 Spring Security 时经常会看见 403(无权限),默认情况下显示的效果如下:

image-20220212194953371

而在实际项目中可能都是一个异步请求,显示上述效果对于用户就不是特别友好了。Spring Security 支持自定义权 限受限

新建类实现

AccessDeniedHandler

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
package com.xxxx.springsecuritydemo.handler;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author zhoubin
* @since 1.0.0
*/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader("Content-Type", "application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
out.flush();
out.close();
}
}

修改配置类

配置类中重点添加异常处理器。设置访问受限后交给哪个对象进行处理。 myAccessDeniedHandler 是在配置类中进行自动注入的。

1
2
3
修改配置类
配置类中重点添加异常处理器。设置访问受限后交给哪个对象进行处理。
myAccessDeniedHandler 是在配置类中进行自动注入的。

基于表达式的访问控制 access()方法使用

之前学习的登录用户权限判断实际上底层实现都是调用access(表达式) 可以通过 access() 实现和之前学习的权限控制完成相同的功能。 以 hasRole 和 和 permitAll 举例

可以通过 access() 实现和之前学习的权限控制完成相同的功能。 以 hasRole 和 和 permitAll 举例

2、 使用自定义方法 虽然这里面已经包含了很多的表达式(方法)但是在实际项目中很有可能出现需要自己自定义逻辑的情况。 判断登录用户是否具有访问当前 URL 权限

1
2
3
4
5
6
7
package com.xxxx.springsecuritydemo.service; 
import org.springframework.security.core.Authentication; import javax.servlet.http.HttpServletRequest;
public interface MyService {
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}


1
//url拦截 http.authorizeRequests()    //login.html不需要被认证    // .antMatchers("/login.html").permitAll()   .antMatchers("/login.html").access("permitAll")    // .antMatchers("/main.html").hasRole("abc")   .antMatchers("/main.html").access("hasRole('abc')")   .anyRequest().access("@myServiceImpl.hasPermission(request,authentication)") 智者
1
2
3
4
5
6
/** * @author zhoubin * @since 1.0.0 */ @Component public class MyServiceImpl implements MyService {  @Override  public boolean hasPermission(HttpServletRequest request, Authentication authentication)  {    
Object obj = authentication.getPrincipal();
if (obj instanceof UserDetails){
UserDetails userDetails = (UserDetails) obj;
Collection authorities = userDetails.getAuthorities();
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI())); } return false;

基于注解的访问控制 在 Spring Security 中提供了一些访问控制的注解。

这些注解都是默认是都不可用的,需要通过 @EnableGlobalMethodSecurity 进行开启后使用。 如果设置的条件允许,程序正常执行。如果不允许会报 500 这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。通常情况下都是写在控制 器方法上的,控制接口URL是否允许被访问。

@Secured

在 启 动 类 ( 也 可 以 在 配 置 类 等 能 够 扫 描 的 类 上 ) 上 添 加 @EnableGlobalMethodSecurity(securedEnabled = true)

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityDemoApplication.class, args);
}
}

1
2
3
4
5
6
7
8
9
/**
* 成功后跳转页面
* @return
*/
@Secured("ROLE_abc")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}

配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
.successForwardUrl("/toMain")

//url拦截
http.authorizeRequests()
//login.html不需要被认证
.antMatchers("/login.html").permitAll()
//所有请求都必须被认证,必须登录后被访问
.anyRequest().authenticated();
//关闭csrf防护
http.csrf().disable();
}

@PreAuthorize/@PostAuthorize

@PreAuthorize 和@PostAuthorize 都是方法或类级别注解。

@PreAuthorize 表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和 access()方法参数取值相同,都是权限表达式。

@PostAuthorize 表示方法或类执行结束后判断权限,此注解很少被使用到。

1
2
3
4
5
6
7
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringsecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecurityDemoApplication.class, args);
}
}

添加@PreAuthorize 在控制器方法上添加@PreAuthorize,参数可以是任何 access()支持的表达式

1
2
3
4
5
6
7
8
9
/**
* 成功后跳转页面
* @return
*/
@PreAuthorize("hasRole('ROLE_abc')")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}

Thymeleaf中SpringSecurity的使用

Spring Security 可以在一些视图技术中进行控制显示效果。例如: JSP 或 Thymeleaf 。在非前后端分离且使用 Spring Boot 的项目中多使用 Thymeleaf 作为视图展示技术。 Thymeleaf 对 Spring Security 的 支 持 都 放 在 thymeleaf-extras-springsecurityX 中,目前最新版本为 5。所 以需要在项目中添加此 jar 包的依赖和 thymeleaf 的依赖。。

1
2
3
4
5
6
7
8
9
10
<!--thymeleaf springsecurity5 依赖-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<!--thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
1
在 html 页面中引入 thymeleaf 命名空间和 security 命名空间
1
2
3
4
5
6
7
8
9
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">

获取属性

可以在html页面中通过 sec:authentication=””

获取 UsernamePasswordAuthenticationToken 中所有 getXXX 的内容,包含父类中的 getXXX 的内容。 根据源码得出下面属性:

  • name :登录账号名称
  • principal :登录主体,在自定义登录逻辑中是
  • UserDetails credentials :凭证
  • authorities :权限和角色
  • details :实际上是 WebAuthenticationDetails 的实例。可以获取 remoteAddress (客户端 ip)和 sessionId (当前 sessionId

在项目 resources 中新建 templates 文件夹,在 templates 中新建demo.html 页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录账号:<span sec:authentication="name"></span><br/>
登录账号:<span sec:authentication="principal.username"></span><br/>
凭证:<span sec:authentication="credentials"></span><br/>
权限和角色:<span sec:authentication="authorities"></span><br/>
客户端地址:<span sec:authentication="details.remoteAddress"></span><br/>
sessionId:<span sec:authentication="details.sessionId"></span><br/>
</body>
</html>

编写Controller thymeleaf 页面需要控制转发,在控制器类中编写下面方法

1
2
3
4
@RequestMapping("/demo")
public String demo(){
return "demo";
}
1
2
3
权限判断
设置用户角色和权限
设定用户具有 admin,/insert,/delete 权限 ROLE_abc 角色
1
2
return new User(username,password, 
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_abc,/insert,/delete"));
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
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录账号:<span sec:authentication="name"></span><br/>
登录账号:<span sec:authentication="principal.username"></span><br/>
凭证:<span sec:authentication="credentials"></span><br/>
权限和角色:<span sec:authentication="authorities"></span><br/>
客户端地址:<span sec:authentication="details.remoteAddress"></span><br/>
sessionId:<span sec:authentication="details.sessionId"></span><br/>


<br>
通过权限判断:
<button sec:authorize="hasAuthority('/insert')">新增</button>
<button sec:authorize="hasAuthority('/delete')">删除</button>
<button sec:authorize="hasAuthority('/update')">修改</button>
<button sec:authorize="hasAuthority('/select')">查看</button>
<br/>
通过角色判断:
<button sec:authorize="hasRole('abc')">新增</button>
<button sec:authorize="hasRole('abc')">删除</button>
<button sec:authorize="hasRole('abc')">修改</button>
<button sec:authorize="hasRole('abc')">查看</button>



</body>
</html>

JWT简介:

什么是JWT

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/ 私钥对来签名,防止被篡改。

官网: https://jwt.io/

标准: https://tools.ietf.org/html/rfc7519

JWT令牌的缺点:

优点

  1. jwt基于json,非常方便解析。
  2. 可以在令牌中自定义丰富的内容,易扩展。
  3. 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
  4. 资源服务使用JWT可不依赖认证服务即可完成授权

缺点:

  1. JWT令牌较长,占存储空间比较大。

JWT组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型(即JWT)以及签名所用的算法(如HMAC SHA256或RSA) 等。这也可以被表示成一个JSON对象。

1
{  "alg": "HS256",  "typ": "JWT" }
  • typ :是类型。
  • alg :签名的算法,

这里使用的算法是HS256算法 我们对头部的json字符串进行BASE64编码(网上有很多在线编码的网站),编码后的字符串如下:

Base64 :

Base64 是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder ,用它们可以非常方便的完成基于 BASE64 的编码和解码。

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9  

负载(Payload)

第二部分是负载,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:

  • 标准中注册的声明(建议但不强制使用)
  1. iss: jwt签发者
  2. sub: jwt所面向的用户
  3. aud: 接收jwt的一方
  4. exp: jwt的过期时间,这个过期时间必须要大于签发时间
  5. nbf: 定义在什么时间之前,该jwt都是不可用的.
  6. iat: jwt的签发时间
  7. jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
  • 公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息, 因为该部分在客户端可解密.

  • 私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该 部分信息可以归类为明文信息。 这个指的就是自定义的claim。比如下面那个举例中的name都属于自定的claim。这些claim跟JWT标准规定的 claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道 是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。

1
2
3
4
5
6
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}

其中 sub 是标准的声明, name 是自定义的声明(公共的或私有的) 然后将其进行base64编码,得到Jwt的

1
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbWVzIiwiYWRtaW4iOnRydWV9 

第二部分: 提示:声明中不要放一些敏感信息。

签证、签名(signature)

jwt的第三部分是一个签证信息,这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret(盐,一定要保密)

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

1
2
3
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaW
F0IjoxNTE2MjM5MDIyfQ.8HI-Lod0ncfVDnbKIPJJqLH998duF9DSDGkx3gRPNVI

注意: secret 是保存在服务器端的, jwt 的签发生成也是在服务器端的, secret 就是用来进行 jwt 的签发和 jwt 的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个 secret , 那就意味着 客户端是可以自我签发 jwt 了。

JJWT简介

什么是JJWT JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJW很容易使用 和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

规范官网:

https://jwt.io/

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xxxx</groupId>
<artifactId>jwtdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jwtdemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

创建测试类JwtTest,用于生成token:

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
package com.xxxx.jwtdemo;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.Base64Codec;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
@SpringBootTest
public class JwtdemoApplicationTests {
/**
* 创建token
*/
@Test
public void testCreatToken() {
//创建一个JwtBuilder对象
JwtBuilder jwtBuilder = Jwts.builder()
//声明的标识{"jti":"888"}
.setId("888")
//主体,用户{"sub":"Rose"}
.setSubject("Rose")
//创建日期{"ita":"xxxxxx"}
.setIssuedAt(new Date())
//签名手段,参数1:算法,参数2:盐
.signWith(SignatureAlgorithm.HS256,"xxxx");
//获取jwt的token
String token = jwtBuilder.compact();
System.out.println(token);
//三部分的base64解密
System.out.println("--------------------");
String[] split = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
//无法解密
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
}

image-20220214234736103

token的验证解析

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送 请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息 (例如用户id),根据这些信息查询数据库返回相应的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testParseToken(){
//token
String token =
"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNTc4ODE0MjUyfQ" +
".-FYFMHyfTcGzq900f_Drfdsges0ge2UjaWvPW9gCDto";
//解析token获取负载中的声明对象
Claims claims = Jwts.parser()
.setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
//打印声明的属性
System.out.println("id:"+claims.getId());
System.out.println("subject:"+claims.getSubject());
System.out.println("issuedAt:"+claims.getIssuedAt());
}

token过期校验

有很多时候,我们并不希望签发的token是永久生效的(上节的token是永久的),所以我们可以为token添加一个
过期时间。原因:从服务器发出的token,服务器自己并不做记录,就存在一个弊端就是,服务端无法主动控制某
token的立刻失效。

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
@Test
public void testCreatTokenHasExp() {
//当前系统时间的长整型
long now = System.currentTimeMillis();
//过期时间,这里是1分钟后的时间长整型
long exp = now + 60 * 1000;
//创建一个JwtBuilder对象
JwtBuilder jwtBuilder = Jwts.builder()
//声明的标识{"jti":"888"}
.setId("888")
//主体,用户{"sub":"Rose"}
.setSubject("Rose")
//创建日期{"ita":"xxxxxx"}
.setIssuedAt(new Date())
//签名手段,参数1:算法,参数2:盐
.signWith(SignatureAlgorithm.HS256, "xxxx")
//设置过期时间
.setExpiration(new Date(exp));
//获取jwt的token
String token = jwtBuilder.compact();
System.out.println(token);
}
@Test
public void testParseTokenHasExp() {
//token
String token = "eyJhbGciOiJIUzI1NiJ9" +

".eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNTc4ODE1MDYyLCJleHAiOjE1Nzg4MTUxMjIsInJvbGVzI
joiYWRtaW4iLCJsb2dvIjoic2hzeHQuanBnIn0.hKog0RsZ9_6II_R8kUCp0HLAouUAYXAJVbz3xtLTUh4";
//解析token获取负载中的声明对象
Claims claims = Jwts.parser()
.setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
//打印声明的属性
System.out.println("id:" + claims.getId());
System.out.println("subject:" + claims.getSubject());
System.out.println("issuedAt:" + claims.getIssuedAt());
DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));
System.out.println("过期时间:"+sf.format(claims.getExpiration()));
System.out.println("当前时间:"+sf.format(new Date()));
}

Swagger:

Swagger注释API :@ApiModel - Chen洋 - 博客园 (cnblogs.com)

SpringBoot更新到2.6.0启动报错 Failed to start bean ‘documentationPluginsBootstrapper‘ 问题处理

(36条消息) SpringBoot更新到2.6.0启动报错 Failed to start bean ‘documentationPluginsBootstrapper‘ 问题处理_CHQIUU的专栏-CSDN博客