Spring Security 5.1.4

本文最后更新于:2021年4月14日 晚上

Spring Security


基于 Spring Security 5.1.4.RELEASE

1. 基本概念

1.1 认证

  • 判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。

1.2 会话

认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前
用户的登录状态所提供的机制。

  • session方式

认证成功后,服务器生成用户相关数据保存在session(当前会话)中,发给客户端 session_id,客户机存在 cookie 中,下次客户端请求会带上 session_id ,服务器可以验证 session 数据,检验合法性,当用户退出系统或 session 过期,session_id 就无效。

  • token方式

用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage
等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。

区别:

  • 基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持
    cookie

  • 基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代
    更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。

1.3 授权

用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

区别于认证

  • 认证是为了保证用户身份的合法性

  • 授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,控制不同的用户能够访问不同的资源。

数据模型

授权可简单理解为:主体资源 进行 怎样的操作「权限/许可」,权限是需要与资源进行关联的,否则权限没有意义。

三者的关系图:

  • 不同的权限可以管理不同的资源,用户需要有对应权限才能访问资源。
  • 引入一个 「角色」概念,用户归类于角色,角色拥有相应权限,最后根据权限访问资源
  • 用户【用户id,账号,密码,…】
  • 角色【角色id,角色名称,…】
  • 权限【权限id,权限标识,权限名称,资源id,…】
  • 资源【资源id,资源名称,访问地址,…】
    • 用户 - 角色 【用户id,角色id,…】
    • 角色 - 权限 【角色id,权限id,….】

企业开发将 资源和权限合为一张权限表:

  • 资源【资源id、资源名称、访问地址、…】
  • 权限【权限id、权限标识、权限名称、资源id、…】
    • 合并为:权限【权限id、权限标识、权限名称、资源名称、资源访问地址、…】

1.4 RBAC

1.4.1 基于角色的访问控制

Role-Based Access Control 按照角色进行授权。

1
2
3
if(主体.hasRole("总经理角色id")) {
查询工资
}
  • 如果上图所需角色添加为老板和总经理,此时就要改代码
1
2
3
if(主体.hasRole("总经理角色id") ||  主体.hasRole("老板角色id")) {
    查询工资
}

1.4.2 基于资源的访问控制

Resource-Based Access Control 按照资源(权限)进行授权。

1
2
3
if(主体.hasPermission("查询工资权限标识")) {
    查询工资
}
  • 代码统一,增加可拓展性

2. 基于Session的认证方式「手动实现security」

2.1 环境搭建

基于 SpringMVC 5.1.5.RELEASE, Servlet3.0实现

2.1.1 创建Maven工程

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
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.xxx.security</groupId>
<artifactId>security-springmvc</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- 不添加该依赖tomcat7:run无法运行项目 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
<build>
<finalName>security-springmvc</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<port>8080</port>
<path>/</path>
<uriEncoding>utf-8</uriEncoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
  • Web项目,packaging打包方式为 war
  • 使用tomcat7-maven-plugin插件来运行工程

2.1.2 Spring容器配置

  • config包下定义ApplicationConfig,它对应web.xml中ContextLoaderListener的配置「Servlet3.0不用xml配置」
1
2
3
4
5
6
@Configuration //相当于applicationContext.xml
@ComponentScan(basePackages = "com.xxx.security.springmvc"
,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class ApplicationConfig {
//在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。
}

2.1.3 servletContext配置

  • config包下定义 WebConfig ,对应DispatcherServlet配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration//就相当于springmvc.xml文件
@EnableWebMvc // 接管 MVC
@ComponentScan(basePackages = "com.xxx.security.springmvc"
,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
//视图解析器
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/");
viewResolver.setSuffix(".jsp"); // 用jsp来演示代码
return viewResolver;
}
}

2.1.4 加载 Spring容器

  • 在init包下定义Spring容器初始化类SpringApplicationInitializer,此类实现WebApplicationInitializer接口,Spring容器启动时加载WebApplicationInitializer接口的所有实现类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
//spring容器,相当于加载 applicationContext.xml
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{ApplicationConfig.class};
}

//servletContext,相当于加载springmvc.xml
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}

//url-mapping
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}

SpringApplicationInitializer相当于web.xml,ApplicationConfig.java对应以下配置的application-context.xml,WebConfig.java对应以下配置的spring-mvc.xmlweb.xml的内容参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<web‐app>
    <listener>
        <listener‐class>org.springframework.web.context.ContextLoaderListener</listenerclass>
    </listener>
    <contextparam>
        <paramname>contextConfigLocation</paramname>
        <paramvalue>/WEBINF/applicationcontext.xml</paramvalue>
    </contextparam
    <servlet>
        <servletname>springmvc</servletname>
        <servletclass>org.springframework.web.servlet.DispatcherServlet</servletclass>
        <initparam>
            <paramname>contextConfigLocation</paramname>
            <paramvalue>/WEBINF/springmvc.xml</paramvalue>
        </initparam>
        <loadonstartup>1</loadonstartup>
    </servlet>
    <servletmapping>
        <servletname>springmvc</servletname>
        <urlpattern>/</urlpattern>
    </servletmapping>
</webapp>

2.2 实现认证功能

2.2.1 认证页面

  • webapp/WEB-INF/views下定义认证页面login.jsp,作为测试认证流程,页面为form表单,包括用户名,密码,触发登录将提交表单信息至/login 「controller」。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<%@ page contentType="text/html;charset=UTF‐8" pageEncoding="utf‐8" %>
<html>
<head>
    <title>用户登录</title>
</head>
<body>
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>
    密&nbsp;&nbsp;&nbsp;码:
    <input type="password" name="password"><br>
    <input type="submit" value="登录">
</form>
</body>
</html>
  • 在WebConfig中新增如下配置,将/直接导向login.jsp页面
1
2
3
4
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("login");
}
  • 使用tomcat7插件运行测试,配置如下


2.2.2 认证接口

  1. 定义认证接口,此接口对传来的用户名密码校验,若成功则返回该用户的详细信息,否则抛出错误异常
  • 在service包下创建AuthenticationService接口
1
2
3
4
5
6
7
8
public interface AuthenticationService {
/**
* 用户认证
* @param authenticationRequest 用户认证请求,账号和密码
* @return 认证成功的用户信息
*/
UserDto authentication(AuthenticationRequest authenticationRequest);
}
  • model包下,认证请求结构
1
2
3
4
5
6
@Data // import lombok.Data;
public class AuthenticationRequest {
//认证请求参数,账号、密码
private String username;
private String password;
}
  • model包下,创建UerDto,作为登录成功后获得的登录用户的信息
1
2
3
4
5
6
7
8
9
10
11
@Data
@AllArgsConstructor
public class UserDto {
//用户身份信息
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
private Set<String> authorities; // 用户权限用Set集合存储
}
  1. 认证实现类,根据用户名查找用户信息,并校验密码。
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
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
//匿名代码块,初始化数据,模拟数据库
{
Set<String> authorities1 = new HashSet<>();
authorities1.add("p1");// p1和/r/r1对应,后面controller会用到
Set<String> authorities2 = new HashSet<>();
authorities2.add("p2");// p2和/r/r2对应,后面controller会用到
userMap.put("zhangsan", new UserDto("1010", "zhangsan", "123", "张三", "133443", authorities1));
userMap.put("lisi", new UserDto("1011", "lisi", "456", "李四", "144553", authorities2));
}

/**
* 用户认证,校验用户身份信息是否合法
* @param authenticationRequest 用户认证请求,账号和密码
* @return 认证成功的用户信息
*/
@Override
public UserDto authentication(AuthenticationRequest authenticationRequest) {
//校验参数是否为空
if (authenticationRequest == null
|| StringUtils.isEmpty(authenticationRequest.getUsername())
|| StringUtils.isEmpty(authenticationRequest.getPassword())) {
throw new RuntimeException("账号和密码为空");
}
//根据账号去查询数据库,这里测试程序采用模拟方法
UserDto user = getUserDto(authenticationRequest.getUsername());
//判断用户是否为空
if (user == null) {
throw new RuntimeException("查询不到该用户");
}
//校验密码
if (!authenticationRequest.getPassword().equals(user.getPassword())) {
throw new RuntimeException("账号或密码错误");
}
//认证通过,返回用户身份信息
return user;
}

//根据账号查询用户信息
private UserDto getUserDto(String userName) {
return userMap.get(userName);
}

//用户信息
private Map<String, UserDto> userMap = new HashMap<>();
}
  1. 登录Controller,对/login请求处理,它调用AuthenticationService完成认证并返回登录结果提示信息:
1
2
3
4
5
6
7
8
9
10
11
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;

@RequestMapping(value = "/login", produces = {"text/plain;charset=UTF-8"})
public String login(AuthenticationRequest authenticationRequest, HttpSession session) {
UserDto userDto = authenticationService.authentication(authenticationRequest);
return userDto.getUsername() + "登录成功";
}
}
  1. Test,访问路径为 /

  • 至此,完成最简单的身份凭证校验,后面对于资源的请求还要单独授权。

2.2.3 实现会话功能

  1. 增加会话控制
  • 在UserDto中定义一个SESSION_USER_KEY,作为Session中存放登录用户信息的key
1
public static final String SESSION_USER_KEY = "_user";
  • 修改LoginController,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将session置为
    失效。
1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping(value = "/login", produces = {"text/plain;charset=UTF-8"})
public String login(AuthenticationRequest authenticationRequest, HttpSession session) {
UserDto userDto = authenticationService.authentication(authenticationRequest);
//存入session
session.setAttribute(UserDto.SESSION_USER_KEY, userDto);
return userDto.getUsername() + "登录成功";
}

@GetMapping(value = "/logout", produces = {"text/plain;charset=UTF-8"})
public String logout(HttpSession session) {
session.invalidate(); // 将session非法化就达到退出效果
return "退出成功";
}
  1. 增加测试资源
  • 修改LoginController,增加测试r1,它从当前会话session中获取当前登录用户,并返回提示信息给前台。
1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping(value = "/r/r1", produces = {"text/plain;charset=UTF-8"})
public String r1(HttpSession session) {
String fullname = null;
Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
if (object == null) {
fullname = "匿名";
} else {
UserDto userDto = (UserDto) object;
fullname = userDto.getFullname();
}
return fullname + "访问资源r1";
}
  1. Test
  • 未登录情况下直接访问测试资源/r/r1

  • 成功登录的情况下访问测试资源/r/r1

在用户登录成功时,该用户信息已被成功放入session,并且后续请求可以正常从session中获取当
前登录用户信息,符合预期结果。


2.2.4 实现授权功能

  • 匿名用户(未登录用户)访问拦截:禁止匿名用户访问某些资源。
  • 登录用户访问拦截:根据用户的权限决定是否能访问某些资源。
  1. 增加权限数据
  • UserDto添加用户权限,用Set集合存储
1
private Set<String> authorities;
  • 在AuthenticationServiceImpl中为模拟用户初始化权限,其中张三给了p1权限,李四给了p2权限。
1
2
3
4
5
6
7
8
9
10
11
 	//用户信息
    private Map<String,UserDto> userMap = new HashMap<>();
    {
        Set<String> authorities1 = new HashSet<>();
        authorities1.add("p1");
        Set<String> authorities2 = new HashSet<>();
        authorities2.add("p2");
        userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张
三","133443",authorities1));
        userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
    }
  1. 增加测试资源,同/r/r1,这是资源r2
1
2
3
4
5
6
7
8
9
10
11
@GetMapping(value = "/r/r2", produces = {"text/plain;charset=UTF-8"})
public String r2(HttpSession session) {
String fullname = null;
Object userObj = session.getAttribute(UserDto.SESSION_USER_KEY);
if (userObj != null) {
fullname = ((UserDto) userObj).getFullname();
} else {
fullname = "匿名";
}
return fullname + " 访问资源r2";
}
  1. 实现授权拦截器
  • interceptor包下定义SimpleAuthenticationInterceptor拦截器
  • 实现 1、校验用户是否登录 2、校验用户是否拥有操作权限
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
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在这个方法中校验用户请求的url是否在用户的权限范围内
//取出用户身份信息
Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
if (object == null) {
//没有认证,提示登录
writeContent(response, "请登录");
}
UserDto userDto = (UserDto) object;
//请求的url
String requestURI = request.getRequestURI();
if (userDto.getAuthorities().contains("p1") && requestURI.contains("/r/r1")) {
return true;
}
if (userDto.getAuthorities().contains("p2") && requestURI.contains("/r/r2")) {
return true;
}
writeContent(response, "没有权限,拒绝访问");

return false;
}

//响应信息给客户端
private void writeContent(HttpServletResponse response, String msg) throws IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.print(msg);
writer.close();
}
}
  • 在 WebConfig中配置拦截器,匹配/r/**的资源为受保护的系统资源,访问该资源的请求进入
    SimpleAuthenticationInterceptor拦截器。
1
2
3
4
5
6
7
@Autowired
private SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
   
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/r/**");
}
  1. Test
  • 未登录

  • 张三登录

  • 李四同理,只能访问r2。r1没有权限访问。

3. Spring Security 快速上手「基于MVC」

  • Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在spring boot项目中加入Spring Security更是十分简单,使用Spring Security减少了为企业系统安全控制编写大量重复代码的工作。

3.1 环境搭建

3.1.1 创建Maven工程

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
<groupId>com.itheima.security</groupId>
<artifactId>security-spring-security</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- spring-security依赖 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<!-- 不添加该依赖tomcat7:run无法运行项目 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
<build>
<finalName>security-springmvc</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

3.1.2 Spring容器配置

  • 同security-springmvc
1
2
3
4
5
6
@Configuration
@ComponentScan(basePackages = "com.xxx.security.springmvc"
                ,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class ApplicationConfig {
    //在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。
}

3.1.3 Servlet Context配置

  • 同security-springmvc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.xxx.security.springmvc"
            ,includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value =
Controller.class)})
public class WebConfig implements WebMvcConfigurer {
    //视频解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB‐INF/views/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
 }

3.1.4 加载 Spring容器

  • init包下定义Spring容器初始化类SpringApplicationInitializer,此类实现WebApplicationInitializer接口,Spring容器启动时加载WebApplicationInitializer接口的所有实现类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SpringApplicationInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { ApplicationConfig.class };//指定rootContext的配置类
    }
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class }; //指定servletContext的配置类
    }
    @Override
    protected String[] getServletMappings() {
        return new String [] {"/"};
    }
}

3.2 认证

3.2.1 认证页面

  • Spring Security默认提供认证页面,不需要额外开发。

3.2.2 安全配置

  • Spring Security提供了用户名密码登录退出会话管理等认证功能,只需要配置即可使用。
  1. 在config包下定义WebSecurityConfig,作为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
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息)
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
return manager;
}

//密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}

//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated() // 所有/r/**的请求必须认证通过
.anyRequest().permitAll() // 除了/r/**,其它的请求可以访问
.and()
.formLogin() // 允许表单登录
.successForwardUrl("/login-success"); // 自定义登录成功的页面地址
}
}

需要说明的是:

  • @EnableWebSecurity注解已经包含了 @Configuration
1
2
3
4
5
6
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {...}
  • userDetailsService() 方法中,返回一个UserDetailsService 给 Spring容S器,Spring Security会使用它来
    获取用户信息。暂时使用InMemoryUserDetailsManager实现类,并在其中分别创建了zhangsan、lisi两个用
    户,并设置密码和权限。InMemoryUserDetailsManagerUserDetailsService 接口实现类。

  • 在configure()中,通过HttpSecurity设置了安全拦截规则
    • 1️⃣url匹配/r/**的资源,经过认证后才能访问
    • 2️⃣其他url完全开放
    • 3️⃣支持form表单认证,认证成功后转向/login-success
  1. 加载 WebSecurityConfig
  • 修改SpringApplicationInitializergetRootConfigClasses()方法,添加 WebSecurityConfig.class
1
2
3
4
@Override
protected Class<?>[] getRootConfigClasses() {
    return new Class<?>[] { ApplicationConfig.class, WebSecurityConfig.class};
}

3.2.3 Spring Security初始化

  • Spring Security 初始化,有两种情况

    • 若当前环境没有使用 Spring 或 Spring MVC,则需要将 WebSecurityConfig(Spring Security配置类) 传入超
      类,以确保获取配置,并创建spring context。
    • 相反,若当前环境已经使用 Spring,我们应该在现有的SpringContext中注册Spring Security「上一步已经做将WebSecurityConfig加载至rootcontext」,此方法可以什么都不做。
  • init包下定义SpringSecurityApplicationInitializer

1
2
3
4
5
6
7
8
public class SpringSecurityApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
public SpringSecurityApplicationInitializer() {
//super(WebSecurityConfig.class);
//没有使用Spring或Spring MVC,则需要将WebSecurityConfig(Spring Security配置类) 传入超
//类,以确保获取配置,并创建spring context。
}
}

3.2.4 默认根路径请求

  • 在WebConfig.java中添加默认请求根路径跳转到/login,此url为Spring Security提供:
  • 这里要用重定向到默认的 /login 界面
1
2
3
4
5
// 默认Url根路径跳转到/login,此url为spring security提供
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/").setViewName("redirect:/login");
}

3.2.5 认证成功页面

  • 在安全配置中,认证成功将跳转到/login-success
1
2
3
4
5
6
7
8
9
10
11
12
//安全拦截机制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.antMatchers("/r/**").authenticated() // 所有/r/**的请求必须认证通过
.anyRequest().permitAll() // 除了/r/**,其它的请求可以访问
.and()
.formLogin() // 允许表单登录
.successForwardUrl("/login-success"); // 自定义登录成功的页面地址
}
  • Spring Security支持form表单认证,认证成功后转向/login-success
  • LoginController中定义/login-success
1
2
3
4
@RequestMapping(value = "/login‐success",produces = {"text/plain;charset=UTF‐8"}) 
public String loginSuccess(){
    return " 登录成功";
}

3.2.6 Test

  1. 启动项目,访问http://localhost:8080/security-spring-security/路径地址

  • 页面会根据WebConfig中addViewControllers配置规则,重定向跳转至/login,/login是pring Security提供的登录页面。
  1. 登录
  • 账户名、密码错误

  • 账户名:zhangsan;密码:123

  1. 输入http://localhost:8080/security-spring-security/logout退出,Log Out自动跳转回登录界面


3.3 授权

  • 实现授权需要对用户的访问进行拦截校验,校验用户的权限是否可以操作指定的资源,Spring Security默认提供授权实现方法。

  • LoginController添加/r/r1/r/r2

1
2
3
4
5
6
7
8
9
10
11
// 测试资源1
@GetMapping(value = "/r/r1", produces = {"text/plain;charset=UTF-8"})
public String r1() {
return " 访问资源1";
}

// 测试资源2
@GetMapping(value = "/r/r2", produces = {"text/plain;charset=UTF-8"})
public String r2() {
return " 访问资源2";
}
  • 之前给两个测试用户绑定了相应的权限,表示访问”/xxx”资源的url需要有”px”的权限
1
2
3
4
5
manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build()); 
// protected void configure(HttpSecurity http) ↓↓↓↓
.antMatchers("/r/r1").hasAuthority("p1")                                      
.antMatchers("/r/r2").hasAuthority("p2")
  • Test
  1. 张三登录,访问r1资源

  2. 张三访问r2资源,403无权限

4 Spring Security 基本原理

4.1 Spring Security本质

  • SpringSecurity 本质是一个过滤器链,从启动可以获取到过滤器链:
    • org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil
      ter
    • org.springframework.security.web.context.SecurityContextPersistenceFilter
    • org.springframework.security.web.header.HeaderWriterFilter
    • org.springframework.security.web.csrf.CsrfFilter
    • org.springframework.security.web.authentication.logout.LogoutFilter
    • org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
    • org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
    • org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
    • org.springframework.security.web.savedrequest.RequestCacheAwareFilter
    • org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
    • org.springframework.security.web.authentication.AnonymousAuthenticationFilter
    • org.springframework.security.web.session.SessionManagementFilter
    • org.springframework.security.web.access.ExceptionTranslationFilter
    • org.springframework.security.web.access.intercept.FilterSecurityInterceptor

重点看三个过滤器:

  1. FilterSecurityInterceptor

一个方法级的权限过滤器, 基本位于过滤链的最底部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if (fi.getRequest() != null && fi.getRequest().getAttribute("__spring_security_filterSecurityInterceptor_filterApplied") != null && this.observeOncePerRequest) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} else {
if (fi.getRequest() != null && this.observeOncePerRequest) {
fi.getRequest().setAttribute("__spring_security_filterSecurityInterceptor_filterApplied", Boolean.TRUE);
}
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, (Object)null);
}
}
}
  • super.beforeInvocation(fi) 查看之前的 filter 是否通过
  • fi.getChain().doFilter(fi.getRequest(), fi.getResponse()) 真正的调用后台的服务
  1. ExceptionTranslationFilter
  • 异常过滤器,用来处理在认证授权过程中抛出的异常
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
public class ExceptionTranslationFilter extends GenericFilterBean {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
try {
chain.doFilter(request, response);
this.logger.debug("Chain processed normally");
} catch (IOException var9) {
throw var9;
} catch (Exception var10) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
RuntimeException ase = (AuthenticationException)this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (ase == null) {
ase = (AccessDeniedException)this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (ase == null) {
if (var10 instanceof ServletException) {
throw (ServletException)var10;
}
if (var10 instanceof RuntimeException) {
throw (RuntimeException)var10;
}
throw new RuntimeException(var10);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", var10);
}
this.handleSpringSecurityException(request, response, chain, (RuntimeException)ase);
}
}
}
  1. UsernamePasswordAuthenticationFilter
  • 对/login 的 POST 请求做拦截,校验表单中用户名,密码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}

4.2 过滤器加载

  • 过滤器加载通过 DelegatingFilterProxy 来配置加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DelegatingFilterProxy extends GenericFilterBean {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized(this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = this.findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = this.initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
this.invokeDelegate(delegateToUse, request, response, filterChain);
}
}
  • this.delegate = this.initDelegate(wac) 初始化代理
1
2
3
4
5
6
7
8
9
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = this.getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
if (this.isTargetFilterLifecycle()) {
delegate.init(this.getFilterConfig());
}
return delegate;
}
  • 会内置 FilterChainProxy
1
2
3
4
5
public class FilterChainProxy extends GenericFilterBean {
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) {
List<Filter> filters = this.getFilters((HttpServletRequest)fwRequest);
}
}
  • private List<Filter> getFilters(HttpServletRequest request) 获取所有过滤器链

4.3 两个重要接口

  1. UserDetailsService
  • 当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口。
  • 查询数据库用户名和密码过程写在这个接口中
1
2
3
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) 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(); // 当前用户是否可用
}

  • 实现类为 User,后面只用实现User这个实体类
1
2
3
4
5
6
7
public class User implements UserDetails, CredentialsContainer {
//@Param username: 此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无
//法接收
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
}
  • 需要做的是:

    • 创建类继承 UsernamePasswordAuthenticationFilter ,重写三个方法

      • attemptAuthentication 尝试认证
      • 父类中的 successfulAuthentication unsuccessfulAuthentication 成功和失败认证
    • 创建类实现 UserDetailsService ,编写查询数据过程,返回 User 对象,该User是安全框架提供的。


  1. PasswordEncoder
  • 数据加密接口,用于返回User对象里面密码加密
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 interface PasswordEncoder {
/**
* 表示把参数按照特定的解析规则进行解析
*/
String encode(CharSequence var1);

/**
* 验证从存储中获取的编码密码与编码后提交的原始密码是否匹配
* @param var1 需要被解析的密码
* @param var2 存储的密码
* @return 密码匹配 true, 不匹配 false
*/
boolean matches(CharSequence var1, String var2);

/**
* 升级密码
* @param encodedPassword 编译后的密码
* @return 解析的密码能够再次进行解析且达到更安全的结果 true, 否则返回
* false。默认返回 false
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}

  • BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器,对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10。
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test01() {
// 创建密码解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 对密码进行加密
String testStr = bCryptPasswordEncoder.encode("test");
// 打印加密之后的数据
System.out.println(" 加密之后数据:\t" + testStr);
// 判断原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("test", testStr);
// 打印比较结果
System.out.println(" 比较结果:\t" + result);
}

5 SpringSecurity Web 权限方案

spring-boot-starter-parent 2.2.1.RELEASE; spring-security-web 5.2.1.RELEASE

5.1 设置登录系统的账号、密码

方式一:application.properties

1
2
spring.security.user.name=hypocrite30
spring.security.user.password=hypocrite30

方式二:编写类实现接口

  • config 包下创建 Security 的配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123"); // 对密码进行编译
auth.inMemoryAuthentication().withUser("hypocrite30").password(password).roles("admin");
}

// 注入密码编码器
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}

方式三:自定义实现类

  • 创建配置类,设置使用哪个userDetailsService实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
// 注入密码编码器
@Bean
PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
  • userDetailsService 选择使用哪个 Service进行认证
  • 编写实现类,返回User对象,User对象有用户名密码和权限
1
2
3
4
5
6
7
8
9
10
@Service("userDetailsService") // 与注入的名称一一对应
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("hypocrite30",
new BCryptPasswordEncoder().encode("password"), auths);
}
}

一般都是用第三种方法,自定义实现类。但是涉及到管理员模式,可以使用第一和第二种方式设置admin。

5.2 整合数据库完成用户认证

  • 整合Mybatis-plus
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
  • 创表 users
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int(0) NOT NULL,
`username` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES (1, 'lucy', '123');
INSERT INTO `users` VALUES (2, 'mary', '456');
SET FOREIGN_KEY_CHECKS = 1;
  • 创建实体类,entity包下
1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users {
private Integer id;
private String username;
private String password;
}
  • 整合Mybatis-plus,创建mapper,放在mapper包下
1
2
3
@Repository
public interface UsersMapper extends BaseMapper<Users> {
}
  • 有Mapper,就需要在启动类上添加@MapperScan扫描
1
@MapperScan("com.xxx.security.mapper") // mapper对应的包路径
  • 配置文件加上数据库连接信息
1
2
3
4
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springsecurity?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
  • 在自定义实现类中调用mapper查询数据库用户名认证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//调用usersMapper方法,根据用户名查询数据库
QueryWrapper<Users> wrapper = new QueryWrapper();
// where username=?
wrapper.eq("username", username);
Users users = usersMapper.selectOne(wrapper);
//判断
if (users == null) {//数据库没有用户名,认证失败
throw new UsernameNotFoundException("用户名不存在!");
}
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
//从查询数据库返回users对象,得到用户名和密码,返回
return new User(users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()), auths);
}
}

5.3 自定义登录界面 & 控制是否认证

  • 在Security配置类进行配置,注意重写的是 形参HttpSecurity http 的 configure()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 配置认证
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 自定义自己编写的登录页面
.loginPage("/login.html") // 配置哪个 url 为登录页面
.loginProcessingUrl("/user/login") // 登录访问路径 url
.successForwardUrl("/success") // 登录成功之后,跳转路径 url
.failureForwardUrl("/fail");// 登录失败之后跳转到哪个 url
http.authorizeRequests()
.antMatchers("/test/hello", "/login") // 表示配置请求路径
.permitAll() // 指定 URL 无需认证。
.anyRequest() // 其他请求
.authenticated(); // 需要认证
http.csrf().disable(); // 关闭 csrf
}
  • controller
1
2
3
4
5
6
7
8
@PostMapping("/success")
public String success() {
return "success";
}
@PostMapping("/fail")
public String fail() {
return "fail";
}
  • 前端表单
1
2
3
4
5
<form action="/login"method="post">
用户名:<input type="text" name="username"/><br/> // username,password名称不能错
密码:<input type="password" name="password"/><br/>
<input type="submit"value=" 提交"/>
</form>

5.4 基于角色或权限进行访问控制

「hasAuthority」

  • 如果当前的主体具有指定权限,返回true,否则返回false。

  • 配置类加上路径和能访问的权限

1
.antMatchers("/test/index").hasAuthority("admins")
  • 自定义Service上为返回的User对象加权限
1
2
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admins");

「hasAnyAuthority」

  • 区别于上面的单个角色,此方法添加多个权限
1
.antMatchers("/test/index").hasAnyAuthority("admins, manager")

「hasRole」

  • 如果用户具备给定角色就允许访问,否则403
1
.antMatchers("/test/index").hasRole("sale")
  • 注:hasRole() 参数不能加 “ROLE_“,会抛出异常
1
2
3
4
5
6
7
8
private static String hasRole(String role) {
Assert.notNull(role, "role cannot be null");
if (role.startsWith("ROLE_")) {
throw new IllegalArgumentException("role should not start with 'ROLE_' since it is automatically inserted. Got '" + role + "'");
} else {
return "hasRole('ROLE_" + role + "')";
}
}
1
.antMatchers("/test/index").access("hasRole('sale')") // 这种写法同上,可加ROLE_,可不加
  • 给用户添加角色,需要加上前缀
1
2
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");

「hasAnyRole」

  • 同上,可添加多个,同样注意加前缀。

5.5 自定义403界面

1
2
//配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html"); // 跳转到自定义403界面
  • 跳转的路径也可以是controller进行跳转
1
2
3
4
5
6
http.exceptionHandling().accessDeniedPage("/unauth");

@GetMapping("/unauth")
public String accessDenyPage(){
return "unauth";
}

5.6 注解

「@Secured」

  • 判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 ROLE_

  • 开启注解功能(下面的注解都要打开注解功能)

@EnableGlobalMethodSecurity(securedEnabled=true) 可以加在启动类配置类

1
2
3
4
5
6
7
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled=true)
public class DemosecurityApplication {
public static void main(String[] args) {
SpringApplication.run(DemosecurityApplication.class, args);
}
}
  • 在控制器方法上添加注解
1
2
3
4
5
@GetMapping("update")
@Secured({"ROLE_sale","ROLE_manager"})
public String update() {
return "hello update";
}

「@PreAuthorize」

  • 注解适合进入方法前的权限验证,判断的是有无权限
1
2
3
4
5
6
@RequestMapping("/preAuthorize")
//@PreAuthorize("hasRole('admins')")
@PreAuthorize("hasAnyAuthority('admins')") //上面的四种方法都可以
public String preAuthorize(){
return "preAuthorize";
}

「@PostAuthorize」

  • 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限
1
2
3
4
5
6
@RequestMapping("/testPostAuthorize")
@PostAuthorize("hasAnyAuthority('admins')")
public String preAuthorize(){
System.out.println("test--PostAuthorize");
return "PostAuthorize";
}
  • 无权限登录完后虽然会虽然会进入403,但是方法里的sout依然会执行

「@PreFilter」

  • 不常用,进入控制器之前对数据进行过滤,一般就是对特定的数据拦截
1
2
3
4
5
6
7
8
9
@RequestMapping("getTestPreFilter")
@PreAuthorize("hasRole('admins')")
@PreFilter("filterObject.id%2 == 0")
public List<Users> getTestPreFilter(@RequestBody List<Users> list) {
list.forEach(t -> {
System.out.println(t.getId() + "\t" + t.getUsername());
});
return list;
}

「@PostFilter」

  • 不常用,权限验证之后对数据进行过滤,留下用户名是 admin1 的数据
1
2
3
4
5
6
7
8
9
10
@GetMapping("getAll")
@PostAuthorize("hasAnyAuthority('admin')")
@PostFilter("filterObject.username == 'admin1'")
public List<Users> getAllUser() {
ArrayList<Users> list = new ArrayList<>();
list.add(new Users(11, "admin1", "6666"));
list.add(new Users(21, "admin2", "888"));
System.out.println(list);
return list;
}

5.7 注销

  • 配置类的configure 方法
1
2
3
//退出
http.logout().logoutUrl("/logout").
logoutSuccessUrl("/test/hello").permitAll();
  • 测试可以先成功登录,再访问资源,退出后再访问相同资源需要登录。

5.8 RememberMe功能

  • 数据库创表
1
2
3
4
5
6
7
CREATE TABLE `persistent_logins`  (
`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`series` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`token` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`last_used` timestamp(0) NOT NULL,
PRIMARY KEY (`series`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
  • 配置类注入数据源,配置操作数据库对象
1
2
3
4
5
6
7
8
9
10
11
//注入数据源
@Autowired
private DataSource dataSource;
//配置对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//jdbcTokenRepository.setCreateTableOnStartup(true); 自动把表创建
return jdbcTokenRepository;
}
  • 配置类配置自动登录
1
2
3
4
.and()
.rememberMe().tokenRepository(persistentTokenRepository()) // 设置使用的repository
.tokenValiditySeconds(60) //设置有效时长,单位秒
.userDetailsService(userDetailsService);
  • 前端的自动登录的checkbox的name必须是 remember-me
1
<input type="checkbox" name="remember-me"/> 自动登录

「RememberMe原理」

  • 第一次认证的流程走上面的路线(1-4),第二次(11-14)

  • 看到 UsernamePasswordAuthenticationFilter 的父类 AbstractAuthenticationProcessingFilterdoFilter 方法在成功认证后,调用 successfulAuthentication 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}

SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}

this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
  • this.rememberMeServices.loginSuccess(request, response, authResult);继续跟进
1
2
3
4
5
6
7
public interface RememberMeServices {
Authentication autoLogin(HttpServletRequest var1, HttpServletResponse var2);

void loginFail(HttpServletRequest var1, HttpServletResponse var2);

void loginSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3);
}
  • 在其实现类 AbstractRememberMeServices
1
2
3
4
5
6
7
8
9
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
if (!this.rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
} else {
this.onLoginSuccess(request, response, successfulAuthentication);
}
}

protected abstract void onLoginSuccess(HttpServletRequest var1, HttpServletResponse var2, Authentication var3);
  • onLoginSuccess 由实现类 PersistentTokenBasedRememberMeServices 实现
1
2
3
4
5
6
7
8
9
10
11
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
String username = successfulAuthentication.getName();
this.logger.debug("Creating new persistent login for user " + username);
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
try {
this.tokenRepository.createNewToken(persistentToken);
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
  • this.tokenRepository.createNewToken(persistentToken) 通过tokenRepository,创建新Token,然后通过cookie传回给浏览器,另一边写入数据库做备份。
  • public class JdbcTokenRepositoryImpl 中会对数据库进行操作,让Token存入数据库中。

  • 「记住我」之后,下次的请求过程是
  • 先被 RememberMeAuthenticationFilter 过滤器过滤,其中的 doFilter 方法会完成自动登录
1
2
3
4
5
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
1
2
public interface RememberMeServices {
Authentication autoLogin(HttpServletRequest var1, HttpServletResponse var2);
  • 实现类 AbstractRememberMeServices
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
String rememberMeCookie = this.extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
} else {
this.logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
this.logger.debug("Cookie was empty");
this.cancelCookie(request, response);
return null;
} else {
UserDetails user = null;
try {
String[] cookieTokens = this.decodeCookie(rememberMeCookie);
user = this.processAutoLoginCookie(cookieTokens, request, response);
this.userDetailsChecker.check(user);
this.logger.debug("Remember-me cookie accepted");
return this.createSuccessfulAuthentication(request, user);
  • 前面判空。然后 try 里头对cookie解码,再调用 processAutoLoginCookie
  • 在实现类中 PersistentTokenBasedRememberMeServices
1
2
3
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
....
return this.getUserDetailsService().loadUserByUsername(token.getUsername());
  • 调用 loadUserByUsername ,即接口 UserDetailsService 里的实现类调用此方法进行数据库的查询,返回之前在数据库中存的cookie,然后进行 check 比对,成功则直接return,createSuccessfulAuthentication成功认证。

5.9 CSRF

  • 跨站请求伪造(Cross-site request forgery),可以理解为网站读取已经存在的cookie用来做恶意认证。

  • 从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCHPOSTPUT 和 DELETE 方法进行防护。主要是针对数据库操作的请求防护。

  • 导入thymeleaf-security
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--对Thymeleaf添加Spring Security标签支持-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 在登录页面添加一个隐藏域
1
2
<input
type="hidden" th:if="${_csrf}!=null" th:value="${_csrf.token}" name="_csrf"/>
  • 关闭安全配置的类中的 csrf
1
// http.csrf().disable();

「原理」

  1. 生成 csrfToken 保存到 HttpSession 或者 Cookie 中

1
2
3
4
5
public interface CsrfToken extends Serializable {
String getHeaderName();
String getParameterName();
String getToken();
}
  • SaveOnAccessCsrfToken 类有个接口 CsrfTokenRepository
1
2
private static final class SaveOnAccessCsrfToken implements CsrfToken {
private transient CsrfTokenRepository tokenRepository;
  • 其实现类 HttpSessionCsrfTokenRepository CookieCsrfTokenRepository

  • CookieCsrfTokenRepository 截取部分
1
2
3
4
5
6
7
8
9
10
11
public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
private String parameterName = "_csrf";

public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
}

private String createNewToken() {
return UUID.randomUUID().toString();
}
}
  1. 请求到来时,从请求中提取 csrfToken,和保存的 csrfToken 做比较,进而判断当前请求是否合法。主要通过 CsrfFilter 过滤器来完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class CsrfFilter extends OncePerRequestFilter {
private final CsrfTokenRepository tokenRepository;

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = csrfToken == null;
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}

request.setAttribute(CsrfToken.class.getName(), csrfToken);
// key: _csrf value: token
request.setAttribute(csrfToken.getParameterName(), csrfToken);
// 验证是否匹配正确
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
}
...

基于微服务和jwt待续。。。

鸣谢:

https://docs.spring.io/spring-security/site/docs/5.3.4.RELEASE/reference/html5/#servlet-hello

https://www.bilibili.com/video/BV1VE411h7aL

https://www.bilibili.com/video/BV15a411A7kP?from=search&seid=12610921816944973603


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!