如何利用SpringSecurity进行认证与授权

目录 一、SpringSecurity简介[1.1 入门Demo](#1.1 入门Demo)二、认证?编辑[2.1 SpringSecurity完整流程](#2.1 SpringSecurity完整流程)[2.2 认证流程详解](#2.2 认证流程详解)[?2.3 自定义认证实现](#?2.3 自定义认证实现)[2.3.1 数据库校验用户](#2.3.1 数据库校验用户)[2.3.2 密码加密存储](#2.3.2 密码加密存储)[2.3.3 登录接口实现](#2.3.3 登录接口实现)[2.3.4 认证过滤器](#2.3.4 认证过滤器)[2.3.5 退出登录?](#2.3.5 退出登录?)三、授权[3.1 权限系统作用](#3.1 权限系统作用)3.2?授权基本流程3.3?授权实现[3.2.1 限制访问资源所需权限](#3.2.1 限制访问资源所需权限)[3.2.2 封装权限信息](#3.2.2 封装权限信息)[3.2.3 从数据库查询权限信息](#3.2.3 从数据库查询权限信息)[3.2.3.1 RBAC权限模型](#3.2.3.1 RBAC权限模型)[3.2.3.2 代码实现](#3.2.3.2 代码实现)?四、自定义失败处理[4.1 创建自定义实现类](#4.1 创建自定义实现类)[4.2 将实现类配置给SpringSecurity](#4.2 将实现类配置给SpringSecurity)五、跨域问题解决方案?六、其他权限校验方法?七、自定义权限校验方法八、基于配置的权限控制* * 一、SpringSecurity简介------------------Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。 一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。 一般Web应用的需要进行认证和授权。 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户* 授权:经过认证后判断当前用户是否有权限进行某个操作而认证和授权也是SpringSecurity作为安全框架的核心功能。### 1.1 入门Demo依赖如下: org.springframework.boot spring-boot-starter-security 引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。 必须登陆之后才能对接口进行访问。访问 localhost:8080/logout 这个链接可以对其进行退出操作。Ps:以上过程了解即可,因为我们实际Web项目中,一般采用我们自定义的登录验证授权方案,不会采取SpringSecurity框架提供的默认方案。二、认证----登录校验流程:--------------------------------------------------------------------------------为了实现以上这种过程,我们需要先对SpringSecurity默认的流程进行了解,才可以对其进行修改,实现我们自定义的方案。### 2.1 SpringSecurity完整流程SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器:图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示:* UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。* ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。* FilterSecurityInterceptor:负责权限校验的过滤器。我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。如果想查看所有的过滤器,可以通过获取Spring容器,Debug方式来查看:### 2.2 认证流程详解箭头代表该方法属于这个实现类的。概念速查:* Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。* AuthenticationManager接口:定义了认证Authentication的方法* UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。* UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。### 2.3 自定义认证实现> 登录 ①自定义登录接口调用ProviderManager的方法进行认证 如果认证通过生成jwt 把用户信息存入redis中②自定义UserDetailsService在这个实现类中去查询数据库> 校验 ①定义Jwt 认证过滤器获取token 解析token获取其中的userid从redis中获取用户信息存入SecurityContextHolder> 这里为什么要存入SecurityContextHolder中呢?我们自定义的JWT过滤器的时候,肯定是需要将这个JWT过滤器放在UsernamePasswordAuthenticationFilter前的,这时我们将从redis获取的用户信息存入SecurityContextHolder才行,否则后续过滤器在进行校验的时候,可能会因为SecurityContextHolder中没有对应的值而判断当前访问用户验证不通过。#### 2.3.1 数据库校验用户> 定义Mapper接口 package com.example.springsecurity_demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.springsecurity_demo.domain.User; public interface UserMapper extends BaseMapper { }> 定义User实体类 package com.example.springsecurity_demo.domain; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Date; @Data @AllArgsConstructor @NoArgsConstructor @TableName("sys_user") public class User implements Serializable { private static final long serialVersionUID = -40356785423868312L; / * 主键 / @TableId private Long id; / * 用户名 / private String userName; / * 昵称 / private String nickName; / * * 密码 / private String password; / * * 账号状态(0正常 1停用) / private String status; / * * 邮箱 / private String email; / * * 手机号 / private String phonenumber; / * * 用户性别(0男,1女,2未知) / private String sex; / * * 头像 / private String avatar; / * * 用户类型(0管理员,1普通用户) / private String userType; / * * 创建人的用户id / private Long createBy; / * * 创建时间 / private Date createTime; / * * 更新人 / private Long updateBy; / * * 更新时间 / private Date updateTime; / * * 删除标志(0代表未删除,1代表已删除) / private Integer delFlag; }> 配置Mapper扫描 package com.example.springsecurity_demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication @MapperScan("com.example.springsecurity_demo.mapper") public class SpringSecurityDemoApplication { public static void main(String\[\] args) { ConfigurableApplicationContext run = SpringApplication.run(SpringSecurityDemoApplication.class, args); System.out.println(1); } } > 核心代码实现 package com.example.springsecurity_demo.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.springsecurity_demo.domain.LoginUser; import com.example.springsecurity_demo.domain.User; import com.example.springsecurity_demo.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; 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.Objects; @Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查询用户信息 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserName,username); User user = userMapper.selectOne(queryWrapper); // 如果没有查询到用户就抛出异常 if (Objects.isNull(user)) { throw new RuntimeException("用户名或者密码错误"); } //TODO 查询对应的权限信息 return new LoginUser(user); } } 因为UserDetailsService方法的返回值是UserDetails(接口):所以需要定义一个类,实现该接口,把用户信息封装在其中。 package com.example.springsecurity_demo.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; @Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.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; } } 注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如:!\[\](https://i-blog.csdnimg.cn/blog_migrate/6dbdcdf8bf86ec897712ba2b6f80f31f.png)这样登陆的时候就可以用fox作为用户名,123作为密码来登陆了。#### 2.3.2 密码加密存储实际项目中我们不会把密码明文存储在数据库中。 默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该 PasswordEncoder来进行密码校验。 我们可以定义一个SpringSecurity的配置类:低版本配置如下: @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }以下是高版本的SpringSecurity(SpringBoot 3 用以下配置): package com.example.springsecurity_demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } #### 2.3.3 登录接口实现接下来我们需要自定义登录接口,这里我们需要让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。(毕竟登录接口如果还需要权限访问,那么就很奇怪了)在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器中。认证成功的话要生成一个JWT,放入响应中返回,并且为了让用户下回请求时需要通过JWT识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。> **Contorller类如下:** package com.example.springsecurity_demo.controller; import com.example.springsecurity_demo.domain.ResponseResult; import com.example.springsecurity_demo.domain.User; import com.example.springsecurity_demo.service.LoginService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class LoginController { @Autowired private LoginService loginService; @PostMapping("/user/login") public ResponseResult login(@RequestBody User user){ return loginService.login(user); } }Ps:虽然字段声明的类型是 LoginService,但实际上注入的是 LoginServiceImpl。这是因为 LoginServiceImpl 实现了 LoginService 接口,因此它被视为 LoginService 的一种类型。 import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { //创建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http //关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous() // .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); // } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } } 实现类如下: import com.sangeng.domain.LoginUser; import com.sangeng.domain.ResponseResult; import com.sangeng.domain.User; import com.sangeng.service.LoginServcie; import com.sangeng.utils.JwtUtil; import com.sangeng.utils.RedisCache; 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.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.Objects; @Service public class LoginServiceImpl implements LoginServcie { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult login(User user) { //AuthenticationManager authenticate进行用户认证 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); //如果认证没通过,给出对应的提示 if(Objects.isNull(authenticate)){ throw new RuntimeException("登录失败"); } //如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userid = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userid); Map

相关推荐
2601_9620697718 分钟前
SpringBoot开发——初步了解SpringBoot
java·spring boot·后端
山峰哥20 分钟前
数据库工程与查询优化案例深度复盘‌
数据库·sql·oracle·编辑器·深度优先·宽度优先
IT大白鼠20 分钟前
MSF数据库与资产管理——专业渗透测试流程
数据库·安全·msf
旧梦952725 分钟前
Java 单例模式:从基础到实战的完整指南
java·开发语言·单例模式
IvorySQL31 分钟前
PostgreSQL 日报|内核多项缺陷修复与数据校验补丁推进(8 月 27 日)
数据库·postgresql·区块链
jay神34 分钟前
【计算机毕业设计】基于Spring Boot的宠物领养管理系统
java·spring boot·后端·vue·毕业设计·宠物
cfm_291439 分钟前
5种Java并发同步工具全解
java
十五喵源码网43 分钟前
基于SpringBoot2+vue2的健身房管理系统
java·毕业设计·springboot·论文笔记
风吹心凉43 分钟前
Agent大脑-RAG知识库
数据库