[阶段3 企业开发基础] 7. SpringSecurity

提示:量大够足,参考尚硅谷Scurity教学视频~

最好的学习方式是官方文档

Spring Secruity官方文档

Spring Security 中文文档

Spring Security源码

Gitee仓库【近期比较忙,有空更新】

1.SpringSecurity框架简介

Spring Security基于Spring,提供了一套Web应用安全性的完整解决方案。

关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 Spring Security 重要核心功能。

  • 用户认证 :验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录
  • 用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。

1.1 快速入门

导入依赖和父工程

 	<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
    </parent>
    <dependencies>
        <!-- spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

1.2 整合SpringSecurity

 <!-- spring-boot-starter-security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

在这里插入图片描述

在这里插入图片描述

1.3 SpringSecurity 特点

  • 和 Spring 无缝整合。

  • 全面的权限控制。

  • 专门为Web 开发而设计。

    • 旧版本不能脱离Web 环境使用。
    • 新版本对整个框架进行了分层抽取,分成了核心模块和Web 模块。单独引入核心模块就可以脱离Web 环境。
  • 重量级。

1.4 shiro

shiro

Apache 旗下的轻量级权限控制框架。

特点

  • 轻量级。 Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
  • 通用性
    • 优点:不局限于Web环境,可以脱离Web 环境使用
    • 缺点:在Web 环境下一些特定的需求需要手动编写代码定制

常见安全管理技术栈组合

  • SSM+shiro
  • Spring Boot/Spring Cloud + Spring Security

以上只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的。

1.5 权限管理的相关概念

主体 principal

使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系统谁就是主体。

认证 authentication

权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证明自己是谁

授权 authorization

将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功能的能力。

2. 认证

2.1 登录校验流程

在这里插入图片描述

2.2 原理

2.2.1 Spring Security完整流程

Spring Security的原理就是一个过滤器链,内部包含各种功能的过滤器

Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:

在这里插入图片描述

UsernamePasswordAuthenticationFilter:负责处理登录时填写用户名和密码后的登录请求,上述快速入门的案例由他负责。[该过滤器会拦截前端提交的 POST 方式的登录表单请求,并进行身份认证]

ExceptionTranslationFilter:负责处理过滤器链中抛出的任何AccessDeniedExceptionAuthenticationException[该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)]

FilterSecurityInterceptor:负责权限校验的过滤器[该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由ExceptionTranslationFilter 过滤器进行捕获和处理。]

手把手教你Debug

1.设置断点

在这里插入图片描述

2.启动调试,查看待调试的对象

在这里插入图片描述

这里以context.getBean(DefaultSecurityFilterChain.class)为例。可以发现DefaultSecurityFilterChaing过滤器中含有15个过滤器组件以及它们的顺序。

在这里插入图片描述

2.2.2 认证流程详解

在这里插入图片描述
Authentication接口:它们的实现类表示当前访问系统的用户,封装了用户相关的信息

AuthenticationManager接口:定义了认证Authentication的方法

UserDetailService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。这些信息封装到Authentication对象中。

在这里插入图片描述

基本认证流程[理解即可]

  1. 提交用户名密码

  2. 封装Authentication对象,此时只有用户名和密码没有权限

  3. 调用authentication方法进行认证

  4. 调用DaoAuthenticationProviderauthenticate方法进行认证

  5. 调用loadUserByUsename方法查询用户

  6. 根据用户名察隅用户信息、权限信息InMemoryUserDetailsManager是在内存中查询

  7. 把对应的用户信息,包括权限信息封装成UserDetails对象

  8. 返回UserDetails对象

  9. 通过PasswordEncoder对比UserDetails中的密码和Authentication的密码是否正确

  10. 如果正确就把UserDetails中的权限信息设置到Authentication对象中

  11. 返回Authentication对象

  12. 如果上一步返回了Authentication对象,就是用SecurityContextHolder.getContext().setAuthentication方法存储该对象。其他过滤器会通过SecurityContextHolder来获取当前用户信息

2.2.3 基本原理

SpringSecurity 本质是一个过滤器链

在这里插入图片描述

SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的 15 个过滤器进行说明:

  1. WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
  2. SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信息就是这个过滤器处理的。
  3. HeaderWriterFilter:用于将头信息加入响应中。
  4. CsrfFilter:用于处理跨站请求伪造。
  5. LogoutFilter:用于处理退出登录。
  6. UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的 usernameParameter passwordParameter 两个参数的值进行修改。
  7. DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
  8. BasicAuthenticationFilter:检测和处理 http basic 认证。
  9. RequestCacheAwareFilter:用来处理请求的缓存
  10. SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。
  11. AnonymousAuthenticationFilte:检测 SecurityContextHolder 中是否存在Authentication对象,如果不存在为其提供一个匿名Authentication`。
  12. SessionManagementFilter:管理 session 的过滤器
  13. ExceptionTranslationFilter:处理 AccessDeniedExceptionAuthenticationException 异常。
  14. FilterSecurityInterceptor:可以看做过滤器链的出口。
  15. RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

代码底层流程:重点看三个过滤器:

FilterSecurityInterceptor

  • FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部

在这里插入图片描述

  • super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。

  • fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务

ExceptionTranslationFilter

  • ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常

在这里插入图片描述

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码。

在这里插入图片描述

UserDetailsService 接口讲解

当没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑

如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:

在这里插入图片描述

返回值UserDetails讲解

这个类是系统默认的用户“主体

// 表示获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();

// 表示获取密码
String getPassword();

// 表示获取用户名
String getUsername();

// 表示判断账户是否过期
boolean isAccountNonExpired();

// 表示判断账户是否被锁定
boolean isAccountNonLocked();

// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();

// 表示当前用户是否可用
boolean isEnabled();

UserDetails 实现类

在这里插入图片描述

以后只需要使用 User 这个实体类即可!

在这里插入图片描述

方法参数 username讲解

表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫username,否则无法接收

PasswordEncoder 接口讲解
// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);

// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);

// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword) {
return false;
}

接口实现类

在这里插入图片描述

BCryptPasswordEncoder是 Spring Security 官方推荐的密码解析器,BCryptPasswordEncoder 是对bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10

2.3 解决问题

2.3.1 思路分析

在这里插入图片描述

在这里插入图片描述

登录

  • 自定义登录接口 调用ProviderMangager的方法进行认证,如果认证通过生成jwt,把用户信息存入Redis
  • 自定义UserDetailsService在该实现类中查询数据库

校验

  • 定义jwt认证过滤器
    • 获取token
    • 解析token
    • redis获取用户信息
    • 存入SecurityContextHolder

2.3.2 准备工作

导入依赖

  		<!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--fastjson依赖-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.33</version>
        </dependency>
        <!--jwt依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>

2.3.3 实现

数据库校验用户

自定义UserDetailsService,让Spring Security使用自定义的类,从数据库中查询用户和密码。

准备工作

建表语句:

CREATE TABLE `sys_user` (
	`id` BIGINT (20) NOT NULL AUTO_INCREMENT COMMENT '主键',
	`user_name` VARCHAR (64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
	`nick_name` VARCHAR (64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
	`password` VARCHAR (64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
	`status` CHAR (1) DEFAULT '0' COMMENT '账户状态(0正常 1停用)',
	`email` VARCHAR (64) DEFAULT NULL COMMENT '邮箱',
	`phonenumber` VARCHAR (32) DEFAULT 'NULL' COMMENT '手机号',
	`sex` CHAR (1) DEFAULT NULL COMMENT '用户性别(0男 1女 2未知)',
	`avatar` VARCHAR (128) DEFAULT NULL COMMENT '头像',
	`user_type` CHAR (1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员1普通用户)',
	`create_by` BIGINT (20) DEFAULT NULL COMMENT '创建者Id',
	`create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
	`update_by` BIGINT (20) DEFAULT NULL COMMENT '更新者',
	`update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
	`del_flag` INT (11) DEFAULT 0 COMMENT '删除标志(0代表未删除,1代表已删除)',
	PRIMARY KEY (`id`)
) ENGINE = INNODB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8mb4 COMMENT '用户表'

导入数据库依赖

 		<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

配置数据库属性

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/springsecurity
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver

定义mapper接口

public interface UserMapper extends BaseMapper<User> {
}

给User添加注解识别表名

@TableName("sys_user")
public class User implements Serializable {

配置mapper扫描

@SpringBootApplication
@MapperScan("com.cyan.mapper")
public class SpringSecurityApplication {

引入spring-test依赖测试mapper

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

核心代码实现

创建一个实现UserDetailsService接口重写方法,根据用户名从数据库中查询用户信息

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 根据用户名查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName, username);
        User user = userMapper.selectOne(queryWrapper);
        // 如果没有查询到用户就抛出异常
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或者密码错误");
        }
 
        //根据用户查询权限信息,添加到LoginUser中
        
        // 封装成UserDetails返回返回
        return new LoginUser(user);
    }
}

注意

如果测试,如果想让用户的密码以明文方式存储,需要在密码前加入{noop}

在这里插入图片描述

密码加密存储

一般情况密码不会进行明文存储,默认使用的PasswordEncoder要求数据库中的密码格式未{id}password,它会根据id去判断密码的加密方式,我们一般不采取该方式。取而代之的使用BCryptPasswordEncoder

定义一个SpringSecurity的配置类,并继承WebSecurityConfigurerAdapter

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 创建BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

测试BCryptPasswordEncoder

@Test
public void testBCryptPasswordEncoder() {
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    String encode = encoder.encode("1234");
    System.out.println(encode);
}

在这里插入图片描述

@Test
public void testBCryptPasswordEncoder() {
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();

    // $2a$10$82L.BJDFR8oI97x2JX2FcOjJPFAApls9ONed8L2VLXD9pbpaJRud6
    System.out.println(encoder.matches(
            "1234",
            "$2a$10$82L.BJDFR8oI97x2JX2FcOjJPFAApls9ONed8L2VLXD9pbpaJRud6"
    ));
}

在这里插入图片描述

登录接口

自定义登录接口,SpringSecurity放行该接口,使用户访问该接口无需登录也可以访问

在接口中通过AuthenticationManagerauthenticate方法进行用户认证,因此需要在SecurityConfig中注入AuthenticationManager

认证成功话将生成一个jwt,放入响应中返回,并且用户下一次请求时可以通过jwt识别出具体用户,应该把用户信息放入redis,并且将用户id作为key

编写控制器方法LoginController

@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user) {
    // 登录
    return loginServcie.login(user);
}

重写SecurityConfig中AuthenticationManager方法

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

源码分析

/**
	 * Override this method to expose the {@link AuthenticationManager} from
	 * {@link #configure(AuthenticationManagerBuilder)} to be exposed as a Bean. For
	 * example:
	 *
	 * <pre>
	 * &#064;Bean(name name="myAuthenticationManager")
	 * &#064;Override
	 * public AuthenticationManager authenticationManagerBean() throws Exception {
	 *     return super.authenticationManagerBean();
	 * }
	 * </pre>
	 * @return the {@link AuthenticationManager}
	 * @throws Exception
	 */
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return new AuthenticationManagerDelegator(this.authenticationBuilder, this.context);
	}

Alt+Ctrl:

在这里插入图片描述

获取用户信息

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(queryWrapper);
        // 如果没有查询到用户就抛出异常
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或者密码错误");
        }

        // 把数据封装成UserDetails返回
        return new LoginUser(user);
    }
}

SecurityConfig中配置放行操作

  @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭csrf
                .csrf().disable()
                // 不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // .antMatchers("/testCors").hasAuthority("system:dept:list222")
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }

实现login

   @Override
    public ResponseResult login(User user) {
        // AuthenticationManager authenticate进行用户认证
        // 将用户名和密码封装为该对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // 如果认证没通过,给出对应的提示
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("登录失败");
        }
        //如果认证通过,使用userId生成一个jwt 将jwt存入ResponseResult返回
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        // 获取userId
        String userId = loginUser.getUser().getId().toString();

        // 使用userId生成jwt
        String jwt = JwtUtil.createJWT(userId);
        Map<String, String> map = new HashMap<>();
        map.put("token", jwt);
        // 把完整的用户信息存入redis  userId作为key
        redisCache.setCacheObject("login:" + userId, loginUser);
        return new ResponseResult(200, "登录成功", map);
    }
认证过滤器

创建包filter实现JwtAuthenticationTokenFilter,根据上述思路,代码如下:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            // 如果没有token直接放行
            filterChain.doFilter(request, response);
            // 防止走完后面过滤器,响应回来再次进入该过滤器
            return;
        }

        // 解析token
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }

        // 从redis中获取用户信息
        String redisKey = "login:" + userId;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)) {
            throw new RuntimeException("用户未登录");
        }

        // 存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

配置认证过滤器

/*配置认证过滤器*/
// 添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
退出登录

只需要定义一个登录接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可

控制器方法LoginController

@RequestMapping("/user/logout")
public ResponseResult logout() {
    return loginServcie.logout();
}

logout

@Override
public ResponseResult logout() {
    // 获取SecurityContextHolder中的用户id
    UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    Long userId = loginUser.getUser().getId();

    // 删除redis中的值
    redisCache.deleteObject("login:" + userId);
    return new ResponseResult(200, "注销成功");
}

3. 授权

3.1 权限系统的作用

权限系统实现不同用户使用不同的功能

为什么还需要在后台进行用户权限的判断

如果仅仅依赖前端去判断用户的权限来显示相应操作,知道对应功能的接口即可绕过前端判断,直接发送请求实现相关操作

3.2 授权的基本流程

在Spring Security中,使用默认的FilterSecurityInterceptor进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。因此在项目中仅需要把当前登录用户的权限信息存入Authentication然后设置资源所需要的权限即可。

3.3 授权实现

3.3.1 限制访问资源所需权限

Spring Security提供了基于注解的权限控制方案,使用注解指定访问对应的资源所需的权限。

开启相关配置

@EnableGlobalMethodSecurity(prePostEnabled = true)

使用相应的注解@PreAuthorize

@RestController
public class HelloController {
    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')")
    public String hello() {
		return "hello";
    }
}

3.3.2 封装权限信息

在写UserDetailsServiceImpl时提及,在查询出用户后还需要获取对应的权限信息,封装到UserDetails中返回。

先直接把权限信息写死封装到UserDetails进行测试,之前定义了UserDetails的实现类LoginUser,对其进行修改封装权限信息

LoginUser

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    private List<String> permissions;

    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }

    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities != null) {
            return authorities;
        }
        
        authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

3.3.3 从数据库中查询权限信息

RBAC权限模型

RBAC权限模型(Role-Based Access Control): 基于角色的权限控制

准备工作

在这里插入图片描述

建表

在这里插入图片描述

在这里插入图片描述

编写Menu实体类

@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
    private static final long serialVersionUID = -54979041104113736L;
    
    @TableId
    private Long id;
    /**
    * 菜单名
    */
    private String menuName;
    /**
    * 路由地址
    */
    private String path;
    /**
    * 组件路径
    */
    private String component;
    /**
    * 菜单状态(0显示 1隐藏)
    */
    private String visible;
    /**
    * 菜单状态(0正常 1停用)
    */
    private String status;
    /**
    * 权限标识
    */
    private String perms;
    /**
    * 菜单图标
    */
    private String icon;
    
    private Long createBy;
    
    private Date createTime;
    
    private Long updateBy;
    
    private Date updateTime;
    /**
    * 是否删除(0未删除 1已删除)
    */
    private Integer delFlag;
    /**
    * 备注
    */
    private String remark;
}
代码实现

只需要用户id查询其所对应的权限信息即可

public interface MenuMapper extends BaseMapper<Menu> {

    List<String> selectPermsByUserId(Long userid);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.cyan.mapper.MenuMapper">


    <select id="selectPermsByUserId" resultType="java.lang.String">
        SELECT
            DISTINCT m.`perms`
        FROM
            sys_user_role ur
            LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
            LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
            LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
        WHERE
            user_id = #{userid}
            AND r.`status` = 0
            AND m.`status` = 0
    </select>
</mapper>
mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml

[以上具体请看源码,近期笔者将放入到Gitee仓库中供大家参考]

4. 自定义异常处理

需求:在认证失败或者是授权失败的情况下也能和返回相同结构的json,让前端能对响应进行统一的处理。

要实现这个功能需要知道SpringSecurity的异常处理机制
在SpringSecurity中,如果在认证或者授权的过程中出现异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

  • 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
  • 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
  • 所以如果需要自定义异常处理,我们只需要自定义AuthenticationEntryPointAccessDeniedHandler然后配置给SpringSecurity即可。

创建handler包用于处理两种异常

AuthenticationEntryPointImpl

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户认证失败请查询登录");
        String json = JSON.toJSONString(result);
        // 处理异常
        WebUtils.renderString(response, json);
    }
}

AccessDeniedHandlerImpl

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(),"您的权限不足");
        String json = JSON.toJSONString(result);
        // 处理异常
        WebUtils.renderString(response,json);
    }
}

在SecurityConfig中配置异常处理器

// 配置异常处理器
http.exceptionHandling()
        // 配置认证失败处理器
        .authenticationEntryPoint(authenticationEntryPoint)
        .accessDeniedHandler(accessDeniedHandler);

5. 跨域

浏览器出于安全的考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的

同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。所以我们就要处理—下,让前端能进行跨域请求。

跨域请求配置

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }
}

开启Spring Security的跨域访问

//允许跨域
http.cors();

6. 其他权限校验方法

hasAuthority 方法

如果当前的主体具有指定的权限,则返回 true,否则返回 false

hasAnyAuthority 方法

如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true.

hasRole 方法

如果用户具备给定角色就允许访问,否则出现 403。如果当前主体具有指定的角色,则返回 true。

前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。

SpringSecurityi还提供其它方法例如: hasAnyAuthorityhasRolehasAnyRole等。

原理
hasAuthority方法实际是执行到了SecurityExpressionRoothasAuthority,它内部其实是调用authenticationgetAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。

  • hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
  • hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。
  • hasAnyRole有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下孽用用)中对应的权限也要有 ROLE_ 这个前缀才可以。

7. 自定义权限校验方法

自定义类

@Component("ex")
public class ExpressionRoot {

    public boolean hasAuthority(String authority){
        // 获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        // 判断用户权限集合中是否存在authority
        return permissions.contains(authority);
    }
}
@RestController
public class HelloController {
    @RequestMapping("/hello")
    @PreAuthorize("@ex.hasAuthority('system:dept:list')")
    public String hello() {
		return "hello";
    }
}

8. CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。
SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。

CSRFI攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

9. 登录成功/认证失败/注销成功处理器

9.1 登录成功处理器

实际上在UsernamePasswordAuthenticationFiter进行登录认证的时候,如果登录成功是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器

SuccessHandler

@Component
public class SuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {

    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println("认证成功");
    }
}

SecurityConfig

@Override
protected void configure(HttpSecurity http) throws Exception {

    http.formLogin().successHandler(successHandler);
    http.authorizeRequests().anyRequest().authenticated();
}

9.2 认证失败处理器

实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录失败是会调用AuthenticationSuccessHandler的方法进行认证失败后的处理的。AuthenticationSuccessHandler就是登录失败处理器

在这里插入图片描述

FailureHandler

@Component
public class FailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

    }
}

SecurityConfig

protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
                    // 配置认证成功处理器
                    .successHandler(successHandler)
                    // 配置认证失败处理器
                    .failureHandler(failureHandler);
}

9.3 注销成功处理器

MyLogoutSuccessHandler

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        
    }
}

SecurityConfig

protected void configure(HttpSecurity http) throws Exception {
http.logout().
            // 配置注销成功处理器
            logoutSuccessHandler(logoutSuccessHandler);

}

10. Spring Security微服务权限方案

10.1 微服务由来

微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。

10.2 微服务优势

  • 微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决
  • 微服务每个模块都可以使用不同的存储方式(比如有的用 redis,有的用 mysql等),数据库也是单个模块对应自己的数据库。
  • 微服务每个模块都可以使用不同的开发技术,开发模式更灵活

10.3 微服务本质

  • 微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过 程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。
  • 微服务的目的是有效的拆分应用,实现敏捷开发和部署。

10.4 微服务认证与授权实现思路

10.4.1 认证授权过程分析

  • 如果是基于Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。

  • 如果是token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限信息中去

在这里插入图片描述

如果系统的模块众多,每个模块都需要进行授权与认证,所以选择基于 token 的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为key,权限列表为value 的形式存入 redis 缓存中,根据用户名相关信息生成token 返回,浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将token 携带到 header 请求头中,Spring-security 解析 header 头获取 token 信息,解析 token 获取当前用户名,根据用户名就可以从redis 中获取权限列表,这样 Spring-security 就能够判断当前请求是否有权限访问

10.4.2 权限管理数据模型

在这里插入图片描述

10.5 JWT

10.5.1 访问令牌的类型

在这里插入图片描述

10.5.2 JWT的组成

在这里插入图片描述

该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名

JWT 头

JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。

{

    "alg": "HS256",

    "typ": "JWT"

}

有效载荷

有效载荷部分,是JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT指定七个默认字段供选择。

iss:发行人

exp:到期时间

sub:主题

aud:用户

nbf:在此之前不可用

iat:发布时间

jti:JWT ID 用于标识该JWT

签名哈希

签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyan Chau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值