学习 Spring Security 的前置知识:
- Spring 框架
- Spring Boot 框架
- Java Web
一、Spring Security 介绍
1、概要
Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。
正如你可能知道的关于安全方面的两个主要区域是”认证“和”授权“(或者访问控制),一般来说,Web 应用安全性包括用户认证和用户授权两个部分,这两点也是 Spring Security 重要核心功能。
(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点就是说系统认为用户是否能登录。
(2)用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说系统会为不同的用户分配不同的角色,而每个角色对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。
2、特点
Spring Security 特点:
- 和 Spring 无缝整合
- 全面的权限控制
- 专门为 Web 开发而设计
- 重量级
一般来说,常见的安全管理技术栈的组合是这样的:
- SSM + Shiro
- Spring Boot/Spring Cloud + Spring Security
以上只是推荐的组合而已,如果单从技术来说,无论怎么组合,都是可以运行的。
二、Spring Security 的入门案例
1、项目环境搭建
第一步:
创建一个 Spring Boot 工程
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/>
</parent>
第二步:
引入相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
第三步:
编写 Controller 进行测试
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String hello() {
return "Hello Spring Security ";
}
}
第四步:
进行访问
访问结果为
2、基本原理
2.1、过滤器链
Spring Security 本质是一个过滤器链。
Debug 走一遍,看源码,重点关注三个过滤器。(查找类快捷键:Ctrl + Shift + N)
(1)FilterSecurityInterceptor
:是一个方法级别的权限过滤器,位于过滤链的最底部。
源码如下:
// 实现了 Filter 接口
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
// 业务逻辑
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
this.invoke(fi);
}
// 业务逻辑调用的方法
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);
}
}
(2)ExceptionTranslationFilter
:是个异常过滤器,用来处理在认证授权过程中抛出的异常。
源码如下:
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);
}
}
(3)UsernamePasswordAuthenticationFilter
:对 /login 的 post 请求进行拦截,校验表单用户名,密码。
源码如下:
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
2.2、过滤器加载过程
2.3 两个重要的接口
(1)UserDetailsService
当什么都没有配置的时候,账号和密码都是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所有我们要通过自定义逻辑控制认证逻辑。
如果需要自定义逻辑时,只需要实现 UserDetailsService
接口即可。接口定义如下:
(2)PasswordEncoder
三、Web 权限
1、用户认证
1.1 设置用户名密码
第一种方式:通过配置文件
在 application.properties
配置文件中配置:
spring.security.user.name=yanghui
spring.security.user.password=yanghui
第二种方式:通过配置类
继承 WebSecurityConfigurerAdapter
类,然后重写 configure()
方法
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 进行加密
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("yang");
auth.inMemoryAuthentication().withUser("yang").password(password).roles("admin");
}
/**
* 需要用到这个,否则会报错
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
第三种方式:自定义编写实现类(常用)
编写类实现 UserDetailsService
接口,实现 loadUserByUsername()
方法
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> authorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("yanghui",
new BCryptPasswordEncoder().encode("yanghui"), authorities);
}
}
Config
配置类:
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 需要用到这个,否则会报错
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
1.2 查询数据库完成认证
第一步:引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
第二步:创建数据库表
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`id` int NOT NULL,
`username` varchar(50) NOT NULL,
`password` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
第三步:创建表对应的实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class Users {
private Integer id;
private String username;
private String password;
}
第四步:整合 MybatisPlus,创建接口
@Mapper
public interface UserMapper extends BaseMapper<Users> {
}
第五步:开始使用(在 MyUserDetailsService
类里面查询数据库)
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 调用 UserMapper 中的方法查询数据库(根据用户名)
QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
Users user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
List<GrantedAuthority> authorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(user.getUsername(),
new BCryptPasswordEncoder().encode(user.getPassword()), authorities);
}
}
第六步:配置数据库
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=1234567890
第七步:启动项目,开始测试
1.3 自定义用户登录界面
在 Spring Security 配置类中重写 configure
方法
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 自定义自己编写的登录页面
* loginPage:登录页面路径
* loginProcessingUrl:登录访问路径
* defaultSuccessUrl:登录成功之后跳转路径
* csrf:关闭 csrf 防护
* 注意:路径前面必须要加 / ,否则会报错
*/
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/test/index").permitAll()
.and().authorizeRequests()
.antMatchers("/", "/test/hello", "/user/login").permitAll()
.anyRequest().authenticated()
.and().csrf().disable();
}
2、用户授权
2.1 基于权限访问控制
hasAuthority() 方法
:如果当前的主体具有指定的权限,则返回 true,否则返回 false。
第一步:在 Spring Security 配置类设置当前访问地址有哪些权限
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 自定义自己编写的登录页面
* loginPage:登录页面路径
* loginProcessingUrl:登录访问路径
* defaultSuccessUrl:登录成功之后跳转路径
* csrf:关闭 csrf 防护
* 注意:路径前面必须要加 / ,否则会报错
*/
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/test/index").permitAll()
.and().authorizeRequests()
.antMatchers("/", "/test/hello", "/user/login").permitAll()
// 当前登录用户,只有具有 admin 权限才可以访问这个路径
.antMatchers("/test/index").hasAuthority("admin")
.anyRequest().authenticated()
.and().csrf().disable();
}
第二步:在 MyUserDetailsService
类中给用户添加权限
List<GrantedAuthority> authorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
第三步:进行测试
hasAnyAuthority() 方法
,如果当前的主体有多个权限,用逗号分隔,设置多个权限。
2.2 基于角色访问控制
hasRole() 方法
:如果用户具备给定角色就允许访问,否则出现 403。如果当前主体具有指定的角色,则返回 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 + "'");
}
// 会帮我们加上一个前缀 ROLE_
return "hasRole('ROLE_" + role + "')";
}
第一步:在配置类中配置(与权限类似)
.antMatchers("/test/index").hasRole("sale")
第二步:在 MyUserDetailsService
类中配置
// 注意这个 ROLE_ 前缀
List<GrantedAuthority> authorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
2.3 自定义 403 页面
在 Spring Security 配置类中进行配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 配置没有权限跳转的页面
*/
http.exceptionHandling().accessDeniedPage("/error.html");
}
2.4 注解的使用
四个注解
首先开启注解功能,在 Spring Security 配置类或者 启动类上添加:
@EnableGlobalMethodSecurity(securedEnabled = true)
@Secured
,用户具有某个角色,可以访问方法,否则不能访问
@Secured({"ROLE_sale", "ROLE_manager"})
@GetMapping("/update")
public String update() {
return "hello index";
}
@PreAuthorize
,适合进入方法前的权限验证
@PreAuthorize("hasAnyAuthority('admin')")
@GetMapping("/update")
public String update() {
return "hello index";
}
@PostAuthorize
,使用并不多,在方法执行之后再进行权限验证
@PostAuthorize("hasAnyAuthority('admin')")
@GetMapping("/update")
public String update() {
return "hello index";
}
@PostFilter
,方法返回数据进行过滤@PreFilter
,传入方法数据进行过滤
2.5 用户注销
在 Spring Security 配置类中配置:
/**
* logoutUrl:注销的地址
* logoutSuccessUrl:注销后要跳转的地址
*/
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/").permitAll();
2.5 实现记住我
具体实现:
第一步:建表 SQL(Spring Security 提供的,在 JdbcTokenRepositoryImpl
类 37 行
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
第二步:在 Spring Security 配置类中添加:
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
第三步:在 configure() 方法中配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60).userDetailsService(userDetailsService);
}
第四步:在登录页面中添加表单项,name 必须为 remember-me
<input type="checkbox" name="remember-me"/>自动登录