SpringSecurity-firstMeeting
SpringSecurity是Spring中的安全框架,相比于Shiro,它提供了更丰富的功能,社区资源也比较多。
一般大型的项目都是用SpringSecurity做安全框架,小项目用Shiro比较上手比较简单。
认证:验证当前用户访问系统的是不是本系统用户,并非要确认具体是那个用户。
授权:经过认证后判断当前用户是否有权限进行某个操作
1、Hello Word
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
只是要是引入SpringSecurity依赖,项目就具备SpringSecurity的安全机制,因此打开任何链接都会跳转到SpringSecurity的默认的登录页面
# 登录: http://localhost:8080/login # 退出 http://localhost:8080/logout # 默认Username user # 默认Password(项目启动时,控制台输出) fce1e9d5-3e8c-48e1-be8c-ccae4eb01f5b
2、初步原理
SpringSecurity的原理就是一个过滤器链,内部包含了提供各种功能的过滤器。
# 核心过滤器 UsernamePasswordAuthentticationFilter:负责处理我们在登陆页面填写用户名和密码的登录请求。 ExceptionTransactionFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。 FilterSecurityInterceptor:负责处理权限校验的过滤器。
# 重要接口 Authentication接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息。 AuthenticationManager接口:定义了认证Authentication的方法 UserDetailsService接口:加载用户特定数据的核心接口,里面定义了一个根据用户名查询用户信息的方法。 UserDetails接口:提供核心用户信息,通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回,然后将这些信息封装到Authentication对象中。
3、认证
3.1 自定义认证
实现UserDetailsService接口
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private SysUserInfoMapper sysUserInfoMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查询用户信息 LambdaQueryWrapper<SysUserInfo> wrapper = Wrappers.lambdaQuery(SysUserInfo.class); wrapper.eq(SysUserInfo::getUsername,username); SysUserInfo userInfo = sysUserInfoMapper.selectOne(wrapper); // 如果没有查询到用户就抛出异常 if (Objects.isNull(userInfo)) { // SpringSecurity认证过程中如果发送异常,为认证失败 throw new RuntimeException("用户名不存在!"); } // 查询对应的权限信息 // 把数据封装成UserDetails(手动实现这个接口) return new LoginUser(userInfo); } }
实现UserDetails接口
错误:There is no PasswordEncoder mapped for the id "null"
没有配置加密规则,需要在密码前加入{noop}表示没有加密规则,如{noop}123456
。
3.2 密码加密 存储
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password(id表示加密规则)。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要把BCryptPasswordEncoder对象注入到Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类继承WebSecurityConfigurerAdapter。
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
用户注册的时候直接使用这个bean的encode(String password)方法加密存入数据库即可。
3.3 自定义登录接口
登录
① 自定义登录接口
调用ProviderManager的方法进行认证如果认证通过生成jwt
把用户信息存入redis中
② 自定义UserDetailsService,在这个类中查询数据库(也就是3.1的做法)
校验
① 定义jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHolder
以上只是一种思路,主要看ProviderManager和SecurityContextHolder
代码实现
在刚才的SpringSecurity配置类中重写里面的方法,加@Bean注解,注入ProviderManager这个Bean去执行认证
登录业务
登录接口
# 传用户名密码 /user/login
认证过滤器
还需要把这个过滤器放在Security过滤器链中并且在UserName...Filter之前执行,在Security配置文件中配置如下
// 将jwt鉴权过滤器放在Security过滤器链中 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
3.4 退出登录
@Override public String logout() { // 获取SecurityContextHolder中的用户ID Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Integer id = loginUser.getSysUserInfo().getId(); // 删除redis中的值 return "注销成功"; }
4、权限
不同的用户实现不同的功能。
4.1 授权基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限。
在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
4.2 授权实现
限制访问资源所需权限
SpringSe为我们提供了基于注解的权限控制方案,这也是我们项目中主要采取的方式。
使用之前开启相关的配置。
@EnableGlobalMethodSecurity(prePostEnabled = true)
然后就可以使用对应的注解
@PreAuthorize("hasAuthority('test')") @RequestMapping("/hello") public String hello(){ return "Hello World"; }
完善登录的时候查询权限的代码,同时还有token解析时完善将权限保存至SecurityContextHolder中。
完善UserDetails类的权限方法
4.3 RBAC权限模型
RBAC权限模型(Role-Based Access Control)即:基于角色权限控制。这是目前最常被开发者使用也是相对容易、通用权限模型。
一个用户可以有多个角色,一个角色可以有多个权限。
4.4 权限获取方式替换为数据库查询
略
4.5 自定义失败处理
我们在还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回我们的接口一样结构的JOSN,这样可以然前端能对相应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权失败的过程中出现了异常会被ExceptionTransactionFilter捕获到。在ExceptionTransactionFilter中会去判断是认证失败还是授权失败出现的异常情况。
如果是认证过程出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法进行异常处理。
所以我们如果需要自定异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
① 自定义实现类
AuthenticationEntryPoint
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { // 处理异常 JSONObject jsonObject = new JSONObject(); jsonObject.put("code", HttpStatus.UNAUTHORIZED.value()); jsonObject.put("msg", "用户认证失败,请从新登录"); String jsonStr = jsonObject.toJSONString(); httpServletResponse.setStatus(200); httpServletResponse.setContentType("application/json"); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.getWriter().print(jsonStr); } }
AccessDeniedHandler
@Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { JSONObject jsonObject = new JSONObject(); jsonObject.put("code", HttpStatus.FORBIDDEN.value()); jsonObject.put("msg", "暂无权限!"); String jsonStr = jsonObject.toJSONString(); httpServletResponse.setStatus(200); httpServletResponse.setContentType("application/json"); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.getWriter().print(jsonStr); } }
② 配置给SpringSecurity
配置类中配置,我们可以使用HttpSecurity对象的方法去配置。
4.5 自定义权限处理
我们定义自己的权限检验方法,在@PreAuthorize注解中使用我们的方法
在SPEL表达式中使用@ex相当于获取容器中bean的名字为ex的对象。然后在调用这个对象的方法
@Component("ex") public class ExpressionRoot { public boolean hasAuthority(String authority) { // 获取当前用户的权限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser principal = (LoginUser) authentication.getPrincipal(); List<String> permissions = principal.getSysUserInfo().getPermissions(); // 判断用户权限集合中是否存在 return permissions.contains("test"); } }
使用
也可以基于配置去设置权限
5、CSRF
csrf是指跨域请求伪造,是web常见的攻击手段之一。
SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储在cookie中,并且需要前端代码去吧token设置到请求头中才行,所以CSRF攻击也就不用担心了。
访问B第三方网站也就超链接的跳转,也就不会有A的Cooike了。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤