Spring Security作为一款安全框架,我发现很多企业中都在使用,但是很多面试者或者工作年限较少的兄弟而言,有点犯怵,所以今天我带着大家详细的走一遍,包会的!!!
完整项目在最后,自行下载使用哦
一、学习思路
如果之前看过我其他文章的同学,可能会发现,我无论任何技术,学习思路都是一样的:
1、它是什么?
2、为什么要用它?
3、如何使用?
二、正文
接下来我们就带着这三个问题来学习SpringSecurity:
1、Spring Security是什么?
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
(白话文:Spring Security就是一个安全框架,包含登录认证(认证),访问鉴权(授权) 两大功能,所谓的登录认证,你可以理解为获取token,而访问鉴权,可以理解是检验token
2、为什么要用Spring Security?
这里我就不做官方接受了,没意义,我就白话文,以我的理解来描述,如果不想听,可以直接到第三部使用环节:
1、我们基本现在都是使用spring框架,而Spring Security完美的基于Spring,有啥理由不用呢?
2、使用Spring Security的公司和人比较多;
3、如何使用Spring Security【重点】?
SpringSecurity 采用的是责任链的设计模式,是由一堆的过滤器链组合而成的,但是我们不需要去仔细了解每一个过滤器的含义和用法,只需要认真思考搞定这几个几个问题即可:如何登录、如何校验账户、认证失败处理、鉴权失败处理
接来下我们开始正文:
3.1、项目环境搭建
项目环境:
Springboot、Mybatis-plus、Redis、Mysql
数据库我简单创建了一下几张表:
- sys_user
- sys_role
- sys_menu
4. sys_user_role
5.sys_role_menu
项目中pom.xml导入相关依赖:
<dependencies>
<!-- security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.6.13</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
<!--<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>-->
</dependency>
<!-- io常用工具类 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
<!-- 阿里JSON解析器 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.23</version>
</dependency>
<!-- Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
不管你用哪种权限框架,第一个要解决的问题就是登录。就是在我们的登录接口中,将账户密码委托给权限框架接管,让权限框架帮我们做校验和权限认证。
3.2、登录认证
3.2.1、准备登录接口
*************************Controller类*************************
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/login")
public Object login(@RequestBody LoginBody loginBody) {
// 生成令牌
return sysUserService.login(loginBody.getUsername(), loginBody.getPassword());
}
*************************业务实现类*************************
@Override
public String login(String username, String password) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password);
AuthenticationContextHolder.setContext(authToken);
// 使用authenticationManager调用loadUserByUsername获取数据库中的用户信息,
Authentication authentication = authenticationManager.authenticate(authToken);
if (authentication == null) {
throw new RuntimeException("登录失败");
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
return tokenUtils.createToken(loginUser);
}
UsernamePasswordAuthenticationToken 就是让框架帮我们托管的 登录凭证。
// 使用authenticationManager调用loadUserByUsername获取数据库中的用户信息,
Authentication authentication = authenticationManager.authenticate(authToken);
这就是spring security帮我们执行 认证 和 授权 的方法,最终返回一个认证结果。
3.2.2、User类
说明一点,spring security中的 用户概念,有自己的一套规则,不能直接用我们程序中自定义的 User类。
因此如果我们要用spring security,就得实现他的用户接口:UserDetails
我们自定义的 Users类
@Data
public class SysUser implements Serializable {
private static final long serialVersionUID = -84734910169707520L;
/**
* 用户ID
*/
@TableId
private Long userId;
/**
* 用户账号
*/
private String userName;
/**
* 用户邮箱
*/
private String email;
/**
* 手机号码
*/
private String phonenumber;
/**
* 用户性别(0男 1女 2未知)
*/
private String sex;
/**
* 密码
*/
private String password;
/**
* 帐号状态(0正常 1停用)
*/
private String status;
/**
* 删除标志(0代表存在 2代表删除)
*/
private String delFlag;
/**
* 创建者
*/
private String createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 备注
*/
private String remark;
实现 spring security 的用户接口的Users类
@Data
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户信息
*/
private SysUser user;
/**
* 用户ID
*/
private Long userId;
/**
* 用户唯一标识
*/
private String token;
/**
* 权限列表
*/
private Set<String> permissions;
public LoginUser(Long userId, SysUser user, Set<String> permissions) {
this.userId = userId;
this.user = user;
this.permissions = permissions;
}
@JSONField(serialize = false)
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
/**
* 账户是否未过期,过期无法验证
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
}
3.2.3、自定义的service接口(认证逻辑)
我们已经有自己的 UserService 了,但是spring security 这个地方也有自己的规范,我们自己写的user Service框架不认。
所以,需要重新写个Service类并且实现SpringSecurity的UserDetailService接口,重写loadUserByUsername方法。
作用:
调用登录的login接口,会经过authenticationManager.authenticate(authenticationToken)方法。此方法会调用loadUserByUsername方法,一般就是到数据库的用户表去查询用户(这里并没有验证密码是否正确), 然后匹配到用户的话就会来查询权限,返回一个UserDetails 对象;否则就抛出异常(或者是提示信息)。
/**
* 用户验证处理
*
* @author ruoyi
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private SysUserService userService;
@Override
public UserDetails loadUserByUsername(String username) {
//查询用户是否存在
QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_name",username);
queryWrapper.eq("del_flag",0);
SysUser user = userService.getOne(queryWrapper);
//用户不存在抛出相应提示
if (Objects.isNull(user)) {
log.info("登录用户:{} 不存在.", username);
throw new RuntimeException("登录用户:" + username + " 不存在");
} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登录用户:{} 已被删除.", username);
throw new RuntimeException("对不起,您的账号:" + username + " 已被删除");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登录用户:{} 已被停用.", username);
throw new RuntimeException("对不起,您的账号:" + username + " 已停用");
}
//验证密码是否正确
userService.validate(user);
//把对应的用户信息和权限信息放入到UserDetails中
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user) {
return new LoginUser(user.getUserId(), user, userService.getMenuPermission(user));
}
3.2.4、spring security的 配置类
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理逻辑
*/
@Autowired
private SpringSecurityFailHandle springSecurityFailHandle;
/**
* 鉴权失败处理逻辑
*/
@Autowired
private SpringAccessDeniedHandler springAccessDeniedHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
// 身份验证管理器, 直接继承即可.
return super.authenticationManagerBean();
}
/**
* 认证
* @param auth the {@link AuthenticationManagerBuilder} to use
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
//注意:这里没用加密方式,采用的是直接对比,企业中会采用加密解密的方式
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.equals(encodedPassword);
}
});
}
到此,认证逻辑完成
3.2.5、登录认证总结
1. 定义登录接口,处理用户登录,如果认证通过,生产一个jwt(token),连同完整的用户信息作为value存入redis;
2. 构建一个自定义的service接口,用于处理自定义认证逻辑,方法内部做用户信息的查询,判断用户名和密码是否正确,最后将用户相关信息存储到UserDetails ;
3.3、访问鉴权
这部分相对比较简单,我们主要要需要完成校验的逻辑和校验失败的处理逻辑。
3.3.1、认证失败的处理类
@Component
public class SpringSecurityFailHandle implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(400);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print("账号或密码错误,请联系管理员");
}
}
3.3.2、鉴权的失败处理类
@Component
public class SpringAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(403);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print("权限不足");
}
}
3.3.3、spring security的 配置类(配置鉴权)
我们要在配置类中做的几件事:
1、接口访问白名单(登录接口不需要鉴权认证)
2、指定认证失败的 处理类
3、指定自定义认证的逻辑的类
4、指定鉴权的失败处理类
/**
* 授权
* @param httpSecurity the {@link HttpSecurity} to modify
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable()
// 认证失败处理类
// 基于token,所以不需要session
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 过滤请求: 设置白名单
.and().authorizeRequests()
.antMatchers("/login").permitAll()
// 静态资源,可匿名访问
// .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
//认证失败的措施
httpSecurity.exceptionHandling().authenticationEntryPoint(springSecurityFailHandle);
//鉴权失败的措施
httpSecurity.exceptionHandling().accessDeniedHandler(springAccessDeniedHandler);
//添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
3.3.4、spring security的完整配置类(配置认证、鉴权)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认知失败处理逻辑
*/
@Autowired
private SpringSecurityFailHandle springSecurityFailHandle;
/**
* 认知失败处理逻辑
*/
@Autowired
private SpringAccessDeniedHandler springAccessDeniedHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
// 身份验证管理器, 直接继承即可.
return super.authenticationManagerBean();
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 认证
* @param auth the {@link AuthenticationManagerBuilder} to use
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.equals(encodedPassword);
}
});
}
/**
* 授权
* @param httpSecurity the {@link HttpSecurity} to modify
* @throws Exception
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable()
// 认证失败处理类
// 基于token,所以不需要session
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 过滤请求: 设置白名单
.and().authorizeRequests()
.antMatchers("/login").permitAll()
// 静态资源,可匿名访问
// .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
//认证失败的措施
httpSecurity.exceptionHandling().authenticationEntryPoint(springSecurityFailHandle);
//鉴权失败的措施
httpSecurity.exceptionHandling().accessDeniedHandler(springAccessDeniedHandler);
//添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
到此,鉴权的基本逻辑完成。
3.3.5、接口权限鉴权使用
在spring security的完整配置类中,我们使用到了这样一个注解:
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
其目的,就是为了我们方便进行接口权限的验证;允许使用@PreAuthorize注解
我们在认证阶段,也就是我们自定义的service接口中,已经存储了权限信息到UserDetails中,我们来看看,redis中缓存的用户信息,所包含的用户权限有哪些:
所以加下来我们自定义权限校验方法,如下:
/**
* 自定义权限校验
*/
@Component("ss")
public class PermissionService {
/**
* 校验是否存在某权限
* @return
*/
public boolean hasPermi(String permission){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser)authentication.getPrincipal();
Set<String> permissions = loginUser.getPermissions();
if (permissions.contains(permission)){
return true;
}
return false;
}
}
如何使用呢?
我们重新定义一个接口
/**
* 用户信息表(SysUser)表控制层
*
* @author makejava
* @since 2025-01-02 17:02:48
*/
@RestController
@RequestMapping("sysUser")
public class SysUserController {
/**
* 服务对象
*/
@Resource
private SysUserService sysUserService;
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("{id}")
@PreAuthorize("@ss.hasPermi('system:user:id')")
public ResponseEntity<SysUser> queryById(@PathVariable("id") Long id) {
return ResponseEntity.ok(this.sysUserService.getById(id));
}
}
我们测试下:
首先,我们先修改下redis中,该用户的权限(设置为不包含该接口的权限)如下:
接下来,我们进行接口访问:
最后,我们再恢复redis中的权限为包含该接口权限:
接着访问接口:
至此,我们完成了,用户登录认证和授权的全部功能!!!
完整版代码下载地址: https://gitee.com/zw_fky/spring_security_test.git
本篇完结!!!