Spring Security超详细教程,白话文教学,包会的!!!


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

数据库我简单创建了一下几张表:
在这里插入图片描述

  1. sys_user
    在这里插入图片描述
  2. sys_role在这里插入图片描述
  3. 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

本篇完结!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值