概要
Spring Security基于Spring框架,提供了一套Web应用安全性的完整解决方案。Spring Boot对于Spring Security提供了自动化配置方案,可以使用更少的配置来使用Spring Security.
一般来说,web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)。
用户认证:验证某用户是否是系统中的合法主体。即用户能否登录该系统。
用户授权:验证某用户是否有权限执行某个操作。
同类产品:
- shiro:Apache旗下的轻量级权限控制框架
Spring Security和Shiro比较
Spring Security特点:
- 重量级框架(依赖很多组件)
- 和Spring无缝整合
- 全面的权限控制
- 专门为web开发设计(旧版本不能脱离Web环境使用,新版本对整个框架进行了分层抽取,分成了核心模块和web模块。单独引入核心模块就可以脱离web环境)
Shiro特点:
- 轻量级
- 通用性
- 好处:不局限于web环境,可以脱离web环境使用
- 坏处:在web环境下一些特定的需求需要手动编写代码
常见安全管理技术栈组合:
- SSM+Shiro
- Spring Boot/Spring Cloud + Spring Security
Spring Security模块划分:
- 核心 spring-security-core.jar
包含身份验证和访问控制类和接口,远程支持和基本配置API. 使用Spring-security的任何应用程序都需要此模块. 支持独立的应用程序,远程客户端,方法(服务层)安全性和JDBC用户配置. 包含包:
org.springframework.security.core
org.springframework.security.access
org.springframework.security.authentication
org.springframework.security.provisioning
- 远程处理 spring-security-remoting.jar
提供与Spring Remoting的集成. 除非你非要编写使用Spring Remoting的远程客户端,否则不需要这样做
主要包:
org.springframework.security.remoting
- 网页 spring-security-web.jar
包含过滤器和相关的Web安全基础结构代码.任何与Servlet API依赖的东西. 如果你需要Spring Security Web认证服务和基于URL的访问控制,则需要它.
主要包:
org.springframework.security.web
- 配置 spring-security-config.jar
包含安全名称空间解析代码和Java配置代码.如果你使用Spring Security XML名称空间精选配置或Spring Security 的java配置支持,则需要它.
主要包是:
org.springframework.security.config
- …
快速入门
1:创建Spring Boot项目
2:导入Spring Security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3:编写controller类测试
注:Spring Security有默认账号密码,账号:user,密码输出在控制台中。
Spring Security基本原理
Spring Security本质是一个过滤器链(有很多过滤器)
几个重要的过滤器
-
FilterSecurityInterceptor(是一个方法级的权限过滤器,基本位于过滤器的最底部)
super.beforeInvocation(filterInvocation) // 查看之前的filter是否通过
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse()); // 真正调用后台服务
- ExceptionTranslationFilter(异常过滤器,用来处理在认证授权中抛出的异常)
- UsernamePasswordAuthenticationFilter(对/login的post请求做拦截,校验表单中的用户名、密码)
两个重要的接口
1:UserDetailsService
当什么也没有配置的时候,账号和密码都是由Spring Security定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要自定义逻辑控制认证逻辑。
如果需要自定义逻辑时, 只需要实现UserDetailsService接口。去数据库查询用户名和密码的过程就写在这里面。(UsernamePasswordAuthenticationFilter中是获取用户名和密码,但查询是在UserDetailsService中做的)
自定义用户认证:
- 需要继承UsernamePasswordAuthenticationFilter类
- 重写attemptAuthentication方法(用户如何认证)
- 重写successfulAuthentication方法(用户认证通过的操作)
- 重写unsuccessfulAuthentication方法(用户认证失败的操作)
- 创建类实现UserDetailsService接口
- 编写查询数据过程,返回User对象(User是安全框架提供的对象)
2:PasswordEncoder
数据加密接口,用于返回User对象里面的密码加密。
使用示例:
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 对密码进行加密
String password = bCryptPasswordEncoder.encode("password");
// 打印加密之后的数据
System.out.println(password);
// 判断原字符加密之后是否和加密之前匹配
boolean result = bCryptPasswordEncoder.matches("password", password);
Web权限方案
用户认证
设置登录的用户名和密码
第一种方式:通过配置文件
// application.yml中配置用户密码 启动项目之后控制台不会再输出之前的默认密码(因为已经指定了用户名和密码)
spring:
security:
user:
name: yangsha
password: yangsha
第二种方式:基于配置类
@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("yangsha1").password(password).roles("admin"); // 将该用户存入内存中
}
}
按照上述代码启动项目登录后会报错,原因是没有注册PasswordEncoder组件。
改进之后的代码:
@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("yangsha1").password(password).roles("admin"); // 将该用户存入内存中
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
第三种:自定义实现类 (大多数情况都使用该方式)
第一步:创建配置类,指定使用的UserDetailService实现类
第二步:编写实现类,返回User对象。(User对象有用户名、密码和操作权限)
代码示例:
/**
* SpringSecurity配置类:用于设置登录的用户名和密码
* @author : yangsha
* @date : 2021/4/28
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
/**
* 获得User对象,并进行返回
* @author : yangsha
* @date : 2021/4/28
*/
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
// 返回User对象 权限不能传null
return new User("yangsha", new BCryptPasswordEncoder().encode("123"), auths);
}
}
基于数据库的认证
/**
* SpringSecurity配置类:用于设置登录的用户名和密码
* @author : yangsha
* @date : 2021/4/28
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
/**
* 配置用户名 密码
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
}
/**
* 放开h2数据库的权限
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
//.antMatchers("/online-signs/**")
.antMatchers("/h2-console/**");
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
/**
* 获得User对象,并进行返回
* @author : yangsha
* @date : 2021/4/28
*/
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 从数据库取出当前用户
SysUser sysUser = sysUserService.getOne(new QueryWrapper<SysUser>().eq("user_name", s));
// 判断
if(ObjectUtils.isEmpty(sysUser)){
throw new RuntimeException("用户名不存在");
}
// 权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
// 返回User对象 权限不能传null
return new User(sysUser.getUserName(), new BCryptPasswordEncoder().encode(sysUser.getPassword()), auths);
}
}
自定义登录页面
- Security中重写(configure(HttpSecurity http)方法)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 自定义编写的登录页面
.loginPage("/login.html") // 登录页面设置
.loginProcessingUrl("/user/login") // 登录的controller路径
.defaultSuccessUrl("/test/index").permitAll() // 登录成功之后的跳转路径
.and().authorizeRequests()
.antMatchers("/", "/user/test", "/user/login").permitAll() //这些路径无需认证
.anyRequest().authenticated() // 所有路径都要认证
.and().csrf().disable(); // 关闭csrf防护
}
- 编写login.html
注:表单中用户名和密码必须定义为username、password。原因如下图:
如果想要改变表单中的传参,需要重写该方法。
- 测试
访问user/test不需要认证
访问user/index需要认证
用户授权
基于权限访问控制
hasAuthority
如果当前主体具有指定权限,就返回true,否则返回false。(一般只针对一个角色操作)
hasAnyAuthority
如果当前主体具有多个权限中的某一个权限(角色中间用逗号分隔开),就返回true。(针对多个角色的操作)
基于角色进行权限控制
hasRole
如果当前主体具有指定角色,就返回true。(一般只针对一个角色操作)
hasAnyRole
如果当前主体具有多个角色中的某一个角色,就返回true。(针对多个角色的操作)
自定义403(没有权限)界面
当上述两种权限不够的时候,就会返回403.
自定义配置:
基于注解的认证授权
@Secure:用户具有某个角色,才能访问该方法
- 需要先在启动类(或者配置类)开启权限注解。
@EnableGlobalMethodSecurity(securedEnabled = true)
@PreAuthorize:方法前拦截判断是否有权限
需要先在启动类(或者配置类)开启权限注解。
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PostAuthorize:方法执行后再判断是否有权限(适合验证带有返回值的权限)
一般此注解用的比较少
需要先在启动类(或者配置类)开启权限注解。
@EnableGlobalMethodSecurity(prePostEnabled = true)
测试结果:如果用户没有权限,会先输出“hello,index”,但不会返回hello。而会返回403(没有权限)
注销
在配置类中添加退出映射地址
// 配置注销
http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll();
自动登录
自动登录流程:
csrf
csrf:跨域请求伪造,一种恶意的攻击方法。