VioletEverGarden
文章16
标签10
分类1
SpringSecurity的使用

SpringSecurity的使用

springsecurity的一些常用方法和在springboot中整合

学习目标

image-20210906203243896

SpringSecruity简介

安全框架概述

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

常用安全框架

  • Spring Security:Spring家族一员。是一个能够为基于Spring的企业应用系统提供声明式的安全访 问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了 Spring IoC , DI(控制反转Inversion of Control,DI:Dependency Injection 依赖注入) 和 AOP(面向切面编程) 功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安 全控制编写大量重复代码的工作。
  • Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。 Spring Security简介 概述 Spring Security是一个高度自定义的安全框架。利用 Spring IoC/DI和AOP功能,为系统提供了声明 式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用 Spring Secruity 的原因有 很多,但大部分都是发现了 javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同 时认识到他们在 WAR 或 EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你 的应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。 正如你可能知道的两个应用程序的两个主要区域是“认证”和“授权”(或者访问控制)。这两点也是 Spring Security 重要核心功能。“认证”,是建立一个他声明的主体的过程(一个“主体”一般是指用户, 设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。 “授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否 有权限去做某些事情。

概述

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

其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式。

快速入门

导入依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>

编写前端页面login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username" /><br/>
密码:<input type="password" name="password" /><br/>
<input type="submit" value="登录" />
</form>
</body>
</html>

访问页面

导入spring-boot-starter-security 启动器后,Spring Security 已经生效,默认拦截全部请求,如果用 户没有登录,跳转到内置登录页面。

image-20210906213839543

默认的用户名为user,密码在控制台中image-20210906213927194

登陆后即可访问login.html和main.html

UserDetailsService详解

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

1
2
3
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

返回值

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

1
2
3
4
5
6
7
8
9
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();//获取所有权限
String getPassword();//获取密码
String getUsername();//获取用户名
boolean isAccountNonExpired();//账号是否过期
boolean isAccountNonLocked();//账号是否被锁定
boolean isCredentialsNonExpired();//凭证是否过期
boolean isEnabled();//是否可用
}

要想返回 UserDetails 的实例就只能返回接口的实现类。SpringSecurity 中提供了如下的实例。我们只需要使用里面的 User 类即可。

注意 User 的全限定路径是: org.springframework.security.core.userdetails.User此处经常和系统中自己开发的 User 类弄混。

在 User 类中提供了很多方法和属性。 其中构造方法有两个,调用其中任何一个都可以实例化

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

  • username :用户名
  • password :密码
  • authorities :用户具有的权限。此处不允许为 null
1
2
3
4
public User(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}

此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface PasswordEncoder {

/**
* 编码原始密码。通常,良好的编码算法将SHA-1或更大的哈希值与8字节或更大的随机生成的盐结合使用
*/
String encode(CharSequence rawPassword);

/**
* 验证从存储中获取的编码密码,也对提交的原始密码进行编码。如果密码匹配,则返回true;否则,返回false。存储的 * 密码本身不会被解码。
*/
boolean matches(CharSequence rawPassword, String encodedPassword);

/**
* 如果需要再次对编码后的密码进行编码以提高安全性,则返回true,否则返回false。默认实现始终返回false
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}

}

内置解析器介绍

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

image-20210907105002445

BCryptPasswordEncoder 简介

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

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

Bcrypt 有两个特点:

  • 每一次 HASH 出来的值不一样
  • 计算非常缓慢

因此使用 Bcrypt 进行加密后,攻击者破解密码成本变得不可接受,但代价是应用自身也会性能受到影响,不过登录行为并不是随时在发生,因此能够忍受。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
public class SpringsecurityDemoApplicationTests {

@Test
public void contextLoads() {
PasswordEncoder pw = new BCryptPasswordEncoder();
//加密
String encode = pw.encode("123");
System.out.println(encode);
//比较密码
boolean matches = pw.matches("123", encode);
System.out.println("=========================");
System.out.println(matches);

}

}

需要注意的是,Spring Security要求:当进行自定义登录逻辑时容器内必须有PasswordEncoder实例。所以需要编写一个配置类,将密码解析器先注入进去:

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

自定义登录

当进行自定义登录逻辑时需要用到之前讲解的UserDetailsService和PasswordEncoder 。

自定义逻辑

在 Spring Security 中实现 UserDetailService 就表示为用户详情服务。在这个类中编写用户认证逻辑。

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
package com.springsecurity.demo.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

/**
* @author zhnag
* @since 1.0.0
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {

@Autowired
private PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("执行自定义登录逻辑");

//1.根据用户名去数据库查询,如果不存在抛UsernameNotFoundException异常
if (!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在");
}
//2.比较密码(注册时已经加密过),如果匹配成功返回UserDetails
String password = passwordEncoder.encode("123");
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}

在实际操作中我们是通过查询数据库获取用户名和密码

本文简单说明即把admin和123当作是数据库查询出的密码

自定义登录页面

虽然 Spring Security 给我们提供了登录页面,但是对于实际项目中,大多喜欢使用自己的登录页 面。所以 Spring Security 中不仅仅提供了登录页面,还支持用户自定义登录页面。实现过程也比较简 单,只需要修改配置类即可。

编写登录页面

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>Title</title>
</head>
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username123"/><br/>
密码:<input type="password" name="password123"/><br/>
记住我:<input type="checkbox" name="remember-me" value="true"/><br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>

修改配置类

修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapte,并重 写 configure 方法。

  • successForwardUrl() :登录成功后跳转地址
  • loginPage() :登录页面
  • loginProcessingUrl :登录页面表单提交地址,此地址可以不真实存在。
  • antMatchers() :匹配内容
  • permitAll() :允许
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单提交
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
.successForwardUrl("/toMain");
http.authorizeRequests()
//login.html不需要被认证
.antMatchers("/login.html").permitAll()
//所有请求都必须被认证,必须登录后被访问
.anyRequest().authenticated();
//关闭csrf防护
http.csrf().disable();
}

编写控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class LoginController {
//该方法不执行
// @RequestMapping("/login")
// public String login(){
// System.out.println("登录方法");
// return "main.html";
// }
/**
* 成功后跳转页面
* @return
*/
@RequestMapping("/toMain")
public String toMain(){
return "redirect:/main.html";
}
}

认证过程其他常用配置

失败跳转

表单处理中成功会跳转到一个地址,失败也可以跳转到一个地址

编写页面error.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
操作失败,请重新登录 <a href= "/login.html">跳转</a>
</body>
</html>

修改表单配置

在配置方法中表单认证部分添加 failureForwardUrl() 方法,表示登录失败跳转的 url。此处依然是POST 请求,所以跳转到可以接收 POST请求的控制器/error中。

1
2
3
4
5
6
7
8
9
10
//表单提交
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
.successForwardUrl("/toMain")
//登录失败后跳转页面,POST请求
.failureForwardUrl("/toError");

添加控制器的方法

在控制器类中添加控制器方法,方法映射路径/error。此处要注意:由于是 POST 请求访问/error。所以如果返回值直接转发到 error.html 中,即使有效果,控制台也会报警告,提示 error.html 不支持POST 访问方式。

设置请求账户和密码的参数名

源码简介

当进行登录时会执行 UsernamePasswordAuthenticationFilter 过滤器。

  • usernamePasrameter :账户参数名
  • passwordParameter :密码参数名
  • postOnly=true :默认情况下只允许POST请求。

image-20210907163716883

所以我们可以设置参数来改变默认的名字

1
2
3
4
5
6
7
8
9
10
11
12
//表单提交
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
.successForwardUrl("/toMain")
//登录失败后跳转页面,POST请求
.failureForwardUrl("/toError")
.usernameParameter("myusername")
.passwordParameter("mypassword");

自定义登录成功处理器

源码分析

使用successForwardUrl()时表示成功后转发请求到地址。内部是通过 successHandler() 方法进行 控制成功后交给哪个类进行处理

image-20210907164437949

image-20210907164555496

ForwardAuthenticationSuccessHandler内部就是最简单的请求转发。由于是请求转发,当遇到需要 跳转到站外或在前后端分离的项目中就无法使用了。

当需要控制登录成功后去做一些事情时,可以进行自定义认证成功控制器。

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.springsecurity.demo.handler;

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;


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);
}
}

修改配置项

使用 successHandler()方法设置成功后交给哪个对象进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
http.formLogin()
//自定义登录页面
.loginPage("/login.html")
//当发现/login时认为是登录,必须和表单提交的地址一样。去执行UserServiceImpl
.loginProcessingUrl("/login")
//登录成功后跳转页面,POST请求
// .successForwardUrl("/toMain")
//和successForwardUrl不能共存
.successHandler(new
MyAuthenticationSuccessHandler("http://www.baidu.com"))
//登录失败后跳转页面,POST请求
.failureForwardUrl("/toError")
.usernameParameter("myusername")
.passwordParameter("mypassword");

自定义失败处理器也是一样的编写一个failureHandler()

同样不能和failureForwardUrl共存

访问控制url匹配

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

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

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

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

anyRequest()

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

1
.anyRequest().authenticated();  

antMatcher()

方法定义如下

1
public C antMatchers(String... antPatterns)  

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

规则如下:

  • ? : 匹配一个字符
  • *:匹配 0 个或多个字符
  • ** :匹配 0 个或多个目录 *

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

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

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

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

regexMatchers()

介绍

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

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

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

两个参数时使用方式

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

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

image-20210907170931516

mvcMatchers()

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

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

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

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

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

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

内置访问控制方法

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

  • permitAll() 表示所匹配的 URL 任何人都允许访问。
  • authenticated() 表示所匹配的 URL 都需要被认证才能访问。
  • anonymous() 表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url会执行 filter 链中
  • denyAll() 表示所匹配的 URL 都不允许被访问。
  • rememberMe() 被“remember me”的用户允许访问
  • fullyAuthenticated() 如果用户不是被 remember me 的,才可以访问。

角色权限判断

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

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

    即在userdetails中设置

    1
    return newUser(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList("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 即可。否则启动报错。

    给用户赋予角色:

    1
    return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc"));

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

    1
    .antMatchers("/main1.html").hasRole("abc")
  • hasAnyRole(String …) 如果用户具备给定角色的任意一个,就允许被访问

  • hasIpAddress(String)

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

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

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

    localhost打印出的是:0:0:0:0:0:0:0:1

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

基于表达式的访问控制

access()方法使用

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

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

以 hasRole 和 和 permitAll 举例

1
2
.antMatchers("/showLogin").access("permitAll")
.antMatchers("/main1.html").access("hasRole('abc')")

使用自定义方法

虽然这里面已经包含了很多的表达式(方法)但是在实际项目中很有可能出现需要自己自定义逻辑的情况。

判断登录用户是否具有访问当前 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
28
29
30
31
32
33
package com.springsecurity.demo.service;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.util.Collection;

/**
* @author zhoubin
* @since 1.0.0
*/
@Service
public class MyServiceImpl implements MyService {

@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//获取主体
Object obj = authentication.getPrincipal();
//判断主体是否属于UserDetails
if (obj instanceof UserDetails){
//获取权限
UserDetails userDetails = (UserDetails) obj;
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
//判断请求的URI是否在权限里
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}

修改配置类

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

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

基于注解的访问控制

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

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

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

开启注解

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

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

@Secured

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

@PreAuthorize/@PostAuthorize

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

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

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

RememberMe功能实现

Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问。

添加依赖

Spring Security 实 现 Remember Me 功 能 时 底 层 实 现 依 赖Spring-JDBC,所以需要导入 SpringJDBC。以后多使用 MyBatis 框架而很少直接导入 spring-jdbc,所以此处导入 mybatis 启动器同时还需 要添加 MySQL 驱动。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- mysql 数据库依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>

配置数据源

在 application.properties 中配置数据源。请确保数据库中已经存在shop数据库

1
2
3
4
spring.datasource.driver-class-name= com.mysql.cj.jdbc.Driver
spring.datasource.url= jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username= root
spring.datasource.password= root

编写配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class RememberMeConfig {
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new
JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//自动建表,第一次启动时需要,第二次启动时注释掉
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
1
2
3
4
5
6
7
8
9
//记住我
http.rememberMe()
//设置数据源
.tokenRepository(persistentTokenRepository)
// .rememberMeParameter()
//超时时间
.tokenValiditySeconds(60)
//自定义登录逻辑
.userDetailsService(userDetailService);

有效时间

默认情况下重启项目后登录状态失效了。但是可以通过设置状态有效时间,即使项目重新启动下次也可以正常登录。

退出登录

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

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

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

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

image-20210908190551312

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

1
2
3
4
5
//退出
http.logout()
.logoutUrl("/logout")
//退出成功后跳转的页面
.logoutSuccessUrl("/login.html");

logout其他常用配置源码解读

clearAuthentication(boolean)

是否清除认证状态,默认为 true

image-20210908191430170

invalidateHttpSession(boolean)

是否销毁 HttpSession 对象,默认为 true

具体看上图

logoutSuccessHandler(LogoutSuccessHandler)

退出成功处理器

image-20210908191638916

也可以自己进行定义退出成功处理器。只要实现了 LogoutSuccessHandler 接口。与之前讲解的登录 成功处理器和登录失败处理器极其类似。

Author:VioletEverGarden
Link:http://violetevgaden.github.io/posts/98c0a732.html
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可