什么是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等框架
    • 企业级安全方案:支持OAuth2,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来进行拦截

  • 如图所示:一个请求要访问到后端,就要从左到右经过这些过滤器.其中绿色的过滤器是负责认证的过滤器,蓝色部分是负责异常处理的过滤器,橙色是负责权限校验的拦截器.
  • 对于我们而言,只需关注UserNamePasswordAuthenticationFilter**->负责登入认证和FilterSecurityInterceptor->负责授权**

对于Spring Security,掌握了过滤器和组件就完全掌握了Spring Security.其使用方法就是对过滤器和组件进行扩展

Spring Security入门

  • 添加Spring Security相关依赖

    				<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    </dependency> <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.6</version>
    </dependency> <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
    </dependency> <dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
    </dependency>

编写UserController进行测试

启动项目,访问localhost:8080进行测试,其会自动跳转到localhost:8080/login登入页面

  • 默认的用户名:user
  • 其密码会在项目启动时打印在控制台

  • 输入正确的用户名和密码时,即可成功访问UserController中的get方法→说明Spring Security保护生效
  • 当然在实际开发中,这种默认配置是不存在的,我们需要扩展这些组件

用户认证

用户认证的流程

核心接口:

  • Authentication接口,表示当前访问系统的用户,封装了用户的信息,其实现类为UsernamePasswordAuthenticationToken

  • AuthenticationManager接口,其定义了Authentication的方法

  • UserDetailsService接口,加载用户特定数据的核心接口,其中定义了一个根据用户名查询用户信息的方法

  • UserDetails接口,提供核心用户信息.将UserDetailsService中获取的信息封装为UserDetails对象返回.并将其封装至Authentication对象中

    UserDetails中的基本方法:

用于认证的核心组件:

对于系统来说,同一时间会有多个用户正在使用,那么如何确认哪个用户正在请求登录接口是登录认证的核心目的.Spring Security提出了:当前登录用户/当前认证用户,Spring Security中使用Authentication来存储认证信息,表示当前用户

在Spring boot中使用安全上下文SecurityContext来获取Authentication,SecurityContext交有SecurityContextHolder来管理,使用以下方法即可获取Authentication

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

  • Spring Security三个认证核心组件为:

    1. Authentication:存储认证信息的上下文,代表当前用户
    2. SecurityContext:上下文对象,用来获取Authentication
    3. SecurityContextHolder:上下文管理对象,用来获取SecurityContext

认证逻辑

  • AuthenticationManager是Spring Security用于执行身份验证的组件,其authenticate方法可以完成认证.Spring Security默认的认证方法是在UsernamePasswordAuthenticationFilter这个过滤器中进行认证的

    关键代码如下:

            // 创建用户认证令牌,使用用户名和密码作为凭证
    UsernamePasswordAuthenticationToken token =
    new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()); // 通过认证管理器执行Spring Security认证流程,返回包含用户详情的认证对象
    Authentication authenticate = authenticationManager.authenticate(token);

加密器PasswordEncoder

  • passwordEncoder在Spring Security中用于处理密码加密存储和验证.
  • 它可以负责将用户提交的明文密码转换为不可逆的加密字符串(如BCrypt算法),之后便将密码存储到数据库中
  • 在用户登录时,验证用户输入的明文密码是否与存储的加密密码一致

若需要自定义加密方法,我们可以编写自定义加密器CustomPasswordEncoder

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中注册新的加密器

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfiguration { // 密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new CustomPasswordEncoder();
}
}

直接使用BCryptPasswordEncoder加密器

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfiguration { // 密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

自定义登录接口

  • 首先要重写SecuritySpring中的方法,可以自己写使用@Bean注册,也可以重写WebSecurityConfigurerAdapter接口的方法

由于Spring Security会对每一个接口都会进行认证,有些接口需要放行,直接让用户访问,我们就得在config()中进行放行

  • 接着需要把AuthenticationManager注入容器→因为要使用其的authenticate方法进行验证
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()方法来实现登录逻辑
    public interface ISysUserService extends IService<SysUser> {
    
        /**
    * @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的认证体系中
    // 忽略未知的属性,避免序列化时出现异常
    @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<? extends org.springframework.security.core.GrantedAuthority>
    **/
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
    return null;
    }
    }
    • 我们还可以使用另一种写法,更加贴合实际的开发环境,若觉得实现UserDetails接口比较繁琐,我们可以继承Spring Security提供的org.springframework.security.core.userdetails.User类.其内部已经帮我们实现了UserDetails接口,省去了大量重写工作
    @Getter
    @Setter
    public class LoginUser extends User { private SysUser user; /**
    * 构造函数
    *
    * @param user 用户信息
    * @param authorities 权限列表
    */
    public LoginUser(SysUser user, Collection<? extends GrantedAuthority> authorities) {
    super(user.getUsername(), user.getPassword(), authorities);
    this.user = user;
    } }
    • 推荐使用实现UserDetails接口的方法.继承User类会受父类牵制
  • UserDetailsServiceImpl

    • UserDetailsServiceImpl实现UserDetails了,扮演着 用户数据与认证流程之间的桥梁角色,其中重写loadUserByUsernmae()核心方法来实现从DB中获取用户信息,并将其封装为 Spring Security可识别的安全对象→UserDetails
    @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<SysUser> 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的底层操作
    @Service("userService")
    public class UserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> 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)决定其能允许访问的资源或操作.

LoginUser改造

  • 在LoginUser中添加permissions字段→用于存储用户权限信息,authorities字段→存储springsecurity中所需的集合

private SysUser user; // 存储用户权限信息
private List<String> permissions; //防止出现序列化问题 @JsonIgnore
// 存储SpringSecurity所需要的权限信息的集合
private List<GrantedAuthority> authorities; public LoginUser(List<String> permissions, SysUser user) {
this.permissions = permissions;
this.user = user;
}
  • 重写getAuthorities()方法用于将permissions中的权限封装为GrantedAuthority对象
    @Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (Objects.isNull(authorities)) {
authorities = new ArrayList<>();
}
// 将权限字符串封装成GrantedAuthority对象
permissions.forEach(permission ->
authorities.add(new SimpleGrantedAuthority(permission))); return authorities;
}

构建查询权限的mapper

  • 在userMapper下创建findUserPermListByUserId()方法用于获取用户权限
public interface UserMapper extends BaseMapper<SysUser> {
/**
* 根据用户id查询权限列表
*
* @param userId 用户id
* @return permList 权限列表
*/
List<String> findUserPermListByUserId(Long userId);
}

改造UserDetailsServiceImpl

  • 调用findUserPermListByUserId()查询权限信息,放回到LoginUser中
        List<String> userPermList = userMapper.findUserPermListByUserId(sysUser.getUserId());

        // 返回用户信息和权限列表
return new LoginUser(userPermList, sysUser);

授权实现

在启动类进行配置

  • 在启动类上配置注解启动,来判断用户对某个控制层的方法是否具有访问权限
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
}

在controller的方法进行配置

  • 在方法上加上@PreAuthorize标签控制接口权限

自定义验证方法的实现方法

  • 编写AuthPermissonUtils方法实现权限验证逻辑
@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<String> permissions = loginUser.getPermissions();
return permissions.contains(permission);
}
}
@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对象的方法去进行异常处理

    @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对象的方法进行异常处理

    @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中进行配置

    • 注入处理器
        @Resource
    private AccessDeniedHandlerImpl accessDeniedHandler; @Resource
    private AuthenticationEntryPointImpl authenticationEntryPoint;
    • 在使用http进行配置
            // 配置异常处理器
    http
    .exceptionHandling()
    .accessDeniedHandler(accessDeniedHandler)
    .authenticationEntryPoint(authenticationEntryPoint);

Spring Security认证与授权的更多相关文章

  1. spring-security-4 (4)spring security 认证和授权原理

    在上一节我们讨论了spring security过滤器的创建和注册原理.请记住springSecurityFilterChain(类型为FilterChainProxy)是实际起作用的过滤器链,Del ...

  2. 11.spring security 认证和授权简单流程了解

    1.总结:昨天主要是对WebSecurityConfigurerAdaptor的三个函数的区分以及了解了spring security的认证和授权流程:再就是动手使用了下thymeleaf和freeM ...

  3. Spring Security认证配置(三)

    学习本章之前,可以先了解下上篇Spring Security认证配置(二) 本篇想要达到这样几个目的: 1.登录成功处理 2.登录失败处理 3.调用方自定义登录后处理类型 具体配置代码如下: spri ...

  4. Spring Security 解析(一) —— 授权过程

    Spring Security 解析(一) -- 授权过程   在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决定先把Spring Security .S ...

  5. spring security 认证源码跟踪

    spring security 认证源码跟踪 ​ 在跟踪认证源码之前,我们先根据官网说明一下security的内部原理,主要是依据一系列的filter来实现,大家可以根据https://docs.sp ...

  6. spring boot:spring security实现oauth2授权认证(spring boot 2.3.3)

    一,oauth2的用途? 1,什么是oauth2? OAuth2 是一个开放标准, 它允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像.照片.视频等), 在这个过程中无须将用户名和密码 ...

  7. 转 - spring security oauth2 password授权模式

    原贴地址: https://segmentfault.com/a/1190000012260914#articleHeader6 序 前面的一篇文章讲了spring security oauth2的c ...

  8. Spring Security认证配置(二)

    学习本章之前,可以先了解下上篇Spring Security基本配置. 本篇想要达到这样几个目的: 1.访问调用者服务时,如果是html请求,则跳转到登录页,否则返回401状态码和错误信息 2.调用方 ...

  9. spring security认证

    1 开发基于表单的认证 Spring security核心的功能 认证(你是谁?) 授权(你能干什么?) 攻击防护(防止伪造身份) spring security实现了默认的用户名+密码认证,默认用户 ...

  10. [转]Spring Security 可动态授权RBAC权限模块实践

    RBAC:基于角色的访问控制(Role-Based Access Control) 先在web.xml 中配置一个过滤器(必须在Struts的过滤器之前) <filter> <fil ...

随机推荐

  1. 玩转云端 | 如何防爬虫?天翼云边缘安全加速平台AccessOne带你涨姿势!

    玩转云端 | 如何防爬虫?天翼云边缘安全加速平台AccessOne带你涨姿势!

  2. Luogu P4287 SHOI2011 双倍回文 题解 [ 紫 ] [ manacher ]

    双倍回文:回文子串结论的经典应用. 结论 先放本题最关键的结论:一个字符串本质不同的回文子串最多只有 \(n\) 个. 考虑如何证明: 假设我们一个一个地在当前字符串(黑色部分)的结尾加入字符(红色部 ...

  3. 流程控制之break、continue和goto

    #### 实例1: ```javapackage com.yeyue.struct; public class BreakDemo { public static void main(String[] ...

  4. 保持Android Service在手机休眠后继续运行的方法

    保持Android Service在手机休眠后继续运行的方法   下面小编就为大家分享一篇保持Android Service在手机休眠后继续运行的方法,具有很好的参考价值,希望对大家有所帮助.一起跟随 ...

  5. 微信小程序之java服务端获取openid

    微信小程序越来越热,最近团队写了一个小程序,这篇博客我将讲一下怎么通过java服务端获取到用户的openid. api文档的授权登陆地址: http://developers.weixin.qq.co ...

  6. Scala样例类及底层实现伴生对象

    package com.wyh.day01 /** * 样例类的使用 * 1.使用case修饰类 * 2.不需要写构造方法,getter,setter方法,toString方法 * 3.直接通过对象名 ...

  7. 浅谈Tox之一

    本文分享自天翼云开发者社区<浅谈Tox之一>,作者:Moonriver What is tox? tox是通用的virtualenv管理和测试命令行工具,可用于: 使用不同的Python版 ...

  8. 全程不用写代码,我用AI程序员写了一个飞机大战

    前言 还在为写代码薅头发吗?还在为给出的需求无处下手而发愁吗?今天宏哥分享一款开发工具的插件,让你以后的编程变得简单起来. 作为一个游戏编程小白,能完成自己工作就不错了,还能玩别的,这在以前想都不敢想 ...

  9. Linux系列:如何用 C#调用 C方法造成内存泄露

    一:背景 1. 讲故事 好久没写文章了,还是来写一点吧,今年准备多写一点 Linux平台上的东西,这篇从 C# 调用 C 这个例子开始.在 windows 平台上,我们常常在 C++ 代码中用 ext ...

  10. vue+elementUI 表格操作按钮添加loading

    前言 表格操作栏,某个操作需要异步请求才能做跳转等 方案 整个列表每行都加一个loading字段,不够优雅 利用$set方法 改变当前行当前按钮loading,可行(代码如下) //按钮 row.lo ...