Spring Security 实践及源码学习
目录
一、Spring Security介绍
二、Spring Security快速入门
三、Spring Security的认证流程
3.1 Spring Security的过滤器链
3.2 分析UsernamePasswordAuthenticationFilter
3.3 分析AuthenticationManager
3.4 分析AuthenticationProvider
3.5 分析查询用户信息过程
3.6 创建会话信息
四、自定义Spring Security认证
4.1 提供登录接口
4.2 分析并解决栈内存溢出问题
4.3 获取异常信息并解决
4.4 密码加密
4.5 认证成功绑定会话
4.6 完成数据库
五、授权管理
5.1 授权操作前准备
所有的表结构,自己创建
5.2 分析用户的角色&权限信息
5.3 完成认证后角色&权限的赋值(配置方式)
5.4 注解授权
一、Spring Security介绍
官网:Spring Security
Spring Security就是一个安全框架,帮助咱们实现认证(登录-Authen)和授权(角色,权限/菜单,Author)操作。
除了SpringSecurity,还有一个Java中常见的安全框架,Apache Shiro。
Spring Security是一个相对比较复杂的安全框架,学习的他方式,要先理解底层流程,然后才能实现自定义认证等操作。
二、Spring Security快速入门
1、创建Maven项目
2、导入依赖
<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、编写一个资源
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接口,完成基本流程的功能
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.util.ObjectUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class LoginController { @Autowiredprivate 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、封装参数,tokenUsernamePasswordAuthenticationToken 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对象 && 默认资源地址被拦截,配置对认证资源放行
package 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*/@Beanpublic AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {// 构建者AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class);// 基于构建者,构建安全管理器AuthenticationManager authenticationManager = builder.build();// 返回安全管理器return authenticationManager;} @Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests(req ->req.mvcMatchers("/user/login").permitAll().anyRequest().authenticated());return http.build();} }
4、再次访问,报错!栈内存移除的错误! 成功了!!
Ps:这里提供的配置方式,是Security高版本的.
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@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
/*** 路径放行* @param http the {@link HttpSecurity} to modify* @throws Exception*/@Overrideprotected 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类型,也指定一个
package 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;} @Overridepublic String getUsername() {return username;} public void setUsername(String username) {this.username = username;} @Overridepublic String getPassword() {return password;} public void setPassword(String password) {this.password = password;} @Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return Collections.emptyList();} @Overridepublic boolean isAccountNonExpired() {return false;} @Overridepublic boolean isAccountNonLocked() {return false;} @Overridepublic boolean isCredentialsNonExpired() {return false;} @Overridepublic boolean isEnabled() {return false;} }
然后是执行的具体的UserDetailsService的实例
package 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 { @Overridepublic 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加密方式。
测试一下效果
public 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值
密码密码加密配置
/*** 采用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 { @Autowiredprivate 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、封装参数,tokenUsernamePasswordAuthenticationToken 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、导入数据库操作的相关依赖
<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、编写配置文件。
spring: datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql:///spring_security?characterEncoding=utf-8username: rootpassword: roottype: com.alibaba.druid.pool.DruidDataSource
4、编写MyBatis相关的接口, 并在启动类扫描接口所在包
import 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接口
package 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 { @Autowiredprivate UserMapper userMapper; @Testvoid findUserByUsername() {User user = userMapper.findUserByUsername("root");System.out.println(user);} }
6、完成UserDetailsService实现类与数据库交互
package 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 { @Autowiredprivate UserMapper userMapper; @Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//1、根据用户名查询用户信息User user = userMapper.findUserByUsername(username); //2、判断查询到的用户名是为否null,为null可以抛出异常,也可以return nullif(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分别代表角色授权跟权限授权。
@GetMapping("/role") public String role(){return "role"; } @GetMapping("/perm") public String perm(){return "perm"; }
并且在配置文件中追加上了对于角色和权限的校验。
@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方法
private 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中查询用户的角色和权限信息。
package 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 { @Autowiredprivate UserMapper userMapper; private final String ROLE_PREFIX = "ROLE_"; @Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//1、根据用户名查询用户信息User user = userMapper.findUserByUsername(username); //2、判断查询到的用户名是为否null,为null可以抛出异常,也可以return nullif(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、在接口上追加授权注解即可
@GetMapping("/role") @PreAuthorize(value = "hasRole('管理员')") public String role(){return "role"; }@GetMapping("/perm") @PreAuthorize("hasAnyAuthority('xxx:yyy')") public String perm(){return "perm"; }