Spring Security 学习

1.简介

spring中的templates需要请求转发!

1.1 概述

Spring Security 安全框架提供声明式的安全访问控制解决方案,充分利用了IOC,DI,AOP

1.2 常用安全框架

  • Spring Security
  • Apache Shiro

2. UserDetailsService详解

 当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:

返回值

返回值 UserDetails 是一个接口,定义如下

要想返回 UserDetails 的实例就只能返回接口的实现类。SpringSecurity 中提供了如下的实例。对于我们只需要使用里面的 User 类即可。注意 User 的全限定路径是:

org.springframework.security.core.userdetails.User此处经常和系统中自己开发的 User 类弄混。

在 User 类中提供了很多方法和属性。

其中构造方法有两个,调用其中任何一个都可以实例化

UserDetails 实现类 User 类的实例。而三个参数的构造方法实际上也是调用 7 个参数的构造方法。

  • username:用户名

  • password:密码

  • authorities:用户具有的权限。此处不允许为 null

 此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security 会根据 User 中的 password和客户端传递过来的 password 进行比较。如果相同则表示认证通过,如果不相同表示认证失败。

 authorities 里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户具有的权限,如有里面没有包含某个权限,而在做某个事情时必须包含某个权限则会出现 403。通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”) 来创建authorities 集合对象的。参数是一个字符串,多个权限使用逗号分隔。

方法参数

方法参数表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无法接收。

异常

 UsernameNotFoundException 用户名没有发现异常。在loadUserByUsername中是需要通过自己的逻辑从数据库中取值的。如果通过用户名没有查询到对应的数据,应该抛出UsernameNotFoundException,系统就知道用户名没有查询到。

PasswordEncoder 密码解析器详解

 Spring Security 要求容器中必须有PasswordEncoder实例。所以当自定义登录逻辑时要求必须给容器注入PaswordEncoder的bean对象。

接口介绍

  • encode():把参数按照特定的解析规则进行解析。

  • matches() :验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。

  • upgradeEncoding():如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false。

内置解析器介绍

在 Spring Security 中内置了很多解析器。

BCryptPasswordEncoder 简介

 BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。

 BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认 10.

3.自定义登录逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* spring security的密码加密和验证 PasswordEncoder
* @author lorin
* @date 2021/11/24 19:51
*/
@Test
public void testPwd() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("lorin");
System.out.println("encode = " + encode);
boolean matches = passwordEncoder.matches("lorin", encode);
System.out.println("matches = " + matches);

}

2.2 比较用户密码和加载信息

1
2
3
4
5
6
7
8
9
10
11
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1.根据username查询数据库
if (!"admin".equals(username)) {
throw new UsernameNotFoundException("用户名或者密码错误");
}
//2.根据查询的对象比较密码
String password = passwordEncoder.encode("123456");
//3.返回用户对象
return new User("admin", password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}

4.自定义登录界面

SecurityConfig

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
/**
* 自定义登录界面
*
* @param http
* @author lorin
* @date 2021/11/24 21:17
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登录界面
http.formLogin().loginPage("/login.html")
// 自定义登录逻辑 登陆访问路径:提交表单之后跳转的地址
.loginProcessingUrl("/login")
// 登录成功界面,是post请求
.successForwardUrl("/toMain")
// 登录失败
.failureForwardUrl("/toFail")
// 配置post账号密码参数
.usernameParameter("username123")
.passwordParameter("pwd");


//授权
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
.antMatchers("/error.html").permitAll()
//所有的请求都必须被认证
.anyRequest().authenticated();
//关闭csrf
http.csrf().disable();
}

controller

1
2
3
4
5
6
7
8
@PostMapping("/toMain")
public String login() {
return "redirect:main.html";
}
@PostMapping("/toFail")
public String fail() {
return "redirect:error.html";
}

5.自定义登录成功页面

MyAuthenticationSuccessHandler

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

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

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

/**
* 自定义登录成功
*
* @author lorin
* @date 2021/11/24 21:51
*/


public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String url;

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


@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//Principal 主体,存放了登录用户的信息
User user = (User) authentication.getPrincipal();
//用户名
System.out.println(user.getUsername());
//输出null
System.out.println(user.getPassword());
//权限
System.out.println(user.getAuthorities());
System.out.println(user);
//详情 请求ip:sessionID
System.out.println(authentication.getDetails());
//凭证
System.out.println(authentication.getCredentials());
response.sendRedirect(url);
}
}

config

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
/**
* 自定义跳转页面
*
* @param http
* @author lorin
* @date 2021/11/24 21:17
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登录界面
http.formLogin().loginPage("/login.html")
// 自定义登录逻辑
.loginProcessingUrl("/login")
// 登录成功界面,是post请求
// .successForwardUrl("/toMain")
.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
// 登录失败
.failureForwardUrl("/toFail");
// 配置post账号密码参数



//授权
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
.antMatchers("/error.html").permitAll()
//所有的请求都必须被认证
.anyRequest().authenticated();
//关闭csrf
http.csrf().disable();
}

6.自定义失败

MyAuthenticationFailureHandler

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

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;

/**
* TODO
*
* @author lorin
* @date 2021/11/24 22:05
*/


public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final 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);
}
}

config

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

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;

/**
* SpringSecurity 配置类
*
* @author lorin
* @date 2021/11/24 19:59
*/

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义登录界面
*
* @param http
* @author lorin
* @date 2021/11/24 21:17
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登录界面
http.formLogin().loginPage("/login.html")
// 自定义登录逻辑
.loginProcessingUrl("/login")
// 登录成功界面,是post请求
// .successForwardUrl("/toMain")
.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
// 登录失败
//.failureForwardUrl("/toFail");
.failureHandler(new MyAuthenticationFailureHandler("/error.html"));



//授权
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
.antMatchers("/error.html").permitAll()
//所有的请求都必须被认证
.anyRequest().authenticated();
//关闭csrf
http.csrf().disable();
}

/**
* PasswordEncoder配置
*
* @return org.springframework.security.crypto.password.PasswordEncoder
* @author lorin
* @date 2021/11/24 20:01
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

}

7.授权

访问控制url匹配

http.authorizeRequests()主要是对url进行控制,也就是我们所说的授权(访问控制)。http.authorizeRequests()也支持连缀写法,总体公式为:

  • url 匹配规则.权限控制方法

 通过上面的公式可以有很多 url 匹配规则和很多权限控制方法。这些内容进行各种组合就形成了Spring Security中的授权。

 在所有匹配规则中取所有规则的交集。配置顺序影响了之后授权效果,越是具体的应该放在前面,越是笼统的应该放到后面。

7.1 anyRequest()

表示匹配所有的请求

1
.anyRequest().authenticated();  

7.2 antMatcher()

方法定义如下

1
public C antMatchers(String... antPatterns)  

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

规则如下:

  • ?: 匹配一个字符

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

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

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

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

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

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

7.3 regexMatchers()

同上使用正则

介绍

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

 演示所有以.js 结尾的文件都被放行。

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

两个参数时使用方式

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

枚举类型 HttpMethod 内置属性如下:

7.4mvcMatchers()

访问控制url匹配

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

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

1
spring.mvc.servlet.path=/yjxxt

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

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

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

1
.antMatchers("/yjxxt/demo").permitAll()

内置访问控制

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

7.1 permitAll()

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

7.2 authenticated()

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

7.3 anonymous()

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

7.4 denyAll()

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

7.5 rememberMe()

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

7.6 fullyAuthenticated()

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

角色权限判断

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

7.1 hasAuthority(String)

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

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

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

7.2 hasAnyAuthority(String …)

如果用户具备给定权限中某一个,就允许访问。

下面代码中由于大小写和用户的权限不相同,所以用户无权访问

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

7.3 hasRole(String)

如果用户具备给定角色就允许访问。否则出现 403。

参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授权。

 在给用户赋予角色时角色需要以:ROLE_开头,后面添加角色名称。例如:ROLE_abc 其中 abc 是角色名,ROLE_是固定的字符开头。

使用 hasRole()时参数也只写 abc 即可。否则启动报错。

给用户赋予角色:

 在配置类中直接写 abc 即可。

1
.antMatchers("/main1.html").hasRole("abc")

7.4 hasAnyRole(String …)

如果用户具备给定角色的任意一个,就允许被访问

7.5 hasIpAddress(String)

如果请求是指定的 IP 就运行访问。

可以通过 request.getRemoteAddr()获取 ip 地址。

需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。

当浏览器中通过 localhost 进行访问时控制台打印的内容:

当浏览器中通过 127.0.0.1 访问时控制台打印的内容:

当浏览器中通过具体 ip 进行访问时控制台打印内容:

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

8.自定义403处理方案

新建类

新建类实现 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
28
29
30
31
32
package com.lorin.config;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

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

/**
* TODO
*
* @author lorin
* @date 2021/11/24 23:02
*/

//可以去配置文件new,也可以交给spring管理
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
//异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);

9.基于表达式的访问控制

access()方法使用

之前学习的登录用户权限判断实际上底层实现都是调用access(表达式)image-20211124233106490

以 hasRole 和 和 permitAll 举例

9.1自定义方法实现

MyService.java

1
2
3
public interface MyService {
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}

MyServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}

修改配置类

在 access 中通过@bean的id名.方法(参数)的形式进行调用配置类中修改如下:

1
2
3
4
5
6
7
8
//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)")

10.基于注解实现

在 Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过@EnableGlobalMethodSecurity 进行开启后使用。

如果设置的条件允许,程序正常执行。如果不允许会报 500

这些注解可以写到 Service 接口或方法上,也可以写到 Controller或 Controller 的方法上。通常情况下都是写在控制器方法上的,控制接口URL是否允许被访问。

10.1@Secured

@Secured 是专门用于判断是否具有角色的。能写在方法或类上。参数要以 ROLE_开头。

开启注解

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

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SpringsecurityDemoApplication {

public static void main(String[] args) {
SpringApplication.run(SpringsecurityDemoApplication.class, args);
}

}

在控制器方法上添加@Secured 注解

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
8
9
10
@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
10
/**
* 成功后跳转页面
* @return
*/
//ROLE_可有可无
@PreAuthorize("hasRole('ROLE_abc')")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}

11.退出登录

用户只需要向 Spring Security 项目中发送/logout 退出请求即可。

退出登录

实现退出非常简单,只要在页面中添加/logout 的超链接即可。

1
<a href="/logout">退出登录</a>

为了实现更好的效果,通常添加退出的配置。默认的退出 url 为/logout,退出成功后跳转到/login?logout

如果不希望使用默认值,可以通过下面的方法进行修改。

1
2
3
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html");

12.Oauth2认证