Spring Security的基本使用及注意事项(一)
文章目录
- Spring Security的基本使用及注意事项(一)
- Spring Security简介
- 认证与授权
- RBAC模型
- Spring Security使用步骤
- Manven坐标引入(使用SpringBoot进行整合)
- 认证
- 实现UserDetailsService接口
- 详细步骤
- 授权
- 实现流程
- Spring Seurtity配置类
- 捕获filter中的异常
- 实现跨域
- 配置密码解析器
- 插入数据时注意,需要将密码的格式设置为BCryptPasswordEncoder采取的格式
2022-08-05 16:19:47 星期五
Spring Security简介
- Spring Security是Spring家族中的权限验证框架,主要可用于后台系统的认证与授权
认证与授权
- 认证,即告知系统当前登录对象,并将用户信息保存;
- 授权,即系统服务当前登录对象可使用哪些功能的过程,系统会在认证结束之后获取到当前用户具有的权限,例如,使用Add(添加信息)功能时,需要拥有管理员权限,则会判断当前用户是否具有管理员权限,如果具有则可以使用添加信息的功能,如果没有,则会返回403。
RBAC模型
- 基于资源或角色的数据权限模型
- 涉及到的数据关系模型:
- 角色、权限、用户、(菜单),其中权限关系是中心;
- 角色——权限:多对多关系,用户——权限:多对多关系,菜单——权限:多对多关系
- 共计四个关系,三个联系,数据库中设计为7个表结构
- 常用的是以权限资源为中心进行验证,可扩展性高,健壮性较强。
Spring Security使用步骤
Spring Security本质上是一条拦截器链,通过各种各样的拦截器进行验证,最终实现完整的权限验证过程。
Manven坐标引入(使用SpringBoot进行整合)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- UsernamePasswordAuthenticationFilter:负责校验用户名与密码的拦截器
- ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException(授权时发生的异常)和AuthenticationException(认证时发生的异常)。
- **FilterSecurityInterceptor:**负责权限校验的过滤器。
认证
- 用户名与密码传递到UsernamePasswordAuthenticationFilter拦截器中
- 拦截器进行对象的封装usernamePasswordAuthenticationToken,通过AuthenticationManager类中的authenticate方法进行验证,验证通过封装成Authentication对象,保存用户的信息,包括但不限于用户名,密码,权限等。
- 其中authenticate方法在认证时需要使用到UserDetailService接口,我们需要实现该接口
- 判断Authentication对象是否为空,如果是null,则代表认证未通过
- 将用户id生成Token令牌返回给用户,将Authentication对象存入缓存中
实现UserDetailsService接口
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getTel, username);
User user = mapperUser.selectOne(queryWrapper);
if (Objects.isNull(user)) {
return null;
}
// 获取用户权限的操作
List<String> strings = mapperPermission.SelectPermissionId(user.getTel());
LambdaQueryWrapper<Permission> queryWrapper1 = new LambdaQueryWrapper<>();
queryWrapper1.select(Permission::getKeyword).in(Permission::getId, strings);
List<Permission> permissions = mapperPermission.selectList(queryWrapper1);
List<String> permissionlist = permissions.stream().map(Permission::getKeyword).collect(Collectors.toList());
System.out.println("权限集合是:" + permissionlist.get(0));
Login login = new Login();
login.setUser(user);
login.setPermissions(permissionlist);
return login;
}
详细步骤
- UserDetilsService接口是Spring Security框架实现用户名密码验证的服务层对象,通过
- 重写UserDetilsService接口loadUserByUsername方法
- loadUserByUsername方法实现自定义的用户名密码验证过程;
- loadUserByUsername方法内部主要实现的任务如下:
- 根据用户名数据库中查找该用户信息,如果没有,则返回空,SPring Security会**抛出异常和AuthenticationException **;
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getTel, username);
User user = mapperUser.selectOne(queryWrapper);
if (Objects.isNull(user)) {
return null;
}
注意的是:在实际开发过程中我们最好将异常信息进行统一的管理,交由Cotroller层统一返回给用户,所以我们返回结果为null,使Spring Security抛出异常
- 查找用户所具有的权限信息,
List<String> strings = mapperPermission.SelectPermissionId(user.getTel());
LambdaQueryWrapper<Permission> queryWrapper1 = new LambdaQueryWrapper<>();
queryWrapper1.select(Permission::getKeyword).in(Permission::getId, strings);
List<Permission> permissions = mapperPermission.selectList(queryWrapper1);
List<String> permissionlist = permissions.stream().map(Permission::getKeyword).collect(Collectors.toList());
- 将用户信息与权限信息封装成UserDetil对象,在本案例中,Login是实现自UserDetil接口,最后返回Login对象
Login login = new Login();
login.setUser(user);
login.setPermissions(permissionlist);
授权
- 在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
- 所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
- 然后设置我们的资源所需要的权限即可。
实现流程
- 获取前端传递的TOken值,取出缓存中存取的UserDetil对象信息,实现过程就是通过新建过滤器,在过滤器中完成解析
http.addFilterBefore(sercityFilter, UsernamePasswordAuthenticationFilter.class);
过滤器类
@Component
public class SercityFilter extends OncePerRequestFilter {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<>();
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_niming"));
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(null, null, grantedAuthorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
return;
}
// 解析token
Claims claims = null;
try {
claims = JWTUntil.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
}
String tel = claims.getSubject();
System.out.println("电话是" + tel);
// 获取缓存中的内容
String login = (String) redisTemplate.opsForHash().get("login", tel);
Login login1 = JSON.parseObject(login, Login.class);
login1.getAuthorities().stream().forEach(new Stream.Builder<GrantedAuthority>() {
@Override
public void accept(GrantedAuthority grantedAuthority) {
System.out.println("第一种" + grantedAuthority);
}
@Override
public Stream<GrantedAuthority> build() {
return null;
}
});
if (Objects.isNull(login)) {
throw new RuntimeException("用户未登录");
}
// TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(login1.getUsername(), login1.getPassword(), login1.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
- 添加Authentication到Sercurity全局对象中,授权过程结束
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(login1.getUsername(), login1.getPassword(), login1.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
认证与授权的基本过程为上述内容
Spring Seurtity配置类
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
public class SecrityConfigure extends WebSecurityConfigurerAdapter {
// @Autowired
// private CrosFileter crosFileter;
@Autowired
private WebApplicationContext applicationContext;
@Autowired
private AuthenticationConfiguration authenticationConfiguration;
@Autowired
private SercityFilter sercityFilter;
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
/**
* 获取标有注解 AnonymousAccess 的访问路径
*/
private String[] getAnonymousUrls() {
RequestMappingHandlerMapping bean = SpringUtils.getBean(RequestMappingHandlerMapping.class);
// new Webappl
// 获取所有的 RequestMapping
Map<RequestMappingInfo, HandlerMethod> handlerMethods = bean.getHandlerMethods();
Set<String> allAnonymousAccess = new HashSet<>();
// 循环 RequestMapping
for (Map.Entry<RequestMappingInfo, HandlerMethod> infoEntry : handlerMethods.entrySet()) {
HandlerMethod value = infoEntry.getValue();
// 获取方法上 AnonymousAccess 类型的注解
AnonymousAccess methodAnnotation = value.getMethodAnnotation(AnonymousAccess.class);
// 如果方法上标注了 AnonymousAccess 注解,就获取该方法的访问全路径
if (methodAnnotation != null) {
if (infoEntry.getKey().getPatternsCondition()!=null){
allAnonymousAccess.addAll(infoEntry.getKey().getPatternsCondition().getPatterns());
}
}
}
allAnonymousAccess.stream().forEach(System.out::println);
return allAnonymousAccess.toArray(new String[0]);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http.cors();
http.csrf().disable()
//跨域请求会先进行一次options请求
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll()
// .antMatchers(HttpMethod.GET,"/video/").anonymous()
//解决跨域问题
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
//
// .antMatchers("/video/user/**").hasAuthority("userpersission")
.antMatchers("/user/login/**").permitAll()
.antMatchers(HttpMethod.POST,"/user").permitAll()
.antMatchers(getAnonymousUrls()).anonymous()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
http.addFilterBefore(sercityFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
// http.addFilterBefore(crosFileter,sercityFilter.getClass());
}
// @Bean
// SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
关闭csrf
// return http.csrf().disable()
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// .and()
// .authorizeRequests()
// .antMatchers("/user/login").anonymous()
// .anyRequest().authenticated()
// .and()
.addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class)
// .build();
//
//
//
// }
@Bean
public AuthenticationManager authenticationManager() throws Exception {
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
return authenticationManager;
}
}
- 首先需要继承WebSecurityConfigurerAdapter
- 重写configure方法
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http.cors();
http.csrf().disable()
//跨域请求会先进行一次options请求
.authorizeRequests()//开启请求验证
.antMatchers(HttpMethod.OPTIONS).permitAll()
// .antMatchers(HttpMethod.GET,"/video/").anonymous()
//解决跨域问题
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
//
// .antMatchers("/video/user/**").hasAuthority("userpersission")
.antMatchers("/user/login/**").permitAll()
.antMatchers(HttpMethod.POST,"/user").permitAll()
.antMatchers(getAnonymousUrls()).anonymous()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and();
http.addFilterBefore(sercityFilter, UsernamePasswordAuthenticationFilter.class);
//允许跨域
// http.addFilterBefore(crosFileter,sercityFilter.getClass());
}
注意事项 permitAll()是允许所有角色访问,anonymous()是允许匿名访问,当发送到后端的请求中带有token时anonymous()将不起作用,
角色必须带有ROLE_前缀
- 注入AuthenticationManager类,用于创建Authentication对象
@Bean
public AuthenticationManager authenticationManager() throws Exception {
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
return authenticationManager;
}
- 开启注解,可以在方法上指定权限类型
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)
- prePostEnabled较为常用的注解,使用该注解指定角色时,不需要添加ROLE_前缀
捕获filter中的异常
- 进行请求转发到controller层
httpServletRequest.setAttribute("Loginecption", new RuntimeException("用户未登录"));
httpServletRequest
.getRequestDispatcher("/user/error")
.forward(httpServletRequest, httpServletResponse);
@GetMapping("error")
public String error(HttpServletRequest request, HttpServletResponse response) throws Exception {
Exception ecption =(Exception) request.getAttribute("Loginecption");
throw new LoginEcption(ecption.getMessage());
实现跨域
public class SpringmvcConfigure implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// WebMvcConfigurer.super.addCorsMappings(registry);
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
在security中也需要设置跨域
// 允许跨域
http.cors()
@Configuration
public class SecurotyConfig extends WebSecurityConfigurerAdapter {
// @Resource
// private AuthFailCompent authFailCompent;
// 采用Security提供的密码加密器BCryptPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Resource
public JwtAuthticateTokenFilter jwtAuthticateTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
http//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
.antMatchers("/test/**").anonymous()
// 除了上面的登录接口,其他都需要认证身份
.anyRequest().authenticated().and();
// .and().formLogin().failureHandler(authFailCompent);
// 将token解析的过滤器,添加到用户名密码校验的过滤器前
http.addFilterBefore(jwtAuthticateTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 允许跨域
http.cors();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
配置密码解析器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
插入数据时注意,需要将密码的格式设置为BCryptPasswordEncoder采取的格式
@Resource
private PasswordEncoder passwordEncoder;
@Test
void contextLoads() {
String encode = passwordEncoder.encode("123456");
User build = User.builder().userName("lmx").password(encode).nickName("飘零书剑").userType("1").build();
userMapper.insert(build);
}