SpringSecurity是Spring下的一个安全框架,与shiro 类似,一般用于用户认证(Authentication)和用户授权(Authorization)两个部分,常与与SpringBoot相整合。
一、介绍
SpringSecurity 过滤器链
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的各个进行说明:
1.WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
2.SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
3.HeaderWriterFilter:用于将头信息加入响应中。
4.CsrfFilter:用于处理跨站请求伪造。
5.LogoutFilter:用于处理退出登录。
6.UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
7.DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
8.BasicAuthenticationFilter:检测和处理 http basic 认证。
9.RequestCacheAwareFilter:用来处理请求的缓存。
10.SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
11.AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
12.SessionManagementFilter:管理 session 的过滤器
13.ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
14.FilterSecurityInterceptor:可以看做过滤器链的出口。
15.RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
流程说明
1.客户端发起一个请求,进入 Security 过滤器链。
2.当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
3.当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
4.当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
二、依赖引入
完整的pom文件如下:
<?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.4.1</version>
<relativePath></relativePath> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>spring-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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>
<mainClass>com.example.springsecurity.SpringSecurityApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
三、配置文件
# 开发时关闭缓存,不然没法看到实时页面
spring.thymeleaf.cache=false
# 用非严格的 HTML
spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=utf-8
spring.thymeleaf.servlet.content-type=text/html
spring.datasource.druid.url=jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.druid.username=root
spring.datasource.druid.password=sunyue
spring.datasource.druid.initial-size=1
spring.datasource.druid.min-idle=1
spring.datasource.druid.max-active=20
spring.datasource.druid.test-on-borrow=true
#springbootjdbc导入包不和以前一样
spring.datasource.druid.driver-class-name= com.mysql.cj.jdbc.Driver
mybatis.type-aliases-package=com.example.springsecurity.entity
mybatis.mapper-locations=classpath:mapper/*.xml
#打印数据库的操作
logging.level.com.example.springsecurity.dao=debug
#redis缓存
### 配置Redis
mybatis.configuration.cache-enabled=true
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=152.136.30.116
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=sunyue
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-idle=200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=1000
四、Security 配置类
SecurityConfig:
package com.example.springsecurity;
import com.example.springsecurity.server.serverImpl.TestUserServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity //注解开启Spring Security的功能
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//明文加密器,只需要在内存中有这个管理对象,如果不添加,从前端登录时会抛出异常Bad credentials(数据库操作需要这个bean,内存不需要,只需要将密码加密就可以)
/*内置的PasswordEncoder实现列表
NoOpPasswordEncoder(已废除)
明文密码加密方式,该方式已被废除(不建议在生产环境使用),不过还是支持开发阶段测试Spring Security的时候使用。
BCryptPasswordEncoder
Argon2PasswordEncoder
Pbkdf2PasswordEncoder
SCryptPasswordEncoder */
//使用以上四个方法都可以解密,但是数据库中得密码也是对应方法得加密(添加用户数据是需要password加密new BCryptPasswordEncoder().encode("123456"))
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();//passwordEncoder的实现类
}
//构造一个内存框架对象,获取数据库中的数据
/* @Bean
public UserDetailsService myUserDetailsService(){
return new TestUserServerImpl();
}*/
//也可以自动注入
@Autowired
private TestUserServer testUserServer;
//用户授权
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//基于内存来存储用户信息(需要加密不然会报错---Encoded password does not look like BCrypt)
/*auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("user").password(new BCryptPasswordEncoder().encode("password")).authorities("user").and() //设置
.withUser("admin").password(new BCryptPasswordEncoder().encode("password")).authorities("admin", "user");*/
//基于数据库来存储用户信息
//auth.userDetailsService(myUserDetailsService());
auth.userDetailsService(testUserServer);
}
//用户权限认证
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//这是认证的请求
.authorizeRequests()
.antMatchers("/", "/home", "/login", "/index", "/error").permitAll() //这些请求 不需要认证
//hasRole和hasAuthority的区别:我们在调用 hasAuthority 方法时,如果数据是从数据库中查询出来的,这里的权限和数据库中保存一致即可,
// 可以不加 ROLE_ 前缀。即数据库中存储的用户角色如果是 admin,这里就是 admin。
//也就是说,使用 hasAuthority 更具有一致性,你不用考虑要不要加 ROLE_ 前缀,数据库什么样这里就是什么样!
// 而 hasRole 则不同,代码里如果写的是 USER,框架会自动加上 ROLE_ 前缀,所以数据库就必须是 ROLE_USER
.antMatchers("/user/**").hasRole("USER") //user及以下路径,需要ROLE_USER角色权限
.antMatchers("/admin/**").hasAuthority("admin")//admin及以下路径,需要admin权限
.and()
//loginPage定制自定义登录页,相当于/toLogin其实自动转到/login,loginProcessingUr指定为login-》toLogin(表单提交只能为login),加了这个需要接受用户名和密码,登录成功跳转 "/"
//.formLogin().loginPage("/toLogin").usernameParameter("username").passwordParameter("password").loginProcessingUrl("/login").defaultSuccessUrl("/")
.formLogin()//自带的login
.and()
.csrf().disable()//关闭csrf
//等出路径为logout,登出成功跳转 "/"
.logout().logoutUrl("/logout").logoutSuccessUrl("/")
.and()
.rememberMe().rememberMeParameter("rememberMe");
}
/**
* 核心过滤器配置,更多使用ignoring()用来忽略对静态资源的控制
*/
@Override
public void configure(WebSecurity web) throws Exception {
web
.ignoring()
.antMatchers("/image/**");
}
}
AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让Security 自动构建一个 AuthenticationManager;如果想要使用该功能你需要配置一个 UserDetailService 和 PasswordEncoder。UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户, PasswordEncoder 用于密码的加密与比对,我们存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。如果重写了该方法,Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用 UserDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功成功则返回一个 Authentication 对象。
五、自定义的UserDetailService
TestUserServer:
package com.example.springsecurity.server.serverImpl;
import com.example.springsecurity.dao.TestUserMapper;
import com.example.springsecurity.entity.TestUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class TestUserServer implements UserDetailsService {
@Autowired
private TestUserMapper testUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//username参数,是在登陆时,用户传递的表单数据username
//主要读取数据库3个值 username password authorities
TestUser testUser = testUserMapper.selectOne(username);
String authorityName = testUser.getAuthority();
//为了返回一个UserDetails 使用User
List<GrantedAuthority> authorities = new ArrayList<>();
GrantedAuthority authority = new SimpleGrantedAuthority(authorityName);
authorities.add(authority);
//这里的User 是这个包下的 org.springframework.security.core.userdetails.User;
return new User(
testUser.getUsername(),
testUser.getPassword(),
authorities);
}
}
六、实体类和DAO等
实体类:
package com.example.springsecurity.entity;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
@Data
@Accessors(chain = true)
public class TestUser implements Serializable {
private Long id;
private String username;
private String password;
private String authority;
private Date created;
private Date updated;
}
DAO
package com.example.springsecurity.dao;
import com.example.springsecurity.entity.TestUser;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface TestUserMapper {
List<TestUser> findAll();
TestUser selectOne(String username);
void insert(TestUser testUser);
void update(TestUser testUser);
void delete(String id);
}
Mapper
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springsecurity.dao.TestUserMapper">
<select id="findAll" resultType="TestUser">
select * from test_user
</select>
<select id="selectOne" resultType="TestUser">
select * from test_user where username=#{username}
</select>
<insert id="insert">
insert into test_user (username,password,authority,created,updated) value (#{username},#{password},#{authority},#{created},#{updated})
</insert>
<update id="update">
update test_user set username = #{username},password=#{password},authority=#{authority} where id = #{id}
</update>
<delete id="delete">
delete from test_user where id = #{id}
</delete>
</mapper>
七、前台代码
1.index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<!--自带登录页-->
<p><a th:href="@{/login}">登录</a></p>
<!--自定义登录页-->
<!--<p><a th:href="@{/toLogin}">登录</a></p>-->
<p><a th:href="@{/user}">用户</a></p>
<p><a th:href="@{/admin}">管理员</a></p>
<p><a th:href="@{/logout}">退出</a></p>
</body>
</html>
2.login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form th:action="@{/login}" method="post">
<input type="text" name="username" placeholder="UserName"/>
<input type="password" name="password" placeholder="PassWord"/>
<input type="checkbox" name="rememberMe"/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
3.user.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>user</title>
</head>
<body>
user
</body>
</html>
4.admin.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>admin</title>
</head>
<body>
admin
</body>
</html>
5.数据库sql文
/*
Navicat MySQL Data Transfer
Source Server : sunyue
Source Server Version : 50724
Source Host : localhost:3306
Source Database : security
Target Server Type : MYSQL
Target Server Version : 50724
File Encoding : 65001
Date: 2021-01-13 23:33:57
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for test_user
-- ----------------------------
DROP TABLE IF EXISTS `test_user`;
CREATE TABLE `test_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(100) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
`authority` varchar(100) DEFAULT NULL,
`created` date DEFAULT NULL,
`updated` date DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of test_user
-- ----------------------------
INSERT INTO `test_user` VALUES ('3', 'user', '$2a$10$Skjo8i3cSopkOtVsvfX5I.eOPCOFm2B/CD4t0VjUDXfTZk6aSAvia', 'ROLE_USER', '2021-01-12', '2021-01-12');
INSERT INTO `test_user` VALUES ('4', 'admin', '$2a$10$3rzQ1Pn.Onx9N/Dy6a5O8.TZfB/kqo/Z1UOj9udQl4ne0AZDaxn4O', 'admin', '2021-01-12', '2021-01-12');
总结:当数据库中authority
字段为ROLE_USER则拥有USER角色权限,可以访问路径/user下,为admin则拥有
/admin下