目录
[一、Spring Security介绍](#一、Spring Security介绍)
[二、Spring Security快速入门](#二、Spring Security快速入门)
[三、Spring Security的认证流程](#三、Spring Security的认证流程)
[3.1 Spring Security的过滤器链](#3.1 Spring Security的过滤器链)
[3.2 分析UsernamePasswordAuthenticationFilter](#3.2 分析UsernamePasswordAuthenticationFilter)
[3.3 分析AuthenticationManager](#3.3 分析AuthenticationManager)
[3.4 分析AuthenticationProvider](#3.4 分析AuthenticationProvider)
[3.5 分析查询用户信息过程](#3.5 分析查询用户信息过程)
[3.6 创建会话信息](#3.6 创建会话信息)
[四、自定义Spring Security认证](#四、自定义Spring Security认证)
[4.1 提供登录接口](#4.1 提供登录接口)
[4.2 分析并解决栈内存溢出问题](#4.2 分析并解决栈内存溢出问题)
[4.3 获取异常信息并解决](#4.3 获取异常信息并解决)
[4.4 密码加密](#4.4 密码加密)
[4.5 认证成功绑定会话](#4.5 认证成功绑定会话)
[4.6 完成数据库](#4.6 完成数据库)
[5.1 授权操作前准备](#5.1 授权操作前准备)
[5.2 分析用户的角色&权限信息](#5.2 分析用户的角色&权限信息)
[5.3 完成认证后角色&权限的赋值(配置方式)](#5.3 完成认证后角色&权限的赋值(配置方式))
[5.4 注解授权](#5.4 注解授权)
一、Spring Security介绍

Spring Security就是一个安全框架,帮助咱们实现认证(登录-Authen)和授权(角色,权限/菜单,Author)操作。
除了SpringSecurity,还有一个Java中常见的安全框架,Apache Shiro。
Spring Security是一个相对比较复杂的安全框架,学习的他方式,要先理解底层流程,然后才能实现自定义认证等操作。
二、Spring Security快速入门
1、创建Maven项目
2、导入依赖
XML
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
3、构建启动类和yml文件
4、编写一个资源
java
package com.fugui.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/hello")
public String hello(){
return "Hello!";
}
}
5、启动测试,访问hello资源,发现需要认证才可以访问

默认用户名就是user,密码可以在你的console里看到。

认证后,再次访问hello资源

三、Spring Security的认证流程
3.1 Spring Security的过滤器链
SpringSecurity提供了一堆的过滤器,这一堆过滤器就组成了过滤器链。
整个认证的流程的触发的位置,其实就是一个Filter开始的。
先查看一下SpringSecurity提供的一堆过滤器
得到一个结论,想剖析整个认证的流程,就要从UsernamePasswordAuthenticationFilter入手。从doFilter方法入手。
3.2 分析UsernamePasswordAuthenticationFilter

3.3 分析AuthenticationManager
UsernamePasswordAuthenticationFilter 中获取的是AuthenticationManager 接口下的ProviderManager 实现类去完成的authenticate方法。
在认证管理器中,它会找到一个 AuthenticationProvider ,去完成具体的认证操作。
3.4 分析AuthenticationProvider
这里执行认证时,走的是DaoAuthenticationProvider。
对象是上面的对象,但是流程走的是他的父类AbstractUserDetailsAuthenticationProvider。
3.5 分析查询用户信息过程

完整的流程图

3.6 创建会话信息
所谓会话,对应技术来说,就是Cookie +Session。
在UsernamePasswordAuthenticationFilter中,认证成功后,会触发success的方法。
1、将用户信息存储到了一个SecurityContext的对象里。
2、将SecurityContext对象,放到了SecurityContextHolder对象中。
之后,在SpringSecurity中还提供了一个 SecurityContextPersistenceFilter ,基于他完成了将SecurityContext和Session的绑定。 直接将SecurityContext扔到了HttpSession的域中,完成了会话的绑定。

四、自定义Spring Security认证
4.1 提供登录接口
1、提供Controller接口,完成基本流程的功能
javapackage com.fugui.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class LoginController { @Autowired private AuthenticationManager authenticationManager; @RequestMapping("/user/login") public String login(String username,String password) { //1、参数校验 if(ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password)) { return "username or password is null!"; } //2、封装参数,token UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,password); //3、执行认证, 认证管理器 Authentication user = authenticationManager.authenticate(token); if(user == null){ return "username or password is incorrect!"; } //4、TODO 认证成功,把用户信息,扔SecurityContext,将SecurityContext扔SecurityContextHolder里 return "success!"; } }
2、因为需要安全管理器,配置AuthenticationManager对象 && 默认资源地址被拦截,配置对认证资源放行
javapackage com.fugui.config; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @EnableWebSecurity public class SecurityConfig { /** * 配置安全管理器 * @param http * @return * @throws Exception */ @Bean public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception { // 构建者 AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class); // 基于构建者,构建安全管理器 AuthenticationManager authenticationManager = builder.build(); // 返回安全管理器 return authenticationManager; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests(req ->req .mvcMatchers("/user/login").permitAll() .anyRequest().authenticated() ); return http.build(); } }
4、再次访问,报错!栈内存移除的错误! 成功了!!
Ps:这里提供的配置方式,是Security高版本的.
java
package com.fugui.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
public class SecurityOldConfig extends WebSecurityConfigurerAdapter {
/**
* 安全管理器配置
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 路径放行
* @param http the {@link HttpSecurity} to modify
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(req -> req
.mvcMatchers("/user/login").permitAll()
.anyRequest().authenticated()
);
}
}
4.2 分析并解决栈内存溢出问题
正常的认证流程,应当是基于AuthenticationManager找到对应可用的AuthenticationProvider,但是经过N多次查找,没找到可以使用的。
现在咱们是SpringBoot工程,导入的依赖也是starter。
因为是自动装配,之前没有主动配置AuthenticationManager,所以他帮我们构建了很多东西。
但是现在我主动配置了AuthenticationManager,那一些之前默认构建的,现在没了!
经过查看UserDetailsServiceAutoConfiguration得知,不会构建UserDetailsManager的实例。
咱们现在的问题是,我希望基于 AuthenticationManager 去找到 DaoAuthenticationProvider 去完成认证的操作。
但是明显现在没有构建 DaoAuthenticationProvider 实例。
DaoAuthenticationProvider 是基于 InitializeUserDetailsBeanManagerConfigurer构建的,所以查看他构建的原理是什么。
最终结论,就是因为在我设置AuthenticationManager后,没有主动的去构建一个UserDetailsService的实例,导致出现了栈内存溢出。
解决方案就是构建好UserDetailsService的实例。
并且因为UserDetailsService重写的loadUserByUsername方法,需要返回UserDetails类型,也指定一个
javapackage com.fugui.pojo; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; import java.util.Collections; public class MyUserDetails implements UserDetails { private String username; private String password; public MyUserDetails() { } public MyUserDetails(String username, String password) { this.username = username; this.password = password; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return Collections.emptyList(); } @Override public boolean isAccountNonExpired() { return false; } @Override public boolean isAccountNonLocked() { return false; } @Override public boolean isCredentialsNonExpired() { return false; } @Override public boolean isEnabled() { return false; } }
然后是执行的具体的UserDetailsService的实例
javapackage com.fugui.service.impl; import com.mashibing.pojo.MyUserDetails; 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.Service; @Service public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return new MyUserDetails("root","root"); } }
Ps:最终重启项目再次测试,发现认证后没任何信息的反馈!
4.3 获取异常信息并解决
发现,错误信息不会被抛出来,很明显是Spring Security在某个位置将异常信息捕获了。
是在SpringSecurity提供的一个ExceptionTranslationFilter,在这个Filter里专门把异常给处理了。
咱们手动的在
认证这个位置加了try-catch,并且捕获异常信息,打印异常。得知是用户被锁定。
通过之前的流程可知,是在DaoAuthenticationProvider里面做的校验。
直接将UserDetailsService中返回的UserDetails对象中的各个信息,调整一下,确保不锁定,不过期,开启,凭据不过期。
最终再次测试,得到一个错误
There is no PasswordEncoder mapped for the id "null"
4.4 密码加密
SpringSecurity默认会将查询出来的用户密码加密,有两个方式:
在配置了密码加密方式后,密码可以直接存储响应的密文
如果没配置密码加密方式,需要在密码前基于{加密方式}告诉SpringSecurity,密码的比较方式
咱们是第二种方式,解决问题很简单,只需要将模拟数据库中查询到的密码前追加{noop}
密码明文存储存在很大的问题。
如果数据库中存储明文密码,然后数据库数据泄露了,那就导致泄露的用户信息任何人都可以登录了。
So,这里不要上noop这种不加密的方式,需要加密。
之前一般比较常见的手段,MD5 + 盐。
而在SpringSecurity中,一般使用BCrypt加密方式。
测试一下效果
javapublic static void main(String[] args) { BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); System.out.println(encoder.encode("123456")); System.out.println(encoder.encode("123456")); System.out.println(encoder.matches("123456", "$2a$10$xqOv.MIxsC60mB1FfQImpu3hkZn0soCg6fZ67CEMf3oumNMWbe2D.")); } $2a$10$xqOv.MIxsC60mB1FfQImpu3hkZn0soCg6fZ67CEMf3oumNMWbe2D. $2a$10$qCieD6PIrfVPJNZoeXkv0OLkAjzdQQLQIL9nilZZAWcM91H.sGPb. BCrypt的密文格式: $2a$:2a代表BCrypt的版本 $10$:10代表计算成本,2^10次算法,数值越大,越安全。这个数值越大,计算的时间成本也就越大。 xqOv.MIxsC60mB1FfQImpu3hkZ:盐 n0soCg6fZ67CEMf3oumNMWbe2D.:hash值
密码密码加密配置
java/** * 采用BCrypt的密码加密手段 * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
Ps:认证成功后,发现其他资源依然不能访问。
4.5 认证成功绑定会话
package com.fugui.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class LoginController { @Autowired private AuthenticationManager authenticationManager; @RequestMapping("/user/login") public String login(String username,String password) { //1、参数校验 if(ObjectUtils.isEmpty(username) || ObjectUtils.isEmpty(password)) { return "username or password is null!"; } //2、封装参数,token UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username,password); //3、执行认证, 认证管理器 Authentication user = null; try { user = authenticationManager.authenticate(token); } catch (AuthenticationException e) { e.printStackTrace(); } if(user == null){ return "username or password is incorrect!"; } //4、认证成功,把用户信息,扔SecurityContext,将SecurityContext扔SecurityContextHolder里 SecurityContextHolder.getContext().setAuthentication(user); return "success!"; } }
流程图

4.6 完成数据库
1、准备库表结构以及对应的实体类。
2、导入数据库操作的相关依赖
XML<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
3、编写配置文件。
XMLspring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///spring_security?characterEncoding=utf-8 username: root password: root type: com.alibaba.druid.pool.DruidDataSource
4、编写MyBatis相关的接口, 并在启动类扫描接口所在包
javaimport com.fugui.entity.User; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; public interface UserMapper { @Select("select * from `user` where username = #{username} ") User findUserByUsername(@Param("username") String username); }
5、先单独测试Mapper接口
javapackage com.fugui.mapper; import com.mashibing.entity.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest class UserMapperTest { @Autowired private UserMapper userMapper; @Test void findUserByUsername() { User user = userMapper.findUserByUsername("root"); System.out.println(user); } }
6、完成UserDetailsService实现类与数据库交互
javapackage com.fugui.service.impl; import com.fugui.entity.User; import com.fugui.mapper.UserMapper; import com.fugui.pojo.MyUserDetails; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.BadCredentialsException; 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.Service; @Service public class MyUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、根据用户名查询用户信息 User user = userMapper.findUserByUsername(username); //2、判断查询到的用户名是为否null,为null可以抛出异常,也可以return null if(user == null){ System.out.println(username + "用户无法找到!"); throw new BadCredentialsException("用户名或密码错误"); } //3、不为null,就将User对象中的信息封装到UserDetails中 UserDetails userDetails = new MyUserDetails(user.getUsername(), user.getPassword()); //4、返回UserDetails即可 return userDetails; } }
7、完整测试一波
五、授权管理
5.1 授权操作前准备
所有的表结构,自己创建
授权一定是在认证操作之后才做的事情。
只有用户登录后,才能知道这个用户具备什么角色。 拿到角色信息后,才能根据角色查询对应的权限信息
经典五张表
5.2 分析用户的角色&权限信息
SpringSecurity的授权操作,并没有将角色和权限用很细粒度的方式区分。
提供了两个接口,一个role,一个perm分别代表角色授权跟权限授权。
java@GetMapping("/role") public String role(){ return "role"; } @GetMapping("/perm") public String perm(){ return "perm"; }
并且在配置文件中追加上了对于角色和权限的校验。
java@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests(req ->req .mvcMatchers("/user/login").permitAll() .mvcMatchers("/role").hasRole("admin") .mvcMatchers("/perm").hasAuthority("user:select") .anyRequest().authenticated() ); return http.build(); }
为了查看授权过程,直接找到 FilterSecurityInterceptor 过滤器,在内部可以看到认证和授权的操作方法。
访问role路径,发现他会给角色前追加一个ROLE_的前缀
访问perm路径,权限信息没有改变
得出结论,虽然只有一个集合,但是在设置信息时,可以指定前缀来区分是角色信息还是权限信息。
5.3 完成认证后角色&权限的赋值(配置方式)
1、给MyUserDetails提供一个authorities属性,并且提供set方法,并且修改get方法
javaprivate Collection<? extends GrantedAuthority> authorities; public void setAuthorities(Collection<? extends GrantedAuthority> authorities) { this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; }
2、需要在MyUserDetailsService中查询用户的角色和权限信息。
javapackage com.fugui.service.impl; import com.fugui.entity.User; import com.fugui.mapper.UserMapper; import com.fugui.pojo.MyUserDetails; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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.Service; import java.util.ArrayList; import java.util.Collection; import java.util.Set; @Service public class MyUserDetailsService implements UserDetailsService { @Autowired private UserMapper userMapper; private final String ROLE_PREFIX = "ROLE_"; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //1、根据用户名查询用户信息 User user = userMapper.findUserByUsername(username); //2、判断查询到的用户名是为否null,为null可以抛出异常,也可以return null if(user == null){ System.out.println(username + "用户无法找到!"); throw new BadCredentialsException("用户名或密码错误"); } //3、不为null,就将User对象中的信息封装到UserDetails中 MyUserDetails userDetails = new MyUserDetails(user.getUsername(), user.getPassword()); //4、查询用户的角色和权限信息,并复制到userDetails中。 //4.1 查询角色信息 Set<String> roleNameSet = userMapper.findRoleNameByUserId(user.getId()); //4.2 查询权限信息 Set<String> permNameSet = userMapper.findPermissionByUserId(user.getId()); //4.3 声明完整的授权集合 Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); //4.4 遍历角色和权限都扔到authorities集合中,注意,角色需要追加前缀! for (String roleName : roleNameSet) { authorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + roleName)); } for (String permName : permNameSet) { authorities.add(new SimpleGrantedAuthority(permName)); } //4.5 设置权限信息到userDetails中 userDetails.setAuthorities(authorities); //5、返回UserDetails即可 return userDetails; } }
3、测试在5.2中的角色和权限信息。
这里测试出了个问题,就是没有重新修改getAuthor......方法,修改完毕后,配置文件方式的授权操作就没有问题了!
5.4 注解授权
前面的配置授权是基于FilterSecurityInterceptor去完成的校验。
但是注解授权是基于AOP实现的。
在5.3中已经完成了角色和权限的赋值,到这直接写注解测试即可。
优先将配置中的授权操作注释!
在Controller头上直接追加响应的注解完成授权。
1、需要在注解授权前,优先加上一个注解到启动类
@EnableMethodSecurity(prePostEnabled = true)
2、在接口上追加授权注解即可
java@GetMapping("/role") @PreAuthorize(value = "hasRole('管理员')") public String role(){ return "role"; } @GetMapping("/perm") @PreAuthorize("hasAnyAuthority('xxx:yyy')") public String perm(){ return "perm"; }