1、什么Spring Security?
Spring Security 是一种强大的框架,它在 Spring 生态系统中扮演着保护应用安全的关键角色。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。
2、认证 和 授权
1.什么是认证?
用户的认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
通俗点说就是系统认为用户是否能登录
在 Spring Security 中,认证是通过AuthenticationManager
接口处理的,它通常使用一系列的AuthenticationProvider
来验证用户身份。
2.什么是授权?
验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。
比如拿文件来说,有的用户只能进行读取,而有的用户可以进行修改。
通俗点讲就是系统判断用户是否有权限去做某些事情。
3、为什么Spring Security (对比)
特征 | Spring Security | Apache Shiro |
---|---|---|
设计哲学 | 集成于 Spring 框架,重点关注企业级安全需求。 | 独立的安全框架,设计简单,易于集成和使用。 |
主要优点 | 和 Spring 无缝整合,全面的权限控制,适应 Web 开发。新版本可脱离 Web 环境使用。 | 轻量级,性能优异,通用性强,不局限于 Web 环境。 |
局限性 | 旧版本依赖 Web 环境,重量级。 | 在 Web 环境下特定需求可能需手动编码。 |
认证支持 | 支持表单登录、LDAP、数据库认证、OAuth等。 | 支持多种认证策略,包括基于角色的访问控制和简单的身份验证。 |
授权支持 | 支持基于角色和表达式的访问控制。 | 提供静态和动态权限分配,支持角色基础的授权。 |
会话管理 | 依赖外部容器或Spring框架的会话管理。 | 内建会话管理,适合于非Web环境下的应用。 |
扩展性和定制 | 高度可定制和可扩展,适应复杂安全需求。 | 提供简单直接的扩展机制,易于快速部署和定制。 |
使用场景 | 适合大型企业级应用,尤其是基于Spring的应用。 | 适合不依赖Spring或需简单安全框架的应用。 |
常见技术栈 | Spring Boot/Spring Cloud + Spring Security | SSM + Shiro |
4、入门案例
1、新建SpringBoot工程securitydemo1(2.2.1.RELEASE)
2、POM
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3、新建控制层
@RestController
public class TestController {
@GetMapping("/hello")
public String test() {
return "hello World";
}
}
4、启动并访问localhost:8080/help进入如下验证页面
默认用户名user,密码为如下默认密码
5、基本原理
1. 认证(Authentication)
认证是过程中第一步,目的是确认一个用户的身份。用户在登录时提供凭据(通常是用户名和密码),Spring Security 通过配置的认证机制来验证这些凭据是否与系统中的相匹配。这个过程通常涉及以下几个组件:
- AuthenticationManager: 这是认证过程的主要入口点,负责协调认证过程。
- AuthenticationProvider: 这是一个接口,定义了具体的认证逻辑。例如,可以通过数据库、LDAP等多种方式来实现。
- UserDetailsService: 在大多数情况下,这个服务用于根据用户名查找用户的详细信息,通常是用户的密码、角色等。
- PasswordEncoder: 用于在验证用户提交的密码时,对存储的密码进行编码比较。
2. 授权(Authorization)
一旦用户通过认证,下一步是确定他们是否有权执行特定的操作。授权可以在访问控制列表、角色基于的控制或更细粒度的权限控制级别上进行。
- AccessDecisionManager: 负责决定访问控制的核心接口。它使用下面的投票器来决定是否授予权限。
- Voter: 在决定一个特定的安全对象(如HTTP请求、方法调用等)是否可以被访问时,投票器进行投票。
- Security Interceptor: 它拦截被保护的操作请求,然后调用
AccessDecisionManager
来决定是否可以执行该操作。
3. 安全过滤器链(Security Filter Chain)
Spring Security 使用一系列的过滤器来应用上述的认证和授权。这些过滤器在应用服务器处理请求的过程中工作,确保所有的HTTP请求都经过严格的安全检查。
- SecurityContextHolder: 在整个请求处理过程中存储关于当前安全上下文的信息,如当前用户的详细信息。
4.过滤器链的加载过程
-
定义 FilterChainProxy:
- Spring Security 使用
FilterChainProxy
类来管理多个安全过滤器。这个代理类是配置在 Spring 容器中的一个 Bean,它包含了一个或多个过滤器链。
- Spring Security 使用
-
配置 SecurityFilterChain:
- 在 Spring Security 的配置中,可以定义一个或多个
SecurityFilterChain
对象,每个对象代表一系列应用于特定请求模式的过滤器列表。 - 每个
SecurityFilterChain
包含了具体的过滤器实例和这些过滤器应当应用的 URL 模式。
- 在 Spring Security 的配置中,可以定义一个或多个
-
过滤器的初始化:
- 在应用启动时,Spring 容器初始化
FilterChainProxy
并设置所有配置的SecurityFilterChain
。这些过滤器在容器中通常是作为单例存在的。 - 过滤器链的顺序非常关键,因为它们决定了过滤器如何拦截和处理进来的 HTTP 请求。
- 在应用启动时,Spring 容器初始化
-
Servlet 容器集成:
FilterChainProxy
通常注册为一个标准的 Servlet 过滤器。在大多数情况下,这是通过在web.xml
中配置或在 Spring Boot 应用中自动配置完成的。- 当一个 HTTP 请求到达应用时,请求首先通过
FilterChainProxy
。代理会根据请求的 URL 模式确定应该应用哪个SecurityFilterChain
。
-
过滤器执行:
- 一旦确定了应用于当前请求的
SecurityFilterChain
,请求就会依次通过链中的每个过滤器。这些过滤器执行各种安全检查,如认证、授权、CSRF保护等。
- 一旦确定了应用于当前请求的
-
自定义和扩展:
- 开发者可以通过添加自定义过滤器到
SecurityFilterChain
来扩展或修改安全行为。这是通过在 Spring Security 的配置中定义额外的过滤器或修改现有过滤器链来实现的。
- 开发者可以通过添加自定义过滤器到
6、接口
1.认证相关接口
-
AuthenticationManager
- 描述: 这是 Spring Security 的核心接口之一,用于管理认证过程。
- 主要方法:
authenticate(Authentication authentication)
— 尝试认证传递的Authentication
对象,并返回一个完全填充的Authentication
实例(包括授予的权限)如果认证成功。
-
AuthenticationProvider
- 描述: 用于实现具体的认证逻辑。
- 主要方法:
authenticate(Authentication authentication)
— 进行实际的认证。 - 主要方法:
supports(Class<?> authentication)
— 确定此AuthenticationProvider
是否可以处理指定的认证类型。
-
UserDetailsService
- 描述: 加载用户特定数据的核心接口。
- 主要方法:
loadUserByUsername(String username)
— 根据用户名加载用户的数据。
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。
-
PasswordEncoder
- 描述: 为存储和验证密码提供编码和解码功能。
- 主要方法:
encode(CharSequence rawPassword)
— 编码原始密码。 - 主要方法:
matches(CharSequence rawPassword, String encodedPassword)
— 验证原始密码和编码后的密码是否匹配。 -
@Test public void test01(){ // 创建密码解析器 BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); // 对密码进行加密 String laptoy = bCryptPasswordEncoder.encode("laptoy"); // 打印加密之后的数据 System.out.println("加密之后数据:\t"+laptoy); //判断原字符加密后和加密之前是否匹配 boolean result = bCryptPasswordEncoder.matches("laptoy", laptoy); // 打印比较结果 System.out.println("比较结果:\t"+result); }
2.授权相关接口
-
AccessDecisionManager
- 描述: 负责授权决策的处理。
- 主要方法:
decide(Authentication authentication, Object secureObject, Collection<ConfigAttribute> configAttributes)
— 根据提供的权限信息和安全对象(如HTTP请求),决定是否允许访问。
-
GrantedAuthority
- 描述: 表示授权信息的接口,如用户的安全角色。
- 主要方法:
getAuthority()
— 返回表示权限的字符串。
3.过滤器相关接口和类
-
FilterChainProxy
- 描述: 管理多个安全过滤器链。
- 功能: 根据请求的URL,决定哪个安全过滤器链被触发。
-
安全过滤器
- 示例:
UsernamePasswordAuthenticationFilter
,BasicAuthenticationFilter
,JwtRequestFilter
等。 - 功能: 执行具体的安全检查,如检查请求头中的令牌、处理用户登录请求等。
- 示例:
7、web权限方案
1、用户认证
设置登录的用户名和密码
1、配置文件方式
spring:
security:
user:
name: zNuyoah
password: zNuyoah
2、配置类方式
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String passwd = bCryptPasswordEncoder.encode("zNuyoah");
auth.inMemoryAuthentication().withUser("zNuyoah").password(passwd).roles("admin");
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3、自定义编写实现类方式(常用)
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 赋予当前用户权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
// 这里的密码可以通过注入 mapper层 从数据库中查找
return new User(
"zNuyoah",
new BCryptPasswordEncoder().encode("zNuyoah"),
auths);
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Bean
public PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
}
2、整合MybatisPlus
1、POM、YML
<!--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>
<!--lombok 用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?serverTimezone=GMT%2B8
username: root
password: root
2、创建数据库 security
create table users (
id bigint primary key auto_increment,
username varchar(20) unique not null,
password varchar(100)
);
insert into users values(1,'zNuyoah','zNuyoah');
3、实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users {
private Integer id;
private String username;
private String password;
}
4、数据交互层
public interface UserMapper extends BaseMapper<Users> {
}
5、业务层
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
Users users = userMapper.selectOne(wrapper);
if (users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
// 授予权限(后面有用)
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User(
users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()),
auths);
}
}
6、配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
@Bean
public PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
7、控制层
@RestController
public class TestController {
@GetMapping("/hello")
public String test() {
return "hello World";
}
}
8、启动类
@MapperScan("com.zNuyoah.mapper")
@SpringBootApplication
public class SecurityDemo1Application {
public static void main(String[] args) {
SpringApplication.run(SecurityDemo1Application.class, args);
}
}
3、自定义用户登录页面
1、配置类配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyUserDetailService myUserDetailService;
@Bean
public PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(password());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义登录页面
http.formLogin()
.loginPage("/login.html") // 自定义页面设置
.loginProcessingUrl("/login") // 登录表单提交路径
.defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
// 权限认证
http.authorizeRequests()
.antMatchers("/", "/hello").permitAll() // 设置忽略的路径
.anyRequest().authenticated() // 设置所有路径都需认证(不包括忽略的路径)
// 关闭csrf保护功能
http.csrf().disable();
}
}
2、resource/static/login.html
<!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="text" name="password"><br>
<input type="submit" value="login">
</form>
</body>
</html>
3、控制层
@RestController
public class TestController {
@GetMapping("/hello")
public String test() {
return "hello World";
}
@GetMapping("/index")
public String index() {
return "hello index";
}
}
4、测试
1、/hello 可以直接访问
2、可以配置自定义登录页面
3、登录成功跳转到 /index
4、用户授权
1、hasAuthority 方法
如果当前的主体具有指定的权限,则返回 true,否则返回 false
配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义登录页面
http.formLogin()
.loginPage("/login.html") // 自定义页面设置
.loginProcessingUrl("/login") // 登录访问路径
.defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
// 权限认证
http.authorizeRequests()
.antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
$ .antMatchers("/index").hasAuthority("admin") // 当前登录用户只有具有admin权限才能访问
.anyRequest().authenticated() // 设置所有路径都需认证(不包括忽略的路径)
// 关闭csrf保护功能
http.csrf().disable();
}
}
当前登录用户只有具有admin权限才能访问
.antMatchers("/index").hasAuthority("admin")
接口
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
Users users = userMapper.selectOne(wrapper);
if (users == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
// 授予admin权限
$ List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
return new User(
users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()),
auths);
}
}
授予admin权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
若当前登录用户无对应权限,返回 403状态码
2、hasAnyAuthority 方法
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义登录页面
http.formLogin()
.loginPage("/login.html") // 自定义页面设置
.loginProcessingUrl("/login") // 登录访问路径
.defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
http.authorizeRequests()
.antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
//.antMatchers("/index").hasAuthority("admin") // 当前登录用户只有具有admin权限才能访问
$ .antMatchers("/index").hasAnyAuthority("admin", "root") // 有其中一个权限就能访问
.anyRequest().authenticated() // 设置所有路径都需认证(不包括忽略的路径)
http.csrf().disable();
}
3、hasRole 方法
如果当前主体具有指定的角色,则返回 true
源码
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 + "')";
}
}
源码显示拼接用户名时为 ROLE_xxx
return "hasRole('ROLE_" + role + "')";
配置类添加角色授权
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义登录页面
http.formLogin()
.loginPage("/login.html") // 自定义页面设置
.loginProcessingUrl("/login") // 登录访问路径
.defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
http.authorizeRequests()
.antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
$ .antMatchers("/index").hasRole("admin")
.anyRequest().authenticated() // 设置所有路径都需认证(不包括忽略的路径)
http.csrf().disable();
}
此时接口方法授权限必须为
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin");
4、hasAnyRole
表示用户具备任何一个条件都可以访问
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义登录页面
http.formLogin()
.loginPage("/login.html") // 自定义页面设置
.loginProcessingUrl("/login") // 登录访问路径
.defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
http.authorizeRequests()
.antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
$ .antMatchers("/index").hasAnyRole("admin", "root")
.anyRequest().authenticated() // 设置所有路径都需认证(不包括忽略的路径)
http.csrf().disable();
}
5、自定义403页面
1、新建页面 /static/unauth.html
<body>
<h1>抱歉,你没有权限访问</h1>
</body>
2、配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
// 自定义登录页面
http.formLogin()
.loginPage("/login.html") // 自定义页面设置
.loginProcessingUrl("/login") // 登录访问路径
.defaultSuccessUrl("/index").permitAll() // 登录成功后跳转到
http.authorizeRequests()
.antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
.antMatchers("/index").hasAuthority("admin")
.anyRequest().authenticated() // 设置所有路径都需认证(不包括忽略的路径)
http.csrf().disable();
}
3、授权
6、注解使用
1、@Secured:判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 ROLE_
主启动类开启注解功能:@EnableGlobalMethodSecurity(securedEnabled=true)
控制层
@GetMapping("/update")
@Secured({"ROLE_sale", "ROLE_xxx"})
public String update() {
return "hello update";
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
2、@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用户的
roles/permissions 参数传到方法中
主启动类开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled = true)
@GetMapping("/update")
@PreAuthorize("hasAnyAuthority('admin')")
public String update() {
return "hello update";
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
3、@PostAuthorize:在方法执行后再进行权限验证,适合验证带有返回值的权限
主启动类开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled = true)
@GetMapping("/update")
@PostAuthorize("hasAnyAuthority('xxx')")
public String update() {
System.out.println("@@@@@@@@@@@@@@@@@@@@");
return "hello update";
}
虽然无法访问页面,但是控制台输出了业务逻辑
4、@PostFilter:权限验证之后对返回值进行过滤
@GetMapping("/getAll")
@PreAuthorize("hasAnyAuthority('admin')")
@PostFilter("filterObject.username == 'zNuyoah'")
public List<Users> getAll() {
ArrayList<Users> list = new ArrayList<>();
list.add(new Users(1, "zNuyoah", "123"));
list.add(new Users(2, "z_Nuyoah", "123"));
System.out.println(list);
return list;
}
5、@PreFilter:进入控制器之前对数据进行过滤
@GetMapping("/getAll")
@PreAuthorize("hasAnyAuthority('admin')")
@PreFilter("filterObject.username == 'laptoy'")
public List<Users> getAll(@RequestBody List<Users> list) {
list.forEach(l -> {
System.out.println(l.getUsername());
});
return list;
}
先登陆后使用PostMan进行测试
7、用户注销
1、login.html
<body>
<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
<input type="submit" value="login">
</form>
</body>
2、success.html
<body>
<h1>登录成功</h1>
<a href="/logout">登出</a>
</body>
3、配置类
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登出
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();
// 自定义登录页面
http.formLogin()
.loginPage("/login.html") // 自定义页面设置
.loginProcessingUrl("/login") // 登录访问路径
.defaultSuccessUrl("/success.html").permitAll() // 登录成功后跳转到
http.authorizeRequests()
.antMatchers("/", "/login").permitAll() // 设置忽略的路径
.anyRequest().authenticated() // 设置所有路径都需认证(不包括忽略的路径)
http.csrf().disable();
}
4、控制层
@RestController
public class TestController {
@GetMapping("/index")
public String index() {
return "hello index";
}
}
5、测试
1.访问 localhost:8080/login.html 登录成功后 点击 登出
2.访问 /index 需要重新进行登录验证
8、记住我功能
1、实现原理
2、认证及存放 Token 过程
代码实现
1、建表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2、配置类 注入数据源,配置操作数据库对象
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
...
// 注入数据源
@Autowired
private DataSource dataSource;
// 配置对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
...
}
3、配置类配置自动登录
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60) // 设置有效时长 秒
.userDetailsService(userDetailsService())
}
4、login.html
<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
记住我:<input type="checkbox" name="remember-me"><br>
<input type="submit" value="login">
</form>在这里插入代码片
配置类全部代码展示
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailService myUserDetailService;
// 注入数据源
@Autowired
private DataSource dataSource;
// 配置对象
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
@Bean
public PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(password());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 登出
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html").permitAll();
// 配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
// 自定义登录页面
http.formLogin()
.loginPage("/login.html") // 自定义页面设置
.loginProcessingUrl("/login") // 登录访问路径
.defaultSuccessUrl("/success.html").permitAll(); // 登录成功后跳转到
// 权限认证
http.authorizeRequests()
.antMatchers("/", "/hello", "/login").permitAll() // 设置忽略的路径
.anyRequest().authenticated(); // 设置所有路径都需认证(不包括忽略的路径)
// 记住我
http.rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60) // 设置有效时长 秒
.userDetailsService(userDetailsService());
// 关闭csrf保护功能
http.csrf().disable();
}
}
9、CSRF
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session
riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非
本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,
CSRF 利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾
经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏
览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身
份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是
用户自愿发出的。
从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,
Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
1、登录页面添加隐藏域
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
2、配置类开启CSRF
// http.csrf().disable();
8、微服务权限方案
1、认证授权过程分析
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于token 的形式进行授权
与认证
1、用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值
2、以 用户名为 key,权限列表为 value 的形式存入 redis 缓存中
3、根据用户名相关信息 生成 token返回
4、浏览器将 token记录到 cookie 中,每次调用 api 接口都默认将 token 携带 到 header 请求头中
5、Spring-security 解析 header 头获取 token 信息,解析 token 获取当前 用户名,根据用户
名就可以从 redis 中获取权限列表,
6、这样 Spring-security 就能够判断当前 请求是否有权限访问
2、数据模型
执行资料里的 SQL脚本
3、搭建项目工程
1、新建Springboot父工程 acl_parent (2.2.1.RELEASE)
●删除 src目录
●POM添加 <packaging>pom</packaging>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.laptoy</groupId>
<artifactId>acl_parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>acl_parent</name>
<description>acl_parent</description>
<modules>
<module>common</module>
<module>infrastructure</module>
<module>service</module>
</modules>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.0.5</mybatis-plus.version>
<velocity.version>2.0</velocity.version>
<swagger.version>2.7.0</swagger.version>
<jwt.version>0.7.0</jwt.version>
<fastjson.version>1.2.28</fastjson.version>
<gson.version>2.8.2</gson.version>
<json.version>20170516</json.version>
<cloud-alibaba.version>0.2.2.RELEASE</cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<!--Spring Cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis-plus 持久层-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${json.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、在父工程创建子模块(以下都是maven工程)
1)common (删除 src目录 POM添加 <packaging>pom</packaging>)
<?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">
<parent>
<artifactId>acl_parent</artifactId>
<groupId>com.laptoy</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common</artifactId>
<packaging>pom</packaging>
<modules>
<module>service_base</module>
<module>spring_security</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided </scope>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<scope>provided </scope>
</dependency>
<!--lombok用来简化实体类:需要安装lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided </scope>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<scope>provided </scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<scope>provided </scope>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
</dependencies>
</project>
●service_base:工具类
<?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">
<parent>
<artifactId>common</artifactId>
<groupId>com.laptoy</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>service_base</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
●spring_security:权限配置
<?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">
<parent>
<artifactId>common</artifactId>
<groupId>com.laptoy</groupId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring_security</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.laptoy</groupId>
<artifactId>service_base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
</dependencies>
</project>
2)infrastructure(删除 src目录 POM添加 <packaging>pom</packaging>)
<?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">
<parent>
<artifactId>acl_parent</artifactId>
<groupId>com.laptoy</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>infrastructure</artifactId>
<packaging>pom</packaging>
<modules>
<module>api_gateway</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
●api_gateway:网关
<?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">
<parent>
<artifactId>infrastructure </artifactId>
<groupId>com.laptoy</groupId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api_gateway</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.laptoy</groupId>
<artifactId>service_base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!--服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>
3)service(删除 src目录 POM添加 <packaging>pom</packaging>)
<?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">
<parent>
<artifactId>acl_parent</artifactId>
<groupId>com.laptoy</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>service</artifactId>
<packaging>pom</packaging>
<modules>
<module>service_acl</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.laptoy</groupId>
<artifactId>service_base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--服务注册-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--服务调用-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
</dependency>
<!--lombok用来简化实体类:需要安装lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
●service_acl:权限管理微服务模块
<?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">
<parent>
<artifactId>service</artifactId>
<groupId>com.laptoy</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>service_acl</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.laptoy</groupId>
<artifactId>spring_security</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
</project>
4、security 工具类
1、密码处理工具类
package com.laptoy.security.security;
import com.laptoy.utils.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* 密码处理工具类
*/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
public DefaultPasswordEncoder(int strength) {
}
// 进行 MD5 加密
@Override
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
/**
* 传入的密码 与 数据库密码进行比较
*
* @param rawPassword 传入的密码
* @param encodedPassword 数据库中的密码
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
2、Token工具类
package com.laptoy.security.security;
import io.jsonwebtoken.CompressionCodecs;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class TokenManager {
// token 有效事件
private long tokenExpiration = 24 * 60 * 60 * 1000;
// 编码密钥用于签名
private String tokenSignKey = "123456";
// 1、根据用户名生成token
public String createToken(String username) {
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey)
.compressWith(CompressionCodecs.GZIP)
.compact();
return token;
}
// 2、根据token字符串得到用户信息
public String getUserInfoFormToken(String token) {
String userInfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return userInfo;
}
// 3、删除token
public void removeToken(String token) {
}
}
3、退出处理器
package com.laptoy.security.security;
import com.laptoy.utils.utils.R;
import com.laptoy.utils.utils.ResponseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
@Autowired
private RedisTemplate redisTemplate;
//构造方法
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 从header获取token
String token = request.getHeader("token");
if (token != null) {
// 从header移除token
tokenManager.removeToken(token);
// 从token获取用户名
String username = tokenManager.getUserInfoFormToken(token);
// 从redis移除token
redisTemplate.delete(username);
}
ResponseUtil.out(response, R.ok());
}
}
4、未授权统一处理类
public class UnAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}