三、SpringBoot Security 快速上手
Spring-boot-Security: 基于Spring Boot整合的快速实现。
1、项目搭建步骤
1、创建maven工程。
父工程我们依然使用上面示例中的同一个父工程。
创建子模块spring-boot-security 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>
<artifactId>AuthDemo</artifactId>
<groupId>com.tuling</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.tuling</groupId>
<artifactId>spring-boot-security</artifactId>
<version>0.0.1</version>
<name>spring-boot-security</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot-version}</version>
</plugin>
</plugins>
</build>
</project>
2、 在resources目录下创建application.properties。 --spring security不需要任何配置就可以直接启动
server.port=8080
spring.application.name=security-springboot
3、创建启动类,注意我们在启动类中,引入了一个Spring Security提供的注解@EnableWebSecurity。
package com.tuling.springbootsecurity;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
@SpringBootApplication
@EnableWebSecurity
public class SpringBootSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootSecurityApplication.class, args);
}
}
4、创建几个简单的资源访问接口
package com.tuling.springbootsecurity.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/mobile")
public class MobileController {
@GetMapping("/query")
public String query(){
return "mobile";
}
}
package com.tuling.springbootsecurity.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/salary")
public class SalaryController {
@GetMapping("/query")
public String query(){
return "salary";
}
}
到这一步呢,我们就完成了一个SpringBoot工程的基础搭建。然后我们就可以启动引用访问MobileController和SalaryController的资源了,这时就会发现,访问这两个资源会转到一个登录页面,要求先登录。登录的用户名是user,密码会在日志中打印。
2、用SpringBoot Security重新实现我们上个应用的认证和授权逻辑。
5、注入免密解析器PasswordEncoder和用户来源UserDetailsService
package com.tuling.springbootsecurity.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyWebConfig implements WebMvcConfigurer {
//默认Url根路径跳转到/login,此url为spring security提供
@Override
public void addViewControllers(ViewControllerRegistry registry)
{
registry.addViewController("/").setViewName("redirect:/login");
}
/**
* 自行注入一个PasswordEncoder。
* @return
*/
@Bean
public PasswordEncoder getPassWordEncoder(){
return new BCryptPasswordEncoder(10);
// return NoOpPasswordEncoder.getInstance();
}
/**
* 自行注入一个UserDetailsService
* 如果没有的话,在UserDetailsServiceAutoConfiguration中会默认注入一个包含user用户的InMemoryUserDetailsManager
* 另外也可以采用修改configure(AuthenticationManagerBuilder auth)方法并注入authenticationManagerBean的方式。
* @return
*/
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(User.withUsername("admin").password(passwordEncoder().encode("admin")).authorities("mobile","salary").build(),
User.withUsername("manager").password(passwordEncoder().encode("manager")).authorities("salary").build(),
User.withUsername("worker").password(passwordEncoder().encode("worker")).authorities("worker").build());
return userDetailsManager;
// return new JdbcUserDetailsManager(DataSource dataSource);
}
}
6、注入校验配置规则:
package com.tuling.springbootsecurity.config;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* 注入一个自定义的配置
*/
@EnableWebSecurity
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
//配置安全拦截策略
@Override
protected void configure(HttpSecurity http) throws Exception {
//链式配置拦截策略
http.csrf().disable()//关闭csrg跨域检查
.authorizeRequests()
.antMatchers("/mobile/**").hasAuthority("mobile") //配置资源权限
.antMatchers("/salary/**").hasAuthority("salary")
.antMatchers("/common/**").permitAll() //common下的请求直接通过
.anyRequest().authenticated() //其他请求需要登录
.and() //并行条件
.formLogin().defaultSuccessUrl("/main.html").failureUrl("/common/loginFailed"); //可从默认的login页面登录,并且登录后跳转到main.html
}
}
7、获取当前用户信息:Spring Security提供了多种获取当前用户信息的方法。
package com.tuling.springbootsecurity.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
@RestController
@RequestMapping("/common")
public class LoginController {
@GetMapping("/getLoginUserByPrincipal")
public String getLoginUserByPrincipal(Principal principal){
return principal.getName();
}
@GetMapping(value = "/getLoginUserByAuthentication")
public String currentUserName(Authentication authentication) {
return authentication.getName();
}
@GetMapping(value = "/username")
public String currentUserNameSimple(HttpServletRequest request) {
Principal principal = request.getUserPrincipal();
return principal.getName();
}
@GetMapping("/getLoginUser")
public String getLoginUser(){
User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return user.getUsername();
}
}
然后我们把前台页面移植过来。
这样,一个简单的Spring Secuity工程就配置完成了。我们来简单总结下。
1、我们可以通过注入一个PasswordEncoder对象来实现密码加密。其中,NoOpPasswordEncoder是一个已过时的加密器,他不会对密码进行任何加密操作。而实际项目中,最常用的是BCryptPasswordEncoder。
2、我们通过注入一个UserDetailsService来管理系统的实体数据。如果我们不自己注入UserDetailsService,那在UserDetailsServiceAutoConfiguration中会默认注入一个包含user用户的UserDetailsService,user用户的密码会打印在控制台日志中。而除了我们系统中使用到的InMemoryUserDetailsManager外,SpringSecurity还提供了JdbcUserDetailsManager来实现对对数据库中的用户数据管理。
另外,关于用户数据来源,可以通过覆盖WebSecurityConfigurerAdapter中的configure(AuthenticationManagerBuilder auth)方法,并注入authenticationManagerBean()的方式进行干预。
3、目前示例中的权限规则都是从内存直接写死的,实际项目中显然都是要从数据库进行加载。而且,目前我们的规则都是基于web请求路径来定制的,而Spring Security实际上还提供了基于注解的方法级别规则配置。
3、项目测试
这样就可以启动任务进行测试了。启动后,可以访问security默认提供的登录页面 http://localhost:8080/login
然后就可以使用之前创建的三个用户分别登陆,登陆后进入测试主页面。
测试页面中,登出 使用的是Security框架提供的默认登出地址 /logout。分别访问mobile和salary下的服务可以看到权限有控制。
4、了解SpringBoot Security项目的扩展点
这样,一个基本的spring-boot-security项目就很快搭建起来了。而Spring Security实际上还提供了相当丰富的扩展点,包括用户名密码校验规则、资源校验规则、Session管理规则等。我们需要了解这些扩展点,这样才能在实际项目中,运用上Spring Security。
1、主体数据来源
SpringSecurity通过引用Spring容器中的UserDetailsService对象来管理主体数据。默认情况下,会注入一个包含user用户的默认主体管理服务。我们演示中就通过注入一个InMemoryUserDetailsManager对象覆盖了默认的主体管理器。
实际项目中的用户信息大都会来自于数据库。在SpringSecurity中,也提供了JdbcUserDetailsManager来实现对数据库的用户信息进行管理。而如果这些不满足实际需求,可以通过自己实现一个UserDetailsService对象并注入到Spring容器中,来实现自定义的主体数据管理。
2、密码解析器
Spring Security提供了很多密码解析器,包括CryptPassEncoder、Argon2PasswordEncoder、Pbkdf2PasswordEncoder等,具体可以参看PassEncoder接口的实现类。其中最常用的一般就是BCryptPasswordEncoder。其中要注意的是,我们在选择不同的密码解析器后,后台存储用户密码时要存储对应的密文。
3、自定义授权及安全拦截策略
最常规的方式是通过覆盖WebSecurityConfigurerAdapter中的protected void configure(HttpSecurity http)方法。通过http来配置自定义的拦截规则。包含访问控制、登录页面及逻辑、退出页面及逻辑等。
自定义登录:http.loginPage()方法配置登录页,http.loginProcessingUrl()方法定制登录逻辑。要注意的是,SpringSecurity的登录页和登录逻辑是同一个地址/login,如果使用自定义的页面,需要将登录逻辑地址也分开。例如: http.loginPage("/index.html").loginProcessingUrl("/login")。
而登录页面的一些逻辑处理,可以参考系统提供的默认登录页。但是这里依然要注意登录页的访问权限。而关于登录页的源码,可以在DefaultLoginPageGeneratingFilter中找到。
记住我功能:登录页面提供了记住我功能,此功能只需要往登录时提交一个remeber-me的参数,值可以是 on 、yes 、1 、 true,就会记住当前登录用户的token到cookie中。http.rememberMe().rememberMeParameter("remeber-me"),使用这个配置可以定制参数名。而在登出时,会清除记住我功能的cookie。
拦截策略:antMachers()方法设置路径匹配,可以用两个星号代表多层路径,一个星号代表一个或多个字符,问号代表一个字符。然后配置对应的安全策略:
permitAll()所有人都可以访问。denyAll()所有人都不能访问。 anonymous()只有未登录的人可以访问,已经登录的无法访问。
hasAuthority、hasRole这些是配置需要有对应的权限或者角色才能访问。 其中,角色就是对应一个ROLE_角色名 这样的一个资源。
另外的两个配置对象中,AuthenticationManagerBuilder配置认证策略,WebSecurity配置补充的Web请求策略。
4、关于csrf
csrf全称是Cross—Site Request Forgery 跨站点请求伪造。这是一种安全攻击手段,简单来说,就是黑客可以利用存在客户端的信息来伪造成正常客户,进行攻击。例如你访问网站A,登录后,未退出又打开一个tab页访问网站B,这时候网站B就可以利用保存在浏览器中的sessionId伪造成你的身份访问网站A。
我们在示例中是使用http.csrf().disable()方法简单的关闭了CSRF检查。而其实Spring Security针对CSRF是有一套专门的检查机制的。他的思想就是在后台的session中加入一个csrf的token值,然后向后端发送请求时,对于GET、HEAD、TRACE、OPTIONS以外的请求,例如POST、PUT、DELETE等,会要求带上这个token值进行比对。
当我们打开csrf的检查,再访问默认的登录页时,可以看到在页面的登录form表单中,是有一个name为csrf的隐藏字段的,这个就是csrf的token。例如我们在freemarker的模板语言中可以使用添加这个参数。
而在查看Spring Security后台,有一个CsrfFilter专门负责对Csrf参数进行检查。他会调用HttpSessionCsrfTokenRepository生成一个CsrfToken,并将值保存到Session中。
5、注解级别方法支持 : 在@Configuration支持的注册类上打开注解@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)即可支持方法及的注解支持。prePostEnabled属性 对应@PreAuthorize。securedEnabled 属性支持@Secured注解,支持角色级别的权限控制。jsr250Enabled属性对应@RolesAllowed注解,等价于@Secured。
6、异常处理:现在前后端分离的状态可以使用@ControllerAdvice注入一个异常处理类,以@ExceptionHandler注解声明方法,往前端推送异常信息。