Spring Security 实现单体认证授权

0.简介

Spring Security是Spring家族的一个安全框架**。相比另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用
Spring Security来做安全框架。小项目使用Shiro的比较多,因为相比Spring Security,Shiro上手更加简单。
一般Web应用都需要进行
认证授权**。
认证:验证当前访问系统的是不是本系统的用户,并且确认具体是哪个用户。
授权:经过认证之后判断当前用户是否有当前操作的权限。
而认证与授权也是SpringSecurity作为安全框架的核心功能。

1.快速入门

1.1 准备工作

先搭建一个简单的SpringBoot工程。

  1. 设置父工程 添加依赖
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.5</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

2.创建启动类

@SpringBootApplication
public class FirstSpringSecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(FirstSpringSecurityApplication.class, args);
    }
}

3.创建controller

@RestController
public class SampleController {
    @GetMapping(value = "/hello")
    public String hello(){
        return "hello";
    }
}

1.2 引入SpringSecurity

在SpringBoot项目中使用SpringSecurity只需要引入依赖即可。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入依赖后尝试访问之前的接口就会自动跳到一个SpringSecurity的默认登录页面,默认用户名是user,密码会在控制台输出。

必须登录之后才能对接口进行访问,相当于已经把接口保护起来了。

2.认证

2.1登录校验流程

登录校验流程

2.2 原理探索

实现登录流程就必须先知道入门案例中的SpringSecurity的流程。

2.2.1 Spring Security完整流程

Spring Security的原理其实就是一个过滤器链,内部包含了各种功能的过滤器。分析一下入门案例中的过滤器。
在这里插入图片描述
图中只是展示了核心过滤器,其他非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名密码后的登录请求。
ExceptionTranslationFilter:负责处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException.
FilterSecurityInterceptor:负责权限校验的过滤器。
通过Debug查看当前系统中SpringSecutiry过滤器链中所有过滤器,以及顺序。
在这里插入图片描述

2.2.2 认证流程详解

在这里插入图片描述
概念:
Authentication接口:它的实现类,表示当前访问系统的用户,封装了用户的相关信息。
AuthenticationManager接口:定义了Authentication的方法。
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要分装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

2.3 解决问题

2.3.1 思路解决

!

!

  1. 登录

    1. 自定义登录接口

    调用ProviderManager的方法进行认证,如果认证通过生成jwt。
    把用户信息存入Redis。
    b.自定义UserDetailsService
    在这个实现类中去查数据库。
    2.校验
    定义jwt认证过滤器
    获取token
    解析token获取userId
    从redis中获取用户信息
    存入SecurityContextHolder

2.3.2 准备工作

  1. 添加依赖
<!--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>${fastjson.version}</version>
</dependency>
<!--jwt依赖-->
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>${java-jwt.version}</version>
</dependency>
  1. 添加redis序列化配置
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJsonRedisSerializer<Object> serializer = new FastJsonRedisSerializer<>(Object.class);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }

}
  1. 定义响应类
@Data
public class ResponseResult<T> {

    @ApiModelProperty(value = "响应状态码")
    private Integer code;

    @ApiModelProperty(value = "响应信息")
    private String msg;

    @ApiModelProperty(value = "响应数据")
    private T data;

    public ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}
  1. JWT工具类与WebUtils
public class JWTUtils {

    private static final String SECRET_KEY = "lakers123456";

    private static final String ISSUER = "LAKERS";

    public static String createToken(Integer userId) {
        return JWT.create().withJWTId(userId.toString())
            .withIssuer(ISSUER)
            .withIssuedAt(new Date())
            .withExpiresAt(getNextMonth())
            .withClaim("userId", userId)
            .sign(Algorithm.HMAC256(SECRET_KEY));
    }

    public static DecodedJWT verifyJWT(String token) {
        Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
        JWTVerifier verifier = JWT.require(algorithm)
            .withIssuer(ISSUER)
            .build();
        return verifier.verify(token);
    }
}
public class WebUtils {

    public static void renderString(HttpServletResponse response, String string) {
        try {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().println(string);
        } catch (IOException e){
            throw new RuntimeException(e);
        }
    }
}
  1. 实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "sys_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId
    private Long id;

    private String userName;

    private String nickname;

    private String password;

    private String status;

    private String email;

    private String phoneNumber;

    private String sex;

    private String avatar;

    private String userType;

    private Long createBy;

    private LocalDateTime createTime;

    private Long updateBy;

    private Integer delFlag;
}

2.3.3 实现

2.3.3.1 数据库校验用户
准备工作

自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己就可以实现从数据库中查询用户名和密码。

  1. 建表(User)
CREATE TABLE `sys_user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',
  `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '昵称',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `phone_number` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `user_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `create_by` bigint DEFAULT NULL,
  `update_by` bigint DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `del_flag` int DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
  1. 引入Mybatis-Plus和mysql连接驱动的依赖
<!--mybatis-plus-->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>${mybatis-plus-boot-starter.version}</version>
</dependency>
<!--mysql连接-->
<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <scope>runtime</scope>
</dependency>
  1. 配置数据库信息
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/authz?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: rootroot
  1. 定义Mapper接口
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
  1. 修改User实体类

类名加上@TableName(value = “sys_user”),id字段添加@TableId

  1. 配置Mapper扫描

@MapperScan(value = “org.lakers.mapper*”)

  1. 添加junit依赖
<!--单元测试-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>
  1. 测试MybatisPlus是否可以正常使用
@SpringBootTest
public class TokenTest {

    @Resource
    public UserMapper userMapper;

    @Test
    public void test(){
        List<User> users = userMapper.selectList(null);
        users.forEach(System.out::println);
    }
}
核心代码实现

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

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserDetailsService, UserService {

    @Resource
    private MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        Optional<User> userOptional = lambdaQuery().eq(User::getUserName, username).oneOpt();
        if (!userOptional.isPresent()) {
            throw new UsernameNotFoundException("用户名或密码错误!");
        }

        // TODO:查询对应的权限信息
        return new LoginUser(user);
    }
}

因为定义的方法返回的类型为UserDetails类型,所以要定义一个类LoginUser,把用户的信息封装在其中。

@Data
@NoArgsConstructor
public class LoginUser implements Serializable, UserDetails {

    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @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;
    }
}

注意:如果要测试,需要往用户表中写入数据,并且想用明文登陆的话,需在密码前加{noop}。

在这里插入图片描述
这样登录的时候就可以用lakers,123456来登录了。

2.3.3.2 密码加密存储

实际项目不会在数据库中存储明文密码。
默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password。它会根据id去判断密码的加密方式,一般不采用这样方式。所以要替换PasswordEncoder。
一般使用SpringSecurity提供的BCryptPasswordEncoder。
使用BCryptPasswordEncoder只需要把它的对象注入Spring容器中,SpringSecurity就会使用改Password Encoder来进行密码校验。
可以定义一个SpringSecurity的配置类。

@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
2.3.3.3 登录接口

自定义登录接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口时,不用登录也可以访问。
在接口中需要通过AuthenticationManager的authenticate方法来进行用户验证,所以需要在SecurityConfig配置中把AuthenticationManager注入容器。
认证成功返回生成token,在响应结果中返回。并且为了让用户下回请求时可以通过token识别出具体的那个用户,需要将用户信息存入redis。

@RestController
public class LoginController {

    @Resource
    private LoginService loginService;

    @ApiOperation(value = "登录")
    @PostMapping(value = "/user/login")
    public ResponseResult<Map<String, String>> login(@RequestBody @Validated User user){
        return loginService.login(user);
    }
}

添加配置

@EnableWebSecurity
public class SecurityConfig {

    @Resource
    private AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public AuthenticationManager authenticationManager () throws Exception{
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http)throws Exception{
        return http.csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 放行登录接口 anonymous允许匿名用户访问,不允许已登入用户访问
                .antMatchers("/user/login").anonymous()
                .anyRequest().authenticated()
                .and()
                // 允许跨域
                .cors()
                .and()
                .build();
    }

}

实现登录代码
@Service
public class LoginServiceImpl implements LoginService {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public ResponseResult<Map<String, String>> login(User user) {
        // AuthenticationManager.authenticate()进行验证
        Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword()));
        if (Objects.isNull(authenticate)){
            throw new RuntimeException("登录失败!");
        }

        // 登录成功生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String token = JWTUtils.createToken(loginUser.getUser().getId().intValue());
        HashMap<String, String> resultMap = new HashMap<>(1);
        resultMap.put("token", token);

        redisTemplate.opsForValue().set("Token:" + loginUser.getUser().getId(), JSON.toJSONString(loginUser));
        return new ResponseResult<>(200, "登录成功", resultMap);
    }
}
2.3.3.4 认证过滤器

编写认证过滤器

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)){
            // 放行
            filterChain.doFilter(request, response);
            return;
        }

        // 解析token
        DecodedJWT jwt = JWTUtils.verifyJWT(token);
        Integer userId = jwt.getClaim("userId").asInt();
        String redisKey = "Token:" + userId;
        String object = redisTemplate.opsForValue().get(redisKey);
        if (!StringUtils.hasText(object)){
            throw new RuntimeException("用户未登录");
        }
        LoginUser user = JSON.parseObject(object, LoginUser.class);

        // 存入SecurityContextHolder
        // TODO:获取权限信息封装到Authentication
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user,
                                                                                                                          null, null);
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

        filterChain.doFilter(request, response);
    }
}
将过滤器添加到Security过滤器链中。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)throws Exception{
    return http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            // 放行登录接口 anonymous允许匿名用户访问,不允许已登入用户访问
            .antMatchers("/user/login").anonymous()
            .anyRequest().authenticated()
            .and()
            // 将token校验添加到过滤器链中
            .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
            .and()
            // 允许跨域
            .cors()
            .and()
            .build();
}
2.3.3.4 退出登录

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

@Override
public ResponseResult<Void> logout() {
    // 获取SecurityContextHolder获取登录用户的id
    UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    LoginUser user = (LoginUser) authentication.getPrincipal();

    // 删除redis
    redisTemplate.delete("Token:" + user.getUser().getId());
    return new ResponseResult<>(200, "注销成功!");
}

3.授权

3.0 权限系统的作用

例如一个学校的图书管理系统,如果是普通学生登录就只能看到借书还书相关的功能,不能让塔看到并且去使用添加删除图书等功能。但是如果是管理员账户登录了,就可以看到并且能过使用添加删除图书功能。
总结就是不同的用户可以使用不同的功能。这就是权限系统要去实现的功能。
不能只依赖前端去判断用户的权限来显示那些按钮。因为这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发请求来实现相关功能的操作。
所以还需要在后台进行用户权限的判断,判断当前用户是否具有相应的权限,必须基于所拥有的权限才能进行相应的操作。

3.1 授权基本流程

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor会从SecurityContextHolder获取其中的Authentication,然后获取其中权限信息。判断用户是否拥有访问当前资所需的权限。
所以在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需的权限即可。

3.2 授权实现

3.2.1 限制访问资源所需的权限

SpringSecurity提供了基于注解的权限控制方案,这也是项目中主要采用的方式。可以通过注解去指定去访问对应资源的权限。
但是先需要开启相关的配置。
@EnableGlobalMethodSecurity(prePostEnabled = true)
然后就可以使用对应的注解。@PreAuthorize

 @RestController
public class HelloController {

    @Resource
    private UserService userService;

    @ApiOperation(value = "获取登录人信息")
    @GetMapping(value = "/helloLoginUser")
    @PreAuthorize("hasAuthority('system:dept:test')")
//    @PreAuthorize("hasAnyAuthority('','')")
//    @PreAuthorize("hasRole('')")
//    @PreAuthorize("hasAnyRole('','')")
    // 调用自己的鉴权方法
//    @PreAuthorize("@LakersExpression.authentication('system:dept:test11')")
    public ResponseResult<UserVo> hello(){
        UserVo vo = userService.getUserVo();
        return ResponseResult.success(vo);
    }
}

3.2.2 封装权限信息

前面在写UserDetailServiceImpl的时候,在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。
先直接把权限信息写死分装到UserDetails中进行测试。
之前定义了UserDetails的实现类LoginUser,想让其封装权限信息就要对其进行修改。

@Data
@NoArgsConstructor
public class LoginUser implements Serializable, UserDetails {

    private User user;

    private List<String> permissions;

    /**
     * redis中不对authorities存储
     */
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 把permissions中String类型的权限信息封装成SimpleGrantedAuthority对象
        if (CollectionUtil.isEmpty(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;
    }
}

登录查询用户也进行修改。
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 查询用户信息
    Optional<User> userOptional = lambdaQuery().eq(User::getUserName, username).oneOpt();
    if (!userOptional.isPresent()) {
        throw new UsernameNotFoundException("用户名或密码错误!");
    }

    // TODO:查询对应的权限信息
    List<String> list = Arrays.asList("test");
    return new LoginUser(user, list);

}
jwt过滤器存入权限信息。
@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
    // 获取token
    String token = request.getHeader("token");
    if (!StringUtils.hasText(token)){
        // 放行
        filterChain.doFilter(request, response);
        return;
    }

    // 解析token
    DecodedJWT jwt = JWTUtils.verifyJWT(token);
    Integer userId = jwt.getClaim("userId").asInt();
    String redisKey = "Token:" + userId;
    String object = redisTemplate.opsForValue().get(redisKey);
    if (!StringUtils.hasText(object)){
        throw new RuntimeException("用户未登录");
    }
    LoginUser user = JSON.parseObject(object, LoginUser.class);

    // 存入SecurityContextHolder
    // TODO:获取权限信息封装到Authentication
    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user,
            null, user.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

    filterChain.doFilter(request, response);
}

3.2.3 从数据库查询权限信息

3.2.3.1 RBAC权限模型

RBAC权限模型(Role-Based-Access-Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

3.2.3.2 准备工作建表

建系统菜单表,系统角色表,角色关联菜单表,用户关联角色表。

CREATE TABLE `sys_menu` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `menu_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `perms` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `role_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE `sys_role_menu` (
  `role_id` bigint NOT NULL,
  `menu_id` bigint NOT NULL,
  PRIMARY KEY (`role_id`,`menu_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
CREATE TABLE `sys_user_role` (
  `user_id` bigint NOT NULL,
  `role_id` bigint NOT NULL,
  PRIMARY KEY (`user_id`,`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
3.2.3.3 代码实现

将原来的权限信息改为从数据库中查出来。

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 查询用户信息
        Optional<User> userOptional = lambdaQuery().eq(User::getUserName, username).oneOpt();
        if (!userOptional.isPresent()) {
            throw new UsernameNotFoundException("用户名或密码错误!");
        }

        // TODO:查询对应的权限信息
        User user = userOptional.get();
        List<String> list = menuMapper.selectPermsByUserId(user.getId());
        return new LoginUser(user, list);

    }

4.自定义失败处理

在SpringSecurity中,如果在认证或者授权过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中回去判断是认证失败还是授权失败。
如果是认证过程中出现的异常会被封装为AuthenticationException然后调用AuthenticationEntryPoint对象的方法进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDenideException然后调用AccessDeniedHandler对象的方法进行处理。
所以我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

  1. 自定义实现类

AuthenticationEntryPoint的实现类AuthenticationEntryPointImpl。

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult<String> responseResult = new ResponseResult<>(HttpStatus.HTTP_UNAUTHORIZED, "用户认证失败请重新登录!");
        String jsonString = JSON.toJSONString(responseResult);
        // 处理异常
        WebUtils.renderString(response, jsonString);
    }
}

AccessDeniedHandler的实现类AccessDeniedHandlerImpl。

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseResult<String> responseResult = new ResponseResult<>(HttpStatus.HTTP_FORBIDDEN, "你的权限不足!");
        String jsonString = JSON.toJSONString(responseResult);
        // 处理异常
        WebUtils.renderString(response, jsonString);
    }
}
  1. 配置给SpringSecurity
@Resource
private AuthenticationEntryPoint authenticationEntryPoint;

@Resource
private AccessDeniedHandler accessDeniedHandler;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http)throws Exception{
    return http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            // 放行登录接口 anonymous允许匿名用户访问,不允许已登入用户访问
            .antMatchers("/user/login").anonymous()
            .anyRequest().authenticated()
            .and()
            // 将token校验添加到过滤器链中
            .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
            // 配置异常处理器
            .exceptionHandling()
            // 配置认证失败处理器
            .authenticationEntryPoint(authenticationEntryPoint)
            // 配置授权失败处理器
            .accessDeniedHandler(accessDeniedHandler)
            .and()
            // 允许跨域
            .cors()
            .and()
            .build();
}

5. 跨域

浏览器出于安全考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵循同源策略,否则就是跨域的Http请求,默认情况下是被禁止的。同源策略要求源相同才能进行通信,即协议、域名、端口号都完全一致。
前后端分离项目,前端项目和后端项目一般都是不同源,所以肯定会存在跨域请求的问题。

  1. 先对SpringBoot配置,允许跨域请求
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry){
        // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }
}
  1. 开启SpringSecurity的跨域请求
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)throws Exception{
    return http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            // 放行登录接口 anonymous允许匿名用户访问,不允许已登入用户访问
            .antMatchers("/user/login").anonymous()
            .anyRequest().authenticated()
            .and()
            // 将token校验添加到过滤器链中
            .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
            // 配置异常处理器
            .exceptionHandling()
            // 配置认证失败处理器
            .authenticationEntryPoint(authenticationEntryPoint)
            // 配置授权失败处理器
            .accessDeniedHandler(accessDeniedHandler)
            .and()
            // 允许跨域
            .cors()
            .and()
            .build();
}

6.其他知识点

1 其他的权限校验方案

@PreAuthorize("hasAuthority('system:dept:test')")
@PreAuthorize("hasAnyAuthority('','')")
@PreAuthorize("hasRole('')")
@PreAuthorize("hasAnyRole('','')")

2 自定义权限校验方法

@Component("LakersExpression")
public class LakersExpressionRoot {

    public boolean authentication(String authority) {
        // 获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();

        // 鉴权
        return permissions.contains(authority);
    }
}

使用如下:
@PreAuthorize(“@LakersExpression.authentication(‘system:dept:test11’)”)

3 基于配置的权限控制

也可以在配置类中使用配置的方式对资源进行权限配置。

@Bean
public SecurityFilterChain filterChain(HttpSecurity http)throws Exception{
    return http.csrf().disable()
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        // 放行swagger
        .antMatchers("/swagger-ui.html","/swagger-resources/**","/webjars/**","/v2/**","/api/**", "/doc.html").permitAll()
        // 放行登录接口 anonymous允许匿名用户访问,不允许已登入用户访问
        .antMatchers("/user/login").anonymous()
        .antMatchers("/user/hello").hasAnyAuthority("test")
        .anyRequest().authenticated()
        .and()
        // 将token校验添加到过滤器链中
        .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
        // 配置异常处理器
        .exceptionHandling()
        // 配置认证失败处理器
        .authenticationEntryPoint(authenticationEntryPoint)
        // 配置授权失败处理器
        .accessDeniedHandler(accessDeniedHandler)
        .and()
        // 允许跨域
        .cors()
        .and()
        .build();
}

4 CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是Web常见的攻击之一。
Spring Security去防止CSRF攻击的方式就是通过csrf_token,后端会生成一个csrf_token,前端发起请求的时候需要携带过来,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
可以发现CSRF攻击依靠的是cookie中携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储在cookie中的,并且需要前端去把token设置到请求头中才可以,所以CSRF就不用担心了。

5 认证成功处理器

6 认证失败处理器

7 注销成功处理器

这三个处理器都是基于UsernamePasswordAuthenticationFilter模式处理的,一般项目中不会直接使用UsernamePasswordAuthenticationFilter,会自定义处理过滤链所以此处不过分描述。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值