SpringSecurity授权

一、RBAC权限模型

1、模型简介

RBAC(Role Based Access Control)中文全称是基于角色的访问控制。在RBAC模型中,权限与角色相关联,不同的角色有不同的权限,用户通过被分配为不同的角色从而获得不同角色的权限,从而简化用户的权限管理。用户与角色关联后,同能进行自主授权和权限专营,必须通过角色来控制授权信息,实现访问控制。

2、依据模型创建数据库表

依据RBAC模型创建一个简单数据库,完整性约束不太强,只是简单实现功能。

-- ----------------------------

-- Table structure for menu

-- ----------------------------

DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '权限id',
  `menu_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '权限名',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of menu

-- ----------------------------

INSERT INTO `menu` VALUES (1, 'sys:user:insert');
INSERT INTO `menu` VALUES (2, 'sys:user:delete');
INSERT INTO `menu` VALUES (3, 'sys:student:insert');
INSERT INTO `menu` VALUES (4, 'sys:student:delete');
INSERT INTO `menu` VALUES (5, 'sys:studet:update');
INSERT INTO `menu` VALUES (6, 'sys:user:login');

-- ----------------------------

-- Table structure for role

-- ----------------------------

DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色id',
  `role_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色名',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of role

-- ----------------------------

INSERT INTO `role` VALUES (1, 'admin');
INSERT INTO `role` VALUES (2, 'student');
INSERT INTO `role` VALUES (3, 'teacher');

-- ----------------------------

-- Table structure for role_menu

-- ----------------------------

DROP TABLE IF EXISTS `role_menu`;
CREATE TABLE `role_menu`  (
  `role_id` bigint NOT NULL COMMENT '角色id',
  `menu_id` bigint NOT NULL COMMENT '权限id',
  PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of role_menu

-- ----------------------------

INSERT INTO `role_menu` VALUES (1, 1);
INSERT INTO `role_menu` VALUES (1, 2);
INSERT INTO `role_menu` VALUES (1, 3);
INSERT INTO `role_menu` VALUES (1, 4);
INSERT INTO `role_menu` VALUES (1, 5);
INSERT INTO `role_menu` VALUES (1, 6);
INSERT INTO `role_menu` VALUES (2, 1);
INSERT INTO `role_menu` VALUES (2, 3);
INSERT INTO `role_menu` VALUES (2, 6);
INSERT INTO `role_menu` VALUES (3, 1);
INSERT INTO `role_menu` VALUES (3, 3);
INSERT INTO `role_menu` VALUES (3, 4);
INSERT INTO `role_menu` VALUES (3, 6);

-- ----------------------------

-- Table structure for user

-- ----------------------------

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `user_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of user

-- ----------------------------

INSERT INTO `user` VALUES (1, 'cx', '$10$S47LvooOhnDT0sVZ7DlShuqfEqcwhyhU3F9GKt0mbgx1vym.zZXWS');
INSERT INTO `user` VALUES (2, 'cxx', '$10$S47LvooOhnDT0sVZ7DlShuqfEqcwhyhU3F9GKt0mbgx1vym.zZXWS');
INSERT INTO `user` VALUES (3, 'cxxx', '$10$S47LvooOhnDT0sVZ7DlShuqfEqcwhyhU3F9GKt0mbgx1vym.zZXWS');

-- ----------------------------

-- Table structure for user_role

-- ----------------------------

DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `role_id` bigint NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------

-- Records of user_role

-- ----------------------------

INSERT INTO `user_role` VALUES (1, 1);
INSERT INTO `user_role` VALUES (2, 2);
INSERT INTO `user_role` VALUES (3, 2);
INSERT INTO `user_role` VALUES (3, 3);

SET FOREIGN_KEY_CHECKS = 1;

二、授权实现

1、根据id查询授权信息

根据用户信息从数据库查询授权信息。定义权限实体类,mapper接口,mapper映射文件。

实体类:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Menu implements Serializable {
    
    

    private static final long serialVersionUID = -54979041104113736L;

    @TableId
    private Long id;
    /**
     * 菜单名
     */
    private String menuName;
}

mapper接口:

@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
    
    

    List<String> selectMenusByUserId(@Param("id") Long id);

}

mapper映射文件:由于只是简单测试,就用多表查询,不考虑性能了。

<?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.cx.authorities.mapper.MenuMapper">


    <select id="selectMenusByUserId" resultType="java.lang.String">
        select
            DISTINCT
            menu.menu_name
        from
            user_role,role,role_menu,menu
        where
            user_id = ${id} and
            user_role.role_id = role.id and
            role.id = role_menu.role_id and
            role_menu.menu_id = menu.id
    </select>
</mapper>

2、封装授权信息

UserDetails实现类会重写getAuthorities()方法,返回值对象就是该用户拥有的权限集合。首先需要在数据库中将用户对应角色下对应权限查询出来,将它通过UserDetailsService传入登录用户实体类LoginUser里面。由于security需要的是GrantedAuthority类型的权限信息,需要将传入LoginUser的权限信息全部装换成GrantedAuthority类型。SimpleGrantedAuthorityGrantedAuthority的实现类,接受字符串信息,所以直接将字符串权限转换成SimpleGrantedAuthority

/**
 * 实现UserDetails接口
 */
@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;
    }

    //存储SpringSecurity所需要的权限信息的集合
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

    /**
     * 返回权限信息
     * @return
     */
    
    @Override
    public  Collection<? extends GrantedAuthority> getAuthorities() {
    
    
        if(authorities!=null){
    
    
            return authorities;
        }
        authorities = new ArrayList<>();
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        for (String permission : permissions) {
    
    
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
            authorities.add(simpleGrantedAuthority);
        }
        return authorities;
    }

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

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


    /**
     * 判断用户是否没过期
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
    
    
        return true;
    }

     /**
     * 判断用户是否锁定
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
    
    
        return true;
    }

    /**
     * 判断用户是否没有超时
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
    
    
        return true;
    }

    /**
     * 判断用户是否可用
     * @return
     */
    @Override
    public boolean isEnabled() {
    
    
        return true;
    }
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private MenuMapper menuMapper;

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

//        查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName, userName);
        User user = userMapper.selectOne(wrapper);

//        未查询到用户
        if(Objects.isNull(user)){
    
    
            throw new RuntimeException("用户名不存在");
        }

//        查询用户权限
        List<String> permissionKeyList =  menuMapper.selectMenusByUserId(user.getId());

//        查询成功返回用户信息
        return new LoginUser(user,permissionKeyList);
    }
}

3、将权限信息存入SecurityContextHolder

用户信息在UserDetails实现类LoginUser方法中封装成为SimpleGrantedAuthority类型后,security内部需要将用户信息及其权限 信息封装成为authenticationToken对象,传入SecurityContextHolder

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
    
    
//        获取token
        String token = httpServletRequest.getHeader("token");
        if (!StringUtils.hasText(token)) {
    
    
//            放行
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
//        解析token
        String id;
        try {
    
    
            Claims claims = JwtUtil.parseJWT(token);
            id = claims.getSubject();
        } catch (Exception e) {
    
    
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
//        redis获取用户信息
        String redisKey = "login:" + id;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
    
    
            throw new RuntimeException("用户未登录");
        }
//        存入securityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//        放行
        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

4、开启权限配置

权限信息封装好并传入SecurityContextHolder后,需要在配置类上开启权限扫描。直接在配置类上加上注解@EnableGlobalMethodSecurity(prePostEnabled = true)

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

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

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

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

}

5、配置接口访问权限

最后在需要授权的api接口上配置好需要的权限。

@RestController
@RequestMapping("/hello")
public class HelloController {
    
    

    @GetMapping
    @PreAuthorize("hasAuthority('sys:user:insert')")
    public String hello(){
    
    
        return "hello";
    }
}

三、自定义异常处理器

1、渲染响应工具类

/**
 * 渲染响应的工具类
 */
public class WebUtils {
    
    
    /**
     * 将字符串渲染到客户端
     * 
     * @param response 渲染对象
     * @param string 待渲染的字符串
     * @return null
     */
    public static String renderString(HttpServletResponse response, String string) {
    
    
        try
        {
    
    
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
    
    
            e.printStackTrace();
        }
        return null;
    }
}

2、认证异常处理器

/**
 * 认证异常处理器
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    
    
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    
    
        R result = R.failed("认证失败");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
    }
}

3、授权异常处理器

/**
 * 自定义授权异常
 */
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    
    
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    
    

        R result = R.failed("授权失败");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);
    }
}

4、配置异常处理器

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;
    
    @Autowired
    private AuthenticationEntryPointImpl authenticationEntryPoint;

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

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
//        添加jwt认证过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//        添加异常处理器
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);
    }

}

四、授权实现效果

1、认证成功与失败

认证成功:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0whhqSIS-1657896478663)(C:\Users\cx\AppData\Roaming\Typora\typora-user-images\image-20220715222447129.png)]

认证失败:

在这里插入图片描述

2、授权成功与失败

授权成功:

能够访问接口,接受数据。

在这里插入图片描述

授权失败:

在这里插入图片描述

五、跨域配置

1、springBoot配置

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

2、SpringSecurity配置

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

猜你喜欢

转载自blog.csdn.net/chenxingxingxing/article/details/125813649