Spring Security 实践及源码学习

目录

[一、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

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接口,完成基本流程的功能

java 复制代码
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 {
​
 @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对象 && 默认资源地址被拦截,配置对认证资源放行

java 复制代码
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
     */
    @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类型,也指定一个

java 复制代码
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;
 }
​
 @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的实例

java 复制代码
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 {
​
 @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加密方式。

测试一下效果

java 复制代码
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值

密码密码加密配置

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、编写配置文件。

XML 复制代码
spring:
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相关的接口, 并在启动类扫描接口所在包

java 复制代码
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接口

java 复制代码
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 {
​
 @Autowired
 private UserMapper userMapper;
​
 @Test
 void findUserByUsername() {
     User user = userMapper.findUserByUsername("root");
     System.out.println(user);
 }
}

6、完成UserDetailsService实现类与数据库交互

java 复制代码
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 {
​
 @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方法

java 复制代码
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中查询用户的角色和权限信息。

java 复制代码
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 {
​
 @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";
}
相关推荐
regret~21 分钟前
【记录】Ubuntu20.04安装mysql
数据库·mysql
idolyXyz34 分钟前
[spring6: SpringApplication.run]-应用启动
spring
见未见过的风景1 小时前
想删除表中重复数据,只留下一条,sql怎么写
数据库·sql
_Kayo_3 小时前
项目学习笔记 display从none切换成block
windows·笔记·学习
哆啦A梦的口袋呀3 小时前
pymongo库:简易方式存取数据
数据库·mongodb
城里有一颗星星3 小时前
6.删除-demo
数据库·go
失重外太空啦4 小时前
Mysql练习
android·数据库·mysql
像风一样自由20204 小时前
Navicat操作指南:MySQL数据库配置与Todo应用部署
数据库·mysql·adb
青竹易寒4 小时前
Redis技术笔记-从三大缓存问题到高可用集群落地实战
数据库·redis·笔记
两圆相切4 小时前
主流数据库的备份与还原差异对比
数据库·oracle