在web应用开发中,安全无疑是十分重要的,选择Spring Security来保护web应用是一个非常好的选择。Spring Security 是spring项目之中的一个安全模块,可以非常方便与spring项目无缝集成。特别是在spring boot项目中加入spring security更是十分简单。本篇我们介绍spring security,以及spring security在web应用中的使用。
Spring Security是一个高度自定义的安全框架。利用Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。
使用Spring Secruity的原因有很多,但大部分都是发现了javaEE的Servlet规范或EJB规范中的安全功能缺乏典型企业应用场景。同时认识到他们在WAR或EAR级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用Spring Security 解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。
Spring Security是Spring项目组提供的安全服务框架,核心功能包括认证和授权。它为系统提供了声明式安全访问控制功能,减少了 为系统安全而编写大量重复代码的工作。
Spring Security 以“The Acegi Secutity System for Spring” 的名字始于2003年年底。其前身为acegi项目。起因是Spring开发者邮件列表中一个问题,有人提问是否考虑提供一个基于Spring的安全实现。限制于时间问题,开发出了一个简单的安全实现,但是并没有深入研究。几周后,Spring社区中其他成员同样询问了安全问题,代码提供给了这些人。2004年1月份已经有20人左右使用这个项目。随着更多人的加入,在2004年3月左右在sourceforge中建立了一个项目。在最开始并没有认证模块,所有的认证功能都是依赖容器完成的,而acegi则注重授权。但是随着更多人的使用,基于容器的认证就显现出了不足。acegi中也加入了认证功能。大约1年后acegi成为Spring子项目。在2006年5月发布了acegi 1.0.0版本。2007年底acegi更名为Spring Security。
Spring Security 的核心功能主要包括如下几个:
在身份验证层面,Spring Security广泛支持各种身份认证模式。这些验证模式绝大多数都是由第三方提供,或相关标准组织提供的,目前Spring Security支持如下认证技术:
HTTP BASIC authentication headers
: 一个基于IEFT RFC的标准。HTTP Digest authentication headers
: 一个基于IETF RFC的标准。HTTP X.509 client certificate exchange
: 一个基于IETF RFC的标准。LDAP
: 一种常见的跨平台身份验证方式。Form-based authentication
: 用于简单的用户界面需求。OpenID authentication
: 一种去中心化的身份认证方式。Authentication based on pre-established request headers
: 类似于 Computer Associates SiteMinder,一种用户身份验证及授权的集中式安全基础方案。Jasig Central Authentication Service
: 单点登录方案。Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker
: 一个Spring远程调用协议。Automatic "remember-me" authentication
: 允许在指定到期时间前自行重新登录系统。Anonymous authentication
: 允许匿名用户使用特定的身份安全访问资源。Run-as authentication
: 允许在一个会话中变换用户身份的机制。Java Authentication and Authorization Service
: JAAS,Java验证和授权API.Java EE container authentication
: 允许系统继续使用容器管理这种身份验证方式。Kerberos
: 一种使用对称密钥机制,允许客户端与服务器相互确认身份的认证协议。准备一个名为 mysecurity 的Mysql数据库
创建SpringBoot项目,添加依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.0version>
dependency>
<dependency>
<groupId>org.springframework.sessiongroupId>
<artifactId>spring-session-data-redisartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
编写配置文件
server:
port: 80
#日志格式
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'
# 数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql:///mysecurity?serverTimezone=UTC
username: root
password01: root
在 template 文件夹编写项目主页面 main.html
DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>主页面title>
head>
<body>
<h1>主页面h1>
body>
html>
编写访问页面控制器
@Controller
public class PageController {
@RequestMapping("/{page}")
public String showPage(@PathVariable String page){
return page;
}
}
启动项目,访问项目主页面http://localhost/main,项目会自动跳转到一个登录页面。这代表Spring Security已经开启了认证功能,不登录无法访问所有资源,该页面就是Spring Security 自带的登录页面。 我们使用 user 作为用户名,控制台中的字符串作为密码登录,登录成功后跳转到项目主页面。
在实际开发中,用户数量不会只有一个,且密码是自己设置的。所以我们需要自定义配置用户信息。首先我们在内存中创建两个用户,Spring Security会将登录页传来的用户名密码和内存中用户名密码做匹配认证。
// Security配置类
@Configuration
public class SecurityConfig {
// 定义认证逻辑
@Bean
public UserDetailsService userDetailsService(){
// 1.使用内存数据进行认证
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 2.创建两个用户
UserDetails user1 = User.withUsername("lt").password("123").authorities("admin").build();
UserDetails user2 = User.withUsername("lt2").password("456").authorities("admin").build();
// 3.将这两个用户添加到内存中
manager.createUser(user1);
manager.createUser(user2);
return manager;
}
//密码编码器,不解析密码
@Bean
public PasswordEncoder passwordEncoder()
{
return NoOpPasswordEncoder.getInstance();
}
}
此时进行认证测试,我们可以将登录页传来的用户名密码和内存中 用户名密码做匹配认证。
在实际项目中,认证逻辑是需要自定义控制的。将 UserDetailsService 接口的实现类放入Spring容器即可自定义认证逻辑。 InMemoryUserDetailsManager 就是 UserDetailsService 接口的一个实现类,它将登录页传来的用户名密码和内存中用户名密码做匹配认证。当然我们也可以自定义 UserDetailsService 接口的实现类。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService 的实现类必须重写 loadUserByUsername 方法,该方法定义了 具体的认证逻辑,参数 username 是前端传来的用户名,我们需要根据传来的用户名查询到该用户(一般是从数据库查询),并将查询到的用户封装成一个UserDetails对象,该对象是Spring Security提供的用户对象,包含用户名、密码、权限。Spring Security会根据 UserDetails对象中的密码和客户端提供密码进行比较。相同则认证通过,不相同则认证失败。
准备数据库数据
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255),
`password` varchar(255) ,
`phone` varchar(255) ,
PRIMARY KEY (`id`)
);
INSERT INTO `users` VALUES (1, 'lt','lt1', '15012345678');
INSERT INTO `users` VALUES (2, '蓝天','lt2', '15012345678');
编写用户实体类
@Data
public class Users {
private Integer id;
private String username;
private String password;
private String phone;
}
编写dao接口
public interface UsersMapper extends BaseMapper<Users> {}
在SpringBoot启动类中添加 @MapperScan 注解,扫描Mapper文件夹
@SpringBootApplication
@MapperScan("com.lt.myspringsecurity.mapper")
public class MysecurityApplication {
public static void main(String[] args)
{
SpringApplication.run(MysecurityApplication.class, args);
}
}
创建 UserDetailsService 的实现类,自定义认证逻辑
@Service
public class MyUserDetailsService
implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
// 自定义认证逻辑
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.构造查询条件
QueryWrapper<Users> wrapper = new QueryWrapper<Users>().eq("username",username);
// 2.查询用户
Users users = usersMapper.selectOne(wrapper);
// 3.封装为UserDetails对象
UserDetails userDetails = User
.withUsername(users.getUsername())
.password(users.getPassword())
.authorities("admin")
.build();
// 4.返回封装好的UserDetails对象
return userDetails;
}
}
测试连接数据库认证
在实际开发中,为了数据安全性,在数据库中存放密码时不会存放 原密码,而是会存放加密后的密码。而用户传入的参数是明文密 码。此时必须使用密码解析器才能将加密密码与明文密码做比对。 Spring Security中的密码解析器是 PasswordEncoder 。
Spring Security要求容器中必须有 PasswordEncoder 实例,之前使用的
NoOpPasswordEncoder 是 PasswordEncoder 的实现类,意思是不解析密码,使用明文密码。
Spring Security官方推荐的密码解析器是 BCryptPasswordEncoder。
@SpringBootTest
public class PasswordEncoderTest {
@Test
public void testBCryptPasswordEncoder(){
//创建解析器
PasswordEncoder encoder = new BCryptPasswordEncoder();
//密码加密
String password = encoder.encode("lt1");
System.out.println("加密后:"+password);
//密码校验
/**
* 参数1:明文密码
* 参数2:加密密码
* 返回值:是否校验成功
*/
boolean result = encoder.matches("lt1","$2a$10$/MImcrpDO21HAP2amayhme8j2SM0YM50");
System.out.println(result);
}
}
在开发中,我们将 BCryptPasswordEncoder 的实例放入Spring容器即可,并 且在用户注册完成后,将密码加密再保存到数据库。
//密码编码器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
虽然Spring Security给我们提供了登录页面,但在实际项目中,更多的是使用自己的登录页面。Spring Security也支持用户自定义登录页面。用法如下:
编写登录页面
在Spring Security配置类自定义登录页面
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
//Spring Security配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义表单登录
http.formLogin()
.loginPage("/login.html") //自定义登录页面
.usernameParameter("username")// 表单中的用户名项
.passwordParameter("password")// 表单中的密码项
.loginProcessingUrl("/login")
// 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
.successForwardUrl("/main")//登录成功后跳转的路径
.failureForwardUrl("/fail");//登录失败后跳转的路径
// 需要认证的资源
http.authorizeRequests().antMatchers("/login.html").permitAll()
//登录页不需要认证
.anyRequest().authenticated();
//其余所有请求都需要认证
//关闭csrf防护
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 静态资源放行
web.ignoring().antMatchers("/css/**");
}
}
CSRF防护
CSRF:跨站请求伪造,通过伪造用户请求访问受信任的站点从而进行非法请求访问,是一种攻击手段。 Spring Security 为了防止CSRF攻击,默认开启了CSRF防护,这限制了除了 GET请求以外的大多数方法。我们要想正常使用Spring Security需要突破CSRF防护。解决方法如下:
方法一:关闭CSRF防护
http.csrf().disable();
方法二:突破CSRF防护
CSRF为了保证不是其他第三方网站访问,要求访问时携带参 数名为_csrf值为令牌,令牌在服务端产生,如果携带的令牌 和服务端的令牌匹配成功,则正常访问。
<form class="form" action="/login" method="post">
<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
<input type="text" placeholder="用户名" name="username">
<input type="password" placeholder="密码" name="password">
<button type="submit">登录button>
form>
用户认证通过后,有时我们需要获取用户信息,比如在网站顶部显示:欢迎您,XXX。Spring Security将用户信息保存在会话中,并提供会话管理,我们可以从 SecurityContext 对象中获取用户信息, SecurityContext 对象与当前线程进行绑定。获取用户信息的写法如下:
@RestController
public class MyController {
// 获取当前登录用户名
@RequestMapping("/users/username")
public String getUsername(){
// 1.获取会话对象
SecurityContext context = SecurityContextHolder.getContext();
// 2.获取认证对象
Authentication authentication = context.getAuthentication();
// 3.获取登录用户信息
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return userDetails.getUsername();
}
}
登录成功后,如果除了跳转页面还需要执行一些自定义代码时, 如:统计访问量,推送消息等操作时,可以自定义登录成功处理器。
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 拿到登录用户的信息
UserDetails userDetails = (UserDetails)authentication.getPrincipal();
System.out.println("用户名:"+userDetails.getUsername());
System.out.println("一些操作...");
// 重定向到主页
response.sendRedirect("/main");
}
}
http.formLogin() // 使用表单登录
.loginPage("/login.html") // 自定义登录页面
.usernameParameter("username") // 表单中的用户名项
.passwordParameter("password") // 表单中的密码项
.loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行 UserDetailsService的方法
//.successForwardUrl("/main") //登录成功后跳转的路径
.successHandler(new MyLoginSuccessHandler()) //登录成功处理器
.failureForwardUrl("/fail"); //登录失败后跳转的路径
登录失败后,如果除了跳转页面还需要执行一些自定义代码时, 如:统计失败次数,记录日志等,可以自定义登录失败处理器。
自定义登录失败处理器
public class MyLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
System.out.println("记录失败日志...");
response.sendRedirect("/fail");
}
}
配置登录失败处理器
http.formLogin() // 使用表单登录
.loginPage("/login.html") // 自定义登录页面
.usernameParameter("username") // 表单中的用户名项
.passwordParameter("password") // 表单中的密码项
.loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
//.successForwardUrl("/main") //登录成功后跳转的路径
.successHandler(new MyLoginSuccessHandler()) //登录成功处理器
//.failureForwardUrl("/fail") //登录失败后跳转的路径
.failureHandler(new MyLoginFailureHandler()); //登录失败处理器
// 需要认证的资源
http.authorizeRequests()
.antMatchers("/login.html").permitAll() //登录页不需要认证
.antMatchers("/fail").permitAll() //失败页不需要认证
.anyRequest().authenticated(); //其余所有请求都需要认证
在系统中一般都有退出登录的操作。退出登录后,Spring Security 进行了以下操作:
在Spring Security中,退出登录的写法如下:
配置退出登录的路径和退出后跳转的路径
// 退出登录配置
http.logout()
.logoutUrl("/logout") // 退出登录路径
.logoutSuccessUrl("/login.html") // 退出登录后跳转的路径
.clearAuthentication(true) //清除认证状态,默认为true
.invalidateHttpSession(true); // 销毁HttpSession对象,默认为true
在网页中添加退出登录超链接
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>主页面</title>
</head>
<body>
<h1>主页面</h1>
<a href="/logout">退出登录</a>
</body>
</html>