1.介绍
Spring Security,是一种基于 Spring AOP 和 Servlet 过滤器的安全框架,它提供全面的安全性解决方案。Spring Security的核心是一系列的过滤器链,通过各种过滤器在 Web 请求级和方法调用级处理身份确认和授权。
Spring Security主要有两个核心功能:
- 登录认证
- 资源授权
登录认证主要是指判断当前登录用户在系统中是否合法,也就是对当前用户的登录名和密码进行系统级的校验。
资源授权主要是指用户是否有权限去做某一件事,在程序面上就是指方法的访问,以及资源、数据的获取。
2.常见过滤器介绍
上面说过了,Spring Security的核心原理其实就是一系列的过滤器,当请求发送到后台时,会通过过滤器对当前请求进行判断和处理,然后根据处理的结果进行不同页面或者方法的进入。下面对Spring Security常用的几个过滤器做一个简单的介绍。
2.1FilterSecurityInterceptor
FilterSecurityInterceptor位于过滤器链的最底部,主要功能是作为一个方法级的权限过滤器,对客户请求进行过滤和处理。
下面是FilterSecurityInterceptor的源码,当前Security版本为5.3.5.
过滤器的核心都是doFilter()方法,可以看到当前过滤器对传入的参数进行了接收,并调用了invoke()方法。
查看invoke()方法,可以当前前面是对一些数据进行了判断。
如果都不为空,首先调用了beforeInvocation()方法,这个方法主要的作用是去判断再调用这个过滤器之前的其它filter是否都正常通过,因为过滤器是链式的,若其中一个发生异常,则不会再去执行之后的过滤方法。
若前面都无问题,会调用fi.getChain().doFilter(fi.getRequest(), fi.getResponse())方法,这个方法才表示真正调用后台服务去处理请求。
2.2UsernamePasswordAuthenticationFilter
![](https://img-blog.csdnimg.cn/20201109103340441.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FfbGxsaw==,size_16,color_FFFFFF,t_70)
这个类主要是用来处理请求登录的。通过源码可以看到,若要使Security对登录进行拦截并处理,在默认情况下必须要发送请求路径名为"login"的"POST"请求,且传入的用户名和密码的key为"username"和"possword"。
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil
ter
org.springframework.security.web.context.SecurityContextPersistenceFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.session.SessionManagementFilter
org.springframework.security.web.access.ExceptionTranslationFilter
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
以上是SpringSecurity的主要过滤器,这里就不一一做介绍了,只调了两个比较简单的对比源码作一介绍,如果感兴趣的可以自己去查看。
3.实际使用
3.1添加依赖
加入Security的jar包,这里因为后面要使用到数据库,所以我加了msql连接和jpa的包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
配上数据库的地址。
spring.datasource.url=jdbc:mysql://localhost:3306/spring_security?characterEncoding=utf8&useSSL=true&serverTimezone=Asia/Shanghai&verifyServerCertificate=false
spring.datasource.username=root
spring.datasource.password=1234
因为我没有配端口,默认启动端口为8080。当前登录页面是Security自动加的。
3.2登录实现
3.2.1简单登录
直接启动项目,会在控制台打印一段字符串。
用此字符串作为密码,用户名为user,进行登录。因为没有做其它配置,所以结果如下。
3.2.2配置文件用户名密码配置
1.在properties配置文件中,加入下面用户名和密码配置。
spring.security.user.name=root
spring.security.user.password=1234
2.新建controller,引入thymeleaf包。并在template包下新建hello.html,用于登陆成功后页面的显示。
@Controller
public class MainController {
@GetMapping("hello")
public String hello(){
return "hello";
}
}
页面中做一个简单的打印。
3.请求“http://localhost:8080/hello”,会先跳转到登录页面,输入配置文件中配置的用户名和密码。登录成功,如下图所示
总结:这种方式是一种很简单的实现登录的方法,只是把用户名和密码写入配置文件,SpringSecurity会自动验证页面输入的数据是否和配置的数据相匹配。这种只适用于当前系统只存在单一用户,且无需数据库用户表的前提下,实际开发中,基本上都是多用户多权限角色的情况,基本上用不到当前方法。
3.2.3基于配置类实现
1.新建个配置类,继承WebSecurityConfigurerAdapter接口,并实现它的configure(AuthenticationManagerBuilder auth)方法。在该方法里面自定义用户名,密码及权限。
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**将密码加入到内存中*/
auth.inMemoryAuthentication()
.withUser("admin").password("123").roles("admin");
}
}
2.将properties中配置的用户名密码注释掉。
3.运行程序报错
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
这个错误是因为没有将密码加密引起的,因为SpringSecurity默认存储密码的格式是"{id}xxxxxxx",id指的是加密方式,可以是bcrypt、sha256等,后面跟着的是加密后的密码.也就是说,程序拿到传过来的密码的时候,会首先查找被“{”和“}”包括起来的id,来确定后面的密码是被怎么样加密的,如果找不到就认为id是null.所以这里应该为密码加密。SpringSecurity框架其实内置了加密方法,叫BCryptPasswordEncoder,只需要调用其中的encode()方法对密码进行加密就行了。
修改configure中的内容,将BCryptPasswordEncode注入进来,并将密码加密。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
/**将密码加入到内存中*/
auth.inMemoryAuthentication()
.withUser("admin").password(passwordEncoder().encode("123")).roles("admin");
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
访问“hello”接口,输入用户名和密码后,登录成功。
总结:此方法在配置用户名密码的基础上做了一定的更新,配置用户名密码仅能同时存在一对,通过配置类的configure()方法,可以将多个用户名,密码和权限加入到内存之中。但是这种方法也有一定的确定,就是当系统角色和权限复杂时,若仅将用户名和密码写入到内存中,然后进行对比。若用户修改了名字和密码,还要去该方法中修改相应的数据,这样不仅开发麻烦,维护起来成本也很高。
3.2.4UserDetailsService接口类实现
介绍:
UserDetailsService接口是SpringSecurity中的一个重要接口,该接口里面仅有一个方法。SpringSecurity用该方法来加载已设定好的用户名、密码和权限,我们可以将已存在的数据放入数据库中,通过该方法兑取到内存之中金秀对比。这样开发维护都会变得简单。
可以看出该方法返回了一个UserDetails对象,点进源码查看。
public interface UserDetails extends Serializable {
//获取登录用户的所有权限
Collection<? extends GrantedAuthority> getAuthorities();
//获取登录用户的密码
String getPassword();
//获取登录用户的登录名
String getUsername();
//当前账户是否过期
boolean isAccountNonExpired();
//当前账户是否锁定
boolean isAccountNonLocked();
//当前账户密码是否过去
boolean isCredentialsNonExpired();
//当前账户是否可用
boolean isEnabled();
}
UserDetails里面有几个属性,我把每个属性代表的意思都注释到了上面,方便理解。可能有朋友会问,这些属性又是哪里来的?可以在这个类按Ctrl+H,可以看到右边实现类里面有个User对象。
打开User对象,可以看到里面有这几个属性的定义,我就不一一描述了。
使用:
1.新建两个表,用户表和角色表。
用户表:新建SysUser对象并实现UserDetails接口中的几个方法,注意这里实现的几个isXX方法,默认为false,把它们改为true。然后重写getAuthorities()方法,将用户所属权限构造GrantedAuthority对象插入到集合中。
@Entity
@Table(name = "sys_user")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysUser implements UserDetails {
@Id
@GeneratedValue
private Long id;
private String username;
private String password;
@ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER)
private List<SysRole> roles;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auths = new ArrayList<>();
List<SysRole> roles = this.getRoles();
for (SysRole role : roles) {
auths.add(new SimpleGrantedAuthority(role.getName()));
}
return auths;
}
// 帐户是否过期
@Override
public boolean isAccountNonExpired() {
return true;
}
// 帐户是否被冻结
@Override
public boolean isAccountNonLocked() {
return true;
}
// 帐户密码是否过期,一般有的密码要求性高的系统会使用到,比较每隔一段时间就要求用户重置密码
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 帐号是否可用
@Override
public boolean isEnabled() {
return true;
}
}
角色表:
@Entity
@Table(name = "sys_role")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysRole {
@Id
@GeneratedValue
private Long id;
private String name;
}
修改配置文件,设置为自动生成表结构。
#配置自动生成表结构
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=false
查看数据库,表结构已生成。生成了三张表,分别是用户表,角色表及它们的关系表。 添加一些测试数据。
2.定义用户对象相关的Repository接口
@Repository
public interface SysUserRepository extends JpaRepository<SysUser,Long> {
}
3.定义一个类实现UserDetailsService接口,重写loadUserByUsername()方法。这里注意我数据库里的密码是直接插入进去,而不是使用加密过的密码。所以这里对于返回的SysUser对象,需要先将密码进行加密,然后在保存进对象返回。
@Service
public class MyUserDetailsService implements UserDetailsService {
private final SysUserRepository sysUserRepository;
public MyUserDetailsService(SysUserRepository sysUserRepository) {
this.sysUserRepository = sysUserRepository;
}
@Override
public UserDetails loadUserByUsername(String userName) {
SysUser sysUser = sysUserRepository.getByUserName(userName);
if (ObjectUtils.isEmpty(sysUser)) {
throw new UsernameNotFoundException("当前用户名不存在");
} else {
String encode = new BCryptPasswordEncoder().encode(sysUser.getPassword());
sysUser.setPassword(encode);
return sysUser;
}
}
}
4.修改配置类的configure方法,这里直接将我们自定义的配置类注入进来。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
5.启动项目,访问“http://localhost:8080/hello”,输入用户名和密码,登录成功。