Spring Security authorization

1. RBAC permission model

1. Model introduction

RBAC (Role Based Access Control) Chinese full name is role-based access control. In the RBAC model, permissions are associated with roles, and different roles have different permissions. Users can obtain permissions of different roles by being assigned to different roles, thereby simplifying user permission management. After the user is associated with the role, they can also perform independent authorization and privilege franchise, and the authorization information must be controlled through the role to achieve access control.

2. Create a database table based on the model

Create a simple database based on the RBAC model, the integrity constraints are not too strong, and the functions are simply implemented.

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

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

2. Implementation of authorization

1. Query authorization information according to id

Query authorization information from the database based on user information. Define permission entity class, mapper interface, mapper mapping file.

Entity class:

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

    private static final long serialVersionUID = -54979041104113736L;

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

mapper interface:

@Mapper
public interface MenuMapper extends BaseMapper<Menu> {
    
    

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

}

mapper mapping file: Since it is only a simple test, multi-table query is used, regardless of performance.

<?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. Package authorization information

The method will be overridden in UserDetailsthe implementation class getAuthorities(), and the return value object is the set of permissions owned by the user. First, you need to query the corresponding permissions under the corresponding role of the user in the database, and pass it into the UserDetailsServicelogin user entity class LoginUser. Since security needs GrantedAuthoritytype permission information, it is necessary to LoginUserreplace all incoming permission information with GrantedAuthoritytype. SimpleGrantedAuthorityIt is GrantedAuthorityan implementation class that accepts string information, so it directly converts string permissions into 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. Store permission information in SecurityContextHolder

After the user information is encapsulated into a type in UserDetailsthe implementation class method , the security needs to encapsulate the user information and its permission information into an object and pass it in .LoginUserSimpleGrantedAuthorityauthenticationTokenSecurityContextHolder

@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. Open permission configuration

After the permission information is encapsulated and passed in SecurityContextHolder, permission scanning needs to be enabled on the configuration class. Annotate directly on the configuration class@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. Configure interface access rights

Finally, configure the required permissions on the API interface that requires authorization.

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

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

3. Custom exception handler

1. Render response tool class

/**
 * 渲染响应的工具类
 */
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. Authentication exception handler

/**
 * 认证异常处理器
 */
@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. Authorization exception handler

/**
 * 自定义授权异常
 */
@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. Configure exception handler

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

}

4. The effect of authorization

1. Authentication success and failure

Authentication succeeded:

[External link image transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the image and upload it directly (img-0whhqSIS-1657896478663) (C:\Users\cx\AppData\Roaming\Typora\typora-user-images\ image-20220715222447129.png)]

Authentication failed:

insert image description here

2. Authorization success and failure

Authorization succeeded:

Can access the interface and accept data.

insert image description here

Authorization failed:

insert image description here

5. Cross-domain configuration

1. springBoot configuration

@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. Spring Security configuration

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

Guess you like

Origin blog.csdn.net/chenxingxingxing/article/details/125813649