前言:
百度百科中是这样解释Spring Security的:
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
对于我的理解:
Spring Security可以集中一个权限控制系统,可以用来保护 Web 应用的安全;
核心功能是:
- 认证(你是谁)
- 授权(你能干什么)
- 攻击防护(防止伪造身份)
集成Spring Security步骤:
一、maven中添加依赖
<!--spring-boot-security安全框架SpringSecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
这个是最首要,也是最关键的;配置好后,直接运行项目,打开浏览器,访问项目上的接口时,你会发现,访问不了,直接进入了一个登入页面,如下图:
原因:因为在SpringBoot中,默认的Spring Security就是生效了的,此时的接口都是被保护的,我们需要通过验证才能正常的访问。
Spring Security提供了一个默认的用户,用户名是user,而密码则是启动项目的时候自动生成的。
我们查看项目启动的日志,会发现如下的一段Log:
每个人看到的password肯定是不一样的,我们直接用user和启动日志中的密码进行登录。
登录成功后,就跳转到了接口正常调用的页面了。
如果你不想一开始就使能Spring Security,怎么办呢;
之前有人告诉我,在application配置中直接加:security.basic.enabled=false 即可;
不行!不行!不行!是真的不行呀,根本没有关闭Spring Security,查了下资料:
只有在spring boot1.5配置security关闭http基本验证,可以在application配置中增加;security.basic.enabled=false进行关闭,但是在spring boot 2.0+之后这样配置就不能生效了,下面的配置文件讲解中会降到怎么进行关闭,^- ^;
二、对SpringSecurity进行相应的配置
新建配置类:SecurityConfig,继承WebSecurityConfigurerAdapter类,然后重写父类中的configure(HttpSecurity http) 方法。如下
/**
* @author AxeLai
* @date 2019-04-30 15:15
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置需要登入验证
http.formLogin() // 定义当需要用户登录时候,转到的登录页面。
.and()
.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.anyRequest() // 任何请求,登录后可以访问
.authenticated();
//配置不需要登入验证
// http.authorizeRequests()
// .anyRequest()
// .permitAll()
// .and()
// .logout()
// .permitAll();
}
}
上面代码中,注释‘配置需要登入验证’,打开‘配置不需要登入验证’就解决了security的关闭问题^- ^.
如果你只是为了测试Security的安全验证功能,上面这些就足够了;但是,如果需要进行登入的用户身份认证,权限设置等,还就需要接着往下看了:
三、配置用户认证逻辑
因为我们是要有自己的一套用户体系的,所以要配置用户认证:
新建LoginFilter配置类,继承UserDetailsService类:
package com.example.demo.loginfilter;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author AxeLai
* @date 2019-05-01 14:01
*/
@Component
public class LoginFilter implements UserDetailsService {
private Logger logger = LoggerFactory.getLogger("adminLogger");
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("用户的用户名: {}", username);
// TODO 根据用户名,查找到对应的密码,与权限
// 封装用户信息,并返回。参数分别是:用户名,密码,用户权限
User user = new User(username, "123456",
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
return user;
}
}
这里我们没有进行过多的校验,用户名可以随意的填写,但是密码必须得是“123456”,这样才能登录成功。
同时可以看到,这里User对象的第三个参数,它表示的是当前用户的权限,我们将它设置为”admin”。
运行一下程序进行测试
输入随意的用户和密码123456,发现根本登入不了;
后台报错:java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
下面说下原因:
这是因为Spring boot 2.0.3引用的security 依赖是 spring security 5.X版本,此版本需要提供一个PasswordEncorder的实例,否则后台汇报错误:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
并且页面毫无响应。
更改:
把SecurityConfig改为:
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author AxeLai
* @date 2019-04-30 15:15
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
//当然,如果你有自己的加密方法,这个方法就写自己的加密方法好了
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置需要登入验证
http.formLogin() // 定义当需要用户登录时候,转到的登录页面。
.and()
.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.anyRequest() // 任何请求,登录后可以访问
.authenticated();
//配置不需要登入验证
// http.authorizeRequests()
// .anyRequest()
// .permitAll()
// .and()
// .logout()
// .permitAll();
}
}
把LoginFilter改为:
package com.example.demo.loginfilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author AxeLai
* @date 2019-05-01 14:01
*/
@Component
public class LoginFilter implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
private Logger logger = LoggerFactory.getLogger("adminLogger");
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("用户的用户名: {}", username);
// TODO 根据用户名,查找到对应的密码,与权限
// 封装用户信息,并返回。参数分别是:用户名,密码,用户权限
User user = new User(username, passwordEncoder.encode("123456"),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
return user;
}
}
重启项目,随便的用户名+密码123456登入,你就发现登入已经成功了 ,直接进入我们项目的首页,后台会获取到你的用户名:
讲解:
1.当我们写LoginFilter 类时;里面实现了一个方法loadUserByUsername,并返回了一个UserDetails。这个UserDetails 就是封装了用户信息的对象,里面包含了七个方法:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
我们在返回UserDetails的实现类User的时候,可以通过User的构造方法,设置对应的参数就可以;
2.SpringSecurity中有一个PasswordEncoder接口:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
我们只需要自己实现这个接口,并在配置文件中配置一下就可以了。
上面我只是暂时以默认提供的一个实现类进行测试:
@Bean
public PasswordEncoder passwordEncoder(){
//当然,如果你有自己的加密方法,这个方法就写自己的加密方法好了
return new BCryptPasswordEncoder();
}
四、个性化用户认证逻辑
在上面的测试中,一直都是使用的默认的登录界面,我相信每个产品都是有自己的登录界面设计的,所以我们这一节了解一下如何自定义登录页面。
我们先写一个简单的登录页面login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<h2>自定义登录页面</h2>
<form action="/user/login" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
</body>
</html>
完成了登录页面之后,就需要将它配置进行SecurityConfig进行更改:
//配置需要登入验证的自定义配置
http.formLogin() // 定义当需要用户登录时候,转到的登录页面。
.loginPage("/login.html") // 设置登录页面
.loginProcessingUrl("/user/login") // 自定义的登录接口
.and()
.authorizeRequests() // 定义哪些URL需要被保护、哪些不需要被保护
.antMatchers("/login.html").permitAll() // 设置所有人都可以访问登录页面
.anyRequest() // 任何请求,登录后可以访问
.authenticated()
.and()
.csrf().disable(); // 关闭csrf防护
这样,每当我们访问被保护的接口的时候,就会调转到login.html页面,如下:
最后附上项目结构图:
代码已上传到:https://github.com/AxeLai/SpringBoot-.git
OVER IS NOT OVER!
番外
上面这些,是在前后端不分离的情况下进行开发编写的,现如今一般项目都前后端分离了,后端提供接口供前端调用,返回JSON格式的数据给前端。刚才那样,调用了被保护的接口,直接进行了页面的跳转,这样执行不太友好。
处理:在前后端分离的情况下,我们登录成功了直接向前端返回用户的个人信息,而不是直接进行跳转。登录失败也是同样的道理。
这里涉及到了Spring Security中的两个接口AuthenticationSuccessHandler和AuthenticationFailureHandler。我们可以实现这个接口,并进行相应的配置就可以了。 当然框架是有默认的实现类的,我们可以继承这个实现类再来自定义自己的业务:
@Component("myAuthenctiationSuccessHandler")
public class MyAuthenctiationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,Authentication authentication) throws IOException, ServletException {
logger.info("登录成功");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
这里我们通过response返回一个JSON字符串回去。
这个方法中的第三个参数Authentication,它里面包含了登录后的用户信息(UserDetails),Session的信息,登录信息等。
@Component("myAuthenctiationFailureHandler")
public class MyAuthenctiationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
logger.info("登录失败");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new BaseResponse(exception.getMessage())));
}
}
这个方法中的第三个参数AuthenticationException,包括了登录失败的信息。
同样的,还是需要在配置文件中进行配置,这里就不贴出全部的代码了,只贴出相应的语句
.successHandler(myAuthenticationSuccessHandler) // 自定义登录成功处理
.failureHandler(myAuthenticationFailureHandler) // 自定义登录失败处理
OVER IS OVER!