Spring Security认证与授权全解析

笔记哥 / 04-18 / 7点赞 / 0评论 / 437阅读
# 什么是Spring Security > Spring Security是基于Spring框架,提供了一套Web应用安全性框架.专门为Java应用提供**用户认证(Authentication)和用户授权(Authorization),支持单体应用到微服务的全场景安全防护** ## 认证(Authentication) - 验证某个用户是否为系统中的合法主体,即确认该用户是否可以访问此系统.认证一般需要用户提供用户名与密码,提供校验用户名与密码完成认证过程 - 简单来说→**认证是判断该用户是否能登录;** - 关键要素:1.`Principal`:用户主体(如用户名)2.`Credentials`:验证凭证(如用户密码)3.`Authorities`:用户权限集合 ## 授权(Authorization) - 是指某个用户是否有权限执行某个操作.在同一系统中,不同用户所具有的权限是不同的.如对某一文件,有些用户只能读不能修改,而有些用户既可读也可以修改.某个角色都有一系列的权限 - 简单来说→**授权是判断该用户是否有权限去做特定的操作;** - 关键要素:1.角色(`Role`):用户分组标识2.权限(`Permission`):具体操作权限 ## 优势和缺点 - 优势 - 深度与Spring整合:无缝支持`Spring Boot、Spring MVC、Spring Data`等框架 - 企业级安全方案:支持O`Auth2,SAML,LDAP,JWT`等协议,满足复杂安全需求 - 旧版本无法脱离Web环境 - 新版本对框架进行分层提取,分为核心板块和Web板块, - 缺点 - 性能开销:默认开启CSRF,Session管理等特性,对高性能场景需手动优化 - 配置复杂度高:默认配置覆盖大量安全规则,需要显式覆盖才能简化 ## 与Shiro对比 | 对比维度 | Spring Security | **Shiro** | | --- | --- | --- | | 生态整合 | 深度集成 Spring 技术栈 | 需手动整合Spring,对非 Spring 项目更友好 | | 微服务支持 | 天然支持 Spring Cloud Security | 需自行实现分布式会话和权限管理 | | **典型场景** | 企业级应用,微服务架构,需要 OAuth2 的 SaaS 系统 | 中小型 Web 应用,移动端后台,快速开发项目 | - 一般来说常见的安全管理技术栈组合是这样: - SSM+Shiro - Spring Boot/Spring Cloud+Spring Security # Spring Security实现原理 > > > 对Web资源最好的保护是Filter,对方法调用的最好方法是AOP > - Spring Security进行认证和权限检验时就是通过一系列的Filter来进行拦截 ![](https://cdn.res.knowhub.vip/c/2504/19/13c6e74c.png?G1YAAMTsdJxIfFKi26hD2jvFHc2ARBZBpYT1es9Z%2byb6fgdD4zNan74%2f%2fKX16SQKkZQJjIsVwSeDMHOuaqGa5gQrJa7h) - 如图所示:一个请求要访问到后端,就要从左到右经过这些过滤器.**其中绿色的过滤器是负责认证的过滤器,蓝色部分是负责异常处理的过滤器,橙色是负责权限校验的拦截器.** - 对于我们而言,只需关注**`UserNamePasswordAuthenticationFilter**->`**负责登入认证和`FilterSecurityInterceptor`**->**负责授权\*\* 对于Spring Security,掌握了过滤器和组件就完全掌握了Spring Security.其使用方法就是对过滤器和组件进行扩展 ## Spring Security入门 - 添加Spring Security相关依赖 ```csharp org.springframework.boot spring-boot-starter-security io.jsonwebtoken jjwt-api 0.12.6 io.jsonwebtoken jjwt-impl 0.12.6 runtime io.jsonwebtoken jjwt-jackson 0.12.6 runtime ``` > > > 编写UserController进行测试 > ![](https://cdn.res.knowhub.vip/c/2504/19/0079963a.png?G1YAAMTsdJxIfIK026hD2jvFHc2ARBZBpYT1es9Z%2byb6fgdD4zNan74%2f%2fKX16SQKkVQIjMyK4JNBmKGoVzDLWUWqxTUc) > > > 启动项目,访问localhost:8080进行测试,其会自动跳转到localhost:8080/login登入页面 > ![](https://cdn.res.knowhub.vip/c/2504/19/4a40e87a.png?G1cAAETn9LwUKOjIvtMdbIlTE20GLNIIKiWs13vO2jfR9wcYlp%2fR%2boz94TetzyA1qEolMAobUhCHMuuF6kncWYWBktcI) - 默认的用户名:user - 其密码会在项目启动时打印在控制台 ![](https://cdn.res.knowhub.vip/c/2504/19/f2f1c386.png?G1YAAETn9LwUKLDovtMdbIlTE20GJLIIKiWs13vO2jfR9wcYlp%2fR%2boz94S%2btzyA1qMpFYDgbkpcCZXZhQSpeqppVy2sE) - 输入正确的用户名和密码时,即可成功访问UserController中的get方法→说明Spring Security保护生效 - 当然在实际开发中,这种默认配置是不存在的,我们需要扩展这些组件 ![](https://cdn.res.knowhub.vip/c/2504/19/d28aa34d.png?G1YAAMTW3Dgp8JBS22gDdWfqnTUDElkElRLW6%2fn%2ftS%2bi9wswLN%2bj9Rn7w19an0FqUJVCYBxsSF4cysJQq0mLivtZPa8R) ## 用户认证 用户认证的流程 ![](https://cdn.res.knowhub.vip/c/2504/19/9522ef9d.png?G1YAAMTmtHFS4PDfRjvQJlZNtBmQyCKolLBe7737NKLvNzCCf2Yfy86Hv%2fSxjDRAVRKBETnAeSlQFkEt6gQx51qT%2bj0N) 核心接口: - `Authentication`接口,表示当前访问系统的用户,封装了用户的信息,其实现类为`UsernamePasswordAuthenticationToken` - `AuthenticationManager`接口,其定义了Authentication的方法 - `UserDetailsService`接口,加载用户特定数据的核心接口,其中定义了一个根据用户名查询用户信息的方法 - `UserDetails`接口,提供核心用户信息.将UserDetailsService中获取的信息封装为`UserDetails`对象返回.并将其封装至Authentication对象中 UserDetails中的基本方法: ![](https://cdn.res.knowhub.vip/c/2504/19/502735a4.png?G1cAAETn9LwUKKDLvtMdbIlTE20GLNIIKiWs13vO2jfR9wcYlp%2fR%2boz94TetzyA1qEohMJwNKcgFZYFUrQlicDP1mtcI) 用于认证的核心组件: > > > 对于系统来说,同一时间会有多个用户正在使用,那么如何确认哪个用户正在请求登录接口是登录认证的核心目的.Spring Security提出了:**当前登录用户/当前认证用户**,Spring Security中使用`Authentication`来存储认证信息,表示当前用户 > > > > 在Spring boot中使用安全上下文`SecurityContext`来获取`Authentication`,`SecurityContext`交有`SecurityContextHolder`来管理,使用以下方法即可获取`Authentication` > ```csharp Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); ``` - Spring Security三个认证核心组件为: 1. `Authentication`:存储认证信息的上下文,代表当前用户 2. `SecurityContext`:上下文对象,用来获取`Authentication` 3. `SecurityContextHolder`:上下文管理对象,用来获取SecurityContext ### 认证逻辑 - `AuthenticationManager`是Spring Security用于执行身份验证的组件,其`authenticate`方法可以完成认证.Spring Security默认的认证方法是在`UsernamePasswordAuthenticationFilter`这个过滤器中进行认证的 关键代码如下: ```csharp // 创建用户认证令牌,使用用户名和密码作为凭证 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()); // 通过认证管理器执行Spring Security认证流程,返回包含用户详情的认证对象 Authentication authenticate = authenticationManager.authenticate(token); ``` ### 加密器PasswordEncoder - `passwordEncoder`在Spring Security中用于处理密码加密存储和验证. - 它可以负责将用户提交的明文密码转换为不可逆的加密字符串(如BCrypt算法),之后便将密码存储到数据库中 - 在用户登录时,验证用户输入的明文密码是否与存储的加密密码一致 > > > 若需要自定义加密方法,我们可以编写自定义加密器`CustomPasswordEncoder` > ```csharp public class CustomPasswordEncoder implements PasswordEncoder { // 自定义密码加密方式,使用MD5加密算法 @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes())).equals(encodedPassword); } // 自定义密码加密方式,使用MD5加密算法 @Override public String encode(CharSequence rawPassword) { return Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes())); } } ``` > > > 并在SecurityConfig中注册新的加密器 > ```csharp @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfiguration { // 密码加密器 @Bean public PasswordEncoder passwordEncoder() { return new CustomPasswordEncoder(); } } ``` > > > 直接使用`BCryptPasswordEncoder`加密器 > ```csharp @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfiguration { // 密码加密器 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` ## 自定义登录接口 - 首先要重写`SecuritySpring`中的方法,可以自己写使用@Bean注册,也可以重写`WebSecurityConfigurerAdapter`接口的方法 > > > 由于Spring Security会对每一个接口都会进行认证,有些接口需要放行,直接让用户访问,我们就得在`config()`中进行放行 > - 接着需要把`AuthenticationManager`注入容器→因为要使用其的authenticate方法进行验证 ```csharp public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 配置密码编码器 * * @return */ @Bean public PasswordEncoder passwordEncoder() { return new CustomPasswordEncoder(); } /** * 配置HTTP安全设置 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http // 禁用CSRF保护(常用于API场景) .csrf().disable() // 配置会话管理为无状态(不创建和使用HTTP Session) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() // 配置请求授权规则 .authorizeRequests() // 允许匿名访问登录端点 .antMatchers("/user/login").permitAll() // 所有其他请求需要认证 .anyRequest().authenticated(); } /** * 暴露AuthenticationManager * * @return * @throws Exception */ @Bean @Override protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } } ``` - IUserService - 在其中编写`login()`方法来实现登录逻辑 ```csharp public interface ISysUserService extends IService { /** * @description: 登录 * @author: HYJ * @date: 2025/4/15 0:01 * @param: [user] * @return: edu.ptu.springsecurity.common.AjaxResult **/ AjaxResult login(SysUser user); } ``` - LoginUser - `LoginUser`实现`UserDetails`接口,重写其中的方法.将业务数据衔接到Spring Security的认证体系中 ```csharp // 忽略未知的属性,避免序列化时出现异常 @JsonIgnoreProperties(ignoreUnknown = true) public class LoginUser implements UserDetails { // 用户信息 private SysUser user; /** * @description: 账号是否未过期 * @author: HYJ * @date: 2025/4/15 0:02 * @param: [] * @return: boolean **/ @Override public boolean isEnabled() { return true; } /** * @description: 密码是否未过期 * @author: HYJ * @date: 2025/4/15 0:02 * @param: [] * @return: boolean **/ @Override public boolean isCredentialsNonExpired() { return true; } /** * @description: 账号是否未锁定 * @author: HYJ * @date: 2025/4/15 0:02 * @param: [] * @return: boolean **/ @Override public boolean isAccountNonLocked() { return true; } /** * @description: 账号是否未过期 * @author: HYJ * @date: 2025/4/15 0:02 * @param: [] * @return: boolean **/ @Override public boolean isAccountNonExpired() { return true; } /** * @description: 获取用户名 * @author: HYJ * @date: 2025/4/15 0:02 * @param: [] * @return: java.lang.String **/ @Override public String getUsername() { return user.getUsername(); } /** * @description: 获取密码 * @author: HYJ * @date: 2025/4/15 0:02 * @param: [] * @return: java.lang.String **/ @Override public String getPassword() { return user.getPassword(); } /** * @description: 获取权限 * @author: HYJ * @date: 2025/4/15 0:02 * @param: [] * @return: java.util.Collection **/ @Override public Collection getAuthorities() { return null; } } ``` - 我们还可以使用另一种写法,更加贴合实际的开发环境,若觉得实现`UserDetails`接口比较繁琐,我们可以继承Spring Security提供的`org.springframework.security.core.userdetails.User`类.其内部已经帮我们实现了`UserDetails`接口,省去了大量重写工作 ```csharp @Getter @Setter public class LoginUser extends User { private SysUser user; /** * 构造函数 * * @param user 用户信息 * @param authorities 权限列表 */ public LoginUser(SysUser user, Collection authorities) { super(user.getUsername(), user.getPassword(), authorities); this.user = user; } } ``` - 推荐使用实现`UserDetails`接口的方法.继承User类会受父类牵制 - UserDetailsServiceImpl - `UserDetailsServiceImpl`实现`UserDetails`了,扮演着 **用户数据与认证流程之间的桥梁**角色,其中重写`loadUserByUsernmae()`核心方法来实现从DB中获取用户信息,并将其封装为 Spring Security可识别的安全对象→`UserDetails` ```csharp @Service public class UserDetailsServiceImpl implements UserDetailsService { @Resource private UserServiceImpl userService; /** * @description: 加载用户信息 * @author: HYJ * @date: 2025/4/15 0:01 * @param: [username] * @return: org.springframework.security.core.userdetails.UserDetails **/ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); // 构建查询条件,根据用户名查询用户信息 wrapper.eq(SysUser::getUsername, username); SysUser user = userService.getOne(wrapper); // 检查用户是否存在 if (Objects.isNull(user)) { throw new RuntimeException("用户不存在"); } // 返回包含用户详细信息的 LoginUser 对象 return new LoginUser(user); } } ``` - UserServiceImpl - 实现login的底层操作 ```csharp @Service("userService") public class UserServiceImpl extends ServiceImpl implements ISysUserService { // 注入认证管理器,用于处理用户认证流程 @Resource private AuthenticationManager authenticationManager; // 注入Redis工具类,用于操作Redis缓存 @Resource private RedisUtil redisUtil; @Override public AjaxResult login(SysUser user) { // 创建用户认证令牌,使用用户名和密码作为凭证 UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()); // 通过认证管理器执行Spring Security认证流程,返回包含用户详情的认证对象 Authentication authenticate = authenticationManager.authenticate(token); // 判断认证是否成功 if (Objects.isNull(authenticate)) { throw new RuntimeException("登录失败,认证信息为空"); } LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); // 生成JWT令牌 String jwt = JWTUtil.createToken(loginUser.getUser()); // 认证成功,将用户信息存入Redis缓存 redisUtil.setCacheObject("user:" + user.getUserId(), user); return AjaxResult.success("登录成功", jwt); } } ``` ## 用户授权 - 用户授权是系统在确认用户身份后,根据其角色或者权限(Permissions)决定其能允许访问的资源或操作. ![](https://cdn.res.knowhub.vip/c/2504/19/e854b290.png?G1cAAETn9LwUKGjYvtMdbIlTE20GLNIIKiWs13vO2jfR9wcYlp%2fR%2boz94TetzyA1qEolMAobUhCHMqSaeBKDwPlSz2sE) ## LoginUser改造 - 在LoginUser中添加permissions字段→用于存储用户权限信息,authorities字段→存储springsecurity中所需的集合 ```csharp private SysUser user; // 存储用户权限信息 private List permissions; //防止出现序列化问题 @JsonIgnore // 存储SpringSecurity所需要的权限信息的集合 private List authorities; public LoginUser(List permissions, SysUser user) { this.permissions = permissions; this.user = user; } ``` - 重写`getAuthorities()`方法用于将permissions中的权限封装为GrantedAuthority对象 ```csharp @Override public Collection getAuthorities() { if (Objects.isNull(authorities)) { authorities = new ArrayList<>(); } // 将权限字符串封装成GrantedAuthority对象 permissions.forEach(permission -> authorities.add(new SimpleGrantedAuthority(permission))); return authorities; } ``` ## 构建查询权限的mapper - 在userMapper下创建`findUserPermListByUserId()`方法用于获取用户权限 ```csharp public interface UserMapper extends BaseMapper { /** * 根据用户id查询权限列表 * * @param userId 用户id * @return permList 权限列表 */ List findUserPermListByUserId(Long userId); } ``` ## 改造UserDetailsServiceImpl - 调用`findUserPermListByUserId()`查询权限信息,放回到LoginUser中 ```csharp List userPermList = userMapper.findUserPermListByUserId(sysUser.getUserId()); // 返回用户信息和权限列表 return new LoginUser(userPermList, sysUser); ``` ## 授权实现 ### 在启动类进行配置 - 在启动类上配置注解启动,**来判断用户对某个控制层的方法是否具有访问权限** ```csharp @SpringBootApplication @EnableGlobalMethodSecurity(prePostEnabled = true) public class SpringSecurityApplication { public static void main(String[] args) { SpringApplication.run(SpringSecurityApplication.class, args); } } ``` ## 在controller的方法进行配置 - 在方法上加上`@PreAuthorize`标签控制接口权限 ### 自定义验证方法的实现方法 - 编写`AuthPermissonUtils`方法实现权限验证逻辑 ```csharp @Component("auth") public class AuthPermissionUtils { /** * 判断是否有该权限 * * @param permission 权限字符串 * @return true 有该权限 false 没有该权限 */ public boolean hasPermission(String permission) { // 获取当前用户的权限信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); // 判断用户是否有该权限 List permissions = loginUser.getPermissions(); return permissions.contains(permission); } } ``` ```csharp @RestController @RequestMapping("/user") public class UserController { @Resource private IUserService sysUserService; @GetMapping("/living") @PreAuthorize("@auth.hasPermission('living')") public Result living() { return Result.success("可以开房"); } @GetMapping("/upgrade") @PreAuthorize("@auth.hasPermission('upgrade')") public Result upgrade() { return Result.success("可以升级房型"); } @GetMapping("/freeBreakfast") @PreAuthorize("@auth.hasPermission('freeBreakfast')") public Result freeBreakfast() { return Result.success("有免费早餐"); } @PostMapping("/login") public Result login(@RequestBody LoginRequest request) { return sysUserService.login(request); } } ``` # 异常处理方法 - 在遇到认证失败和授权失败时,我们希望可以放回与接口相同的json结构,这样可以让前端进行统一处理 - 如果**认证过程**中出现异常会被封装成AuthenticationException如何调用**`AuthenticationEntryPoint`**对象的方法去进行异常处理 ```csharp @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); response.getWriter().write( JSON.toJSONString(Result.fail(401, "用户身份认证不通过")) ); } } ``` - 如果授权过程中出现的异常就会被封装AccessDeniedException然后调用**AuthenticationEntryPoint**对象的方法进行异常处理 ```csharp @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); response.getWriter().write( JSON.toJSONString(Result.fail(401, "用户身份认证不通过")) ); } } ``` ## 在SecurityConfig中进行配置 - 注入处理器 ```csharp @Resource private AccessDeniedHandlerImpl accessDeniedHandler; @Resource private AuthenticationEntryPointImpl authenticationEntryPoint; ``` - 在使用http进行配置 ```csharp // 配置异常处理器 http .exceptionHandling() .accessDeniedHandler(accessDeniedHandler) .authenticationEntryPoint(authenticationEntryPoint); ```