security原理就是一系列过滤器链,1主要负责处理登录请求,2负责处理异常,3负责校验
过滤链逻辑
- 用户提交用户名和密码首先经过UsernamePasswordAuthenticationFilter,这个过滤器主要是封装
Authentication对象
,此时还没有封装权限,作用就是UserDetails对象中的数据进行校验。注意
:这个Authentication对象我们可以在UsernamePasswordAuthenticationFilter前加上一个过滤器进行封装,然后直接在我们的过滤器中调用ProvideManger进行下一步 - 进入ProvideManger,然后进入UserDetailsService,
默认框架是从内存中查找用户信息封装UserDetails对象
,这里我们自己实现UserDetailsService从数据查询,然后返回UserDetails对象,在这一步中我们查询权限,然后封装到UserDetails对象中 - 返回的UserDetails对象和Authentication中的数据进行校验,通过PasswordEncoder进行密码比对,然后将正确的UserDetails的权限设置到Authentication中,返回Authentication。
- 如果上一步返回了Authenticationd对象,我们可以通过SecurityContextHolder.getContext().setAuthentication方法存储该对象。
值得注意的是SecurityContextHolder是各过滤链检查的对象,SecurityContextHolder中封装了Authentication
所以我们只需要实现UserDetails接口,从数据库查询用户名和密码,然后封装对象
登录接口
- 整体流程,用户输入用户名和密码,会封装到Authentication中,然后经过UserDetailsService(主要是根据用户名从数据库中查询数据,并获取用户的权限信息)中,返回UserDetails对象,经过和Authentication的对比判断是否用户名和密码正确,以及用户拥有的权限
在Authentication和UserDetails的密码比对过程中,默认会用PasswordEncoder进行加密,但是一般我们不会使用默认的加密方式
- springsecurity给我们提供了BCryptPasswordEncoder加密方式
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//重写,可以注入此对象
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
private JwtAuthFilter jwtAuthFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
//关闭csrf
csrf().disable()
//不通过session获取securityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//对于登录接口,允许匿名访问,还没登录可以访问,登录了就访问不了
.antMatchers("/admin/login","/admin/logout").anonymous()
//一直允许
.antMatchers("/admin/test").permitAll()
//除上面所有请求都需要认证,认证之后可以访问
.anyRequest().authenticated();
//设置过滤器顺序
http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
- 这里我们实现它的UserDetails,根据查出来的数据进行Authentication对比
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private Admin admin;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return admin.getPassword();
}
@Override
public String getUsername() {
return admin.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- UserDetailsService实现类
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Resource
private AdminService adminService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//根据用户名查询用户信息
LambdaQueryWrapper<Admin> lambdaQueryWrapper =new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(Admin::getUsername,s);
Admin one = adminService.getOne(lambdaQueryWrapper);
if(Objects.isNull(one)){
//抛出的异常会交由FilterSecurityInterceptor处理
throw new RuntimeException("对象为空");
}
// TODO 查询权限信息
return new LoginUser(one);
}
}
此时输入数据库中的用户名和密码,可以通过,密码是加密后的密码
自己创建controller登录
- 登录逻辑
- 用户第一次登录,对用户进行AuthenticationManager authenticate 进行用户认证,验证完之后UserDetails已经有数据了
- 认证通过了就将LoginUser 的信息存入Redis,key为key+id,然后根据用户id和username生成token
- 第二次用户访问,进入我们的拦截器,从请求头中获取token,然后解析id,拼接key+id,查询redis,获取LoginUser
- 将获取的LoginUser 封装到Authentication中,因为Authentication已经有数据了,所以其他请求可以通过。
@PostMapping("/login")
public ResponseResult login(@RequestBody Admin admin){
return adminService.login(admin);
}
经过AuthenticationManager和UserDetails的认证封装到Authentication对象,可以清楚的看到principal的数据和封装信息
@Service("adminService")
public class AdminServiceImpl extends ServiceImpl<AdminDao, Admin> implements AdminService {
//此对象是封装校验的
@Resource
private AuthenticationManager authenticationManager;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public ResponseResult login(Admin admin) {
//AuthenticationManager authenticate 进行用户认证
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(admin.getUsername(), admin.getPassword());
Authentication authenticate = authenticationManager.authenticate(authToken);//会调用UseDetailsService
//如果认证没通过,返回提示
if(Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
//如果认证通过了就生成jwt,存入redis
LoginUser principal = (LoginUser) authenticate.getPrincipal();
String id = principal.getAdmin().getId().toString();
String username = principal.getAdmin().getUsername();
String jwtToken = JWTUtils.getJwtToken(id, username);
//存入redis
redisTemplate.opsForValue().set("login"+id, JSONUtil.toJsonStr(principal));
return ResponseResult.ok().data("token",jwtToken);
}
}
创建拦截器
从token中获取id,然后根据id查询用户信息,还有权限信息,然后存入SecurityContextHolder中,后面的拦截器(如FilterSecurityInterceptor)都是从这个对象中验证
- 由于我们的过滤器的先后顺序没有设置,框架首先走的是自己的过滤器,发现没有验证,直接会抛出异常
- 所以我们需要配置拦截器的顺序
@Autowired
private JwtAuthFilter jwtAuthFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
......
//设置过滤器顺序
http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
}
- 拦截器
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
//继承OncePerRequestFilter是为了避免多次过滤
@Autowired
private StringRedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//从token中获取id,然后根据id查询用户信息
String id = JWTUtils.getMemberIdByJwtToken(httpServletRequest);
if(StrUtil.isBlank(id)){
filterChain.doFilter(httpServletRequest,httpServletResponse);
return;
}
//根据key获取redis的数据
String loginUserStr = redisTemplate.opsForValue().get("login" + id);
LoginUser loginUser = JSONUtil.toBean(loginUserStr, LoginUser.class);
//封装权限 TODO
//保存到SecurityContextHolder
if(BeanUtil.isNotEmpty(loginUser)){
//授权,后面执行根据loginUser UserDetailsService会进行封装数据
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数网络安全工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点!真正的体系化!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
Kd-1715540810272)]
[外链图片转存中…(img-ZtXSwFMt-1715540810273)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点!真正的体系化!
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!