一、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 类型。SimpleGrantedAuthority 是GrantedAuthority 的实现类,接受字符串信息,所以直接将字符串权限转换成SimpleGrantedAuthority 。
@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<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
authorities = new ArrayList<>();
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();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@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 {
String token = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
String id;
try {
Claims claims = JwtUtil.parseJWT(token);
id = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
String redisKey = "login:" + id;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
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().disable()
.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 {
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().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").anonymous()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
}
}
四、授权实现效果
1、认证成功与失败
认证成功:
认证失败:
2、授权成功与失败
授权成功:
能够访问接口,接受数据。
授权失败:
五、跨域配置
1、springBoot配置
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.allowedHeaders("*")
.maxAge(3600);
}
}
2、SpringSecurity配置
http.cors();
|