• 13、使用Spring Security进行权限控制


    在这里插入图片描述
    Spring Security:认证+授权
    认证:判断是否登录,账户是否存在,密码是否正确
    授权:管理员和普通用户访问不同路径的权限不同,加以配置

    登录检查:

    • 之前采用拦截器实现登录检查,这是简单的权限管理方案,现在将其废弃。
      授权配置:
    • 对当前系统内包含的所有的请求,分配访问权限(普通用户、版主、管理员)。
      认证方式:
    • 绕过Security认证流程,采用原来的认证方案。
      CSRF配置:
    • 防止CSRF攻击的基本原理,以及表单、AJAX相关的配置。

    1、废弃登录拦截器

    普通的拦截器,通过实现HandlerInterceptor接口实现拦截器,通过实现WebMvcConfigurer接口实现一个配置类,在配置类中注入拦截器,最后再通过@Configuration注解注入配置。
    这里废弃了HandlerInterceptor实现的拦截器,
    WebMvcConfig.java

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
    
        @Autowired
        private AlphaInterceptor alphaInterceptor;
    
        @Autowired
        private LoginTicketInterceptor loginTicketInterceptor;
    
        @Autowired
        private MessageInterceptor messageInterceptor;
    
        @Autowired
        private DataInterceptor dataInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry){
            registry
                    .addInterceptor(alphaInterceptor)
                    .excludePathPatterns("/*/*.css","/*/*.js","/*/*/png","/*/*.jpg", "/*/*.jpeg")
                    .addPathPatterns("/register","/login");
    
            registry
                    .addInterceptor(loginTicketInterceptor)
                    .excludePathPatterns("/*/*.css","/*/*.js","/*/*/png","/*/*.jpg", "/*/*.jpeg");
            registry
                    .addInterceptor(messageInterceptor)
                    .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg");
    
            registry
                    .addInterceptor(dataInterceptor)
                    .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    2、Security配置类

    1、在常量接口中定义权限,以便于使用

        /**
         * 权限: 普通用户
         */
        String AUTHORITY_USER = "user";
    
        /**
         * 权限: 管理员
         */
        String AUTHORITY_ADMIN = "admin";
    
        /**
         * 权限: 版主
         */
        String AUTHORITY_MODERATOR = "moderator";
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2、配置Security,主要是对授权的配置,(认证使用的是原来的认证方案,没有对登录认证进行相关配置);此外,Security底层默认会拦截/logout请求,进行退出处理,为了执行我们自己的退出代码,要覆盖默认的路径。

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter implements CommunityConstant {
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/resources/**");
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 授权
            http.authorizeRequests()
                    .antMatchers(
                            "/user/setting",
                            "/user/upload",
                            "/user/forgetPassword",
                            "/discuss/add",
                            "/discuss/my",
                            "/comment/add/**",
                            "/letter/**",
                            "/notice/**",
                            "/like",
                            "/follow",
                            "/unfollow")
                    .hasAnyAuthority(AUTHORITY_USER, AUTHORITY_ADMIN, AUTHORITY_MODERATOR)
                    .antMatchers("/discuss/top", "/discuss/wonderful")
                    .hasAnyAuthority(AUTHORITY_MODERATOR)
                    .antMatchers("/discuss/delete", "/data/**", "/data/uv", "/data/dau", "/actuator/**")
                    .hasAnyAuthority(AUTHORITY_ADMIN)
                    .anyRequest()
                    .permitAll()
                    .and()
                    .csrf()
                    .disable();
            // 权限不够时的处理
            // 1.没有登录,这里有bug,登录后不刷新点赞,关注会显示没有登录
            // 2.权限不足
            http.exceptionHandling()
                    .authenticationEntryPoint(
                            new AuthenticationEntryPoint() {
                                // 没有登录
                                @Override
                                public void commence(
                                        HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException e)
                                        throws IOException, ServletException {
                                    String xRequestedWith = request.getHeader("x-requested-with");
                                    if ("XMLHttpRequest".equals(xRequestedWith)) {
                                        response.setContentType("application/plain;charset=utf-8");
                                        PrintWriter writer = response.getWriter();
                                        writer.write(CommunityUtil.getJSONString(403, "你还没有登录哦!"));
                                    } else {
                                        response.sendRedirect(request.getContextPath() + "/login");
                                    }
                                }
                            })
                    .accessDeniedHandler(
                            new AccessDeniedHandler() {
                                // 权限不足
                                @Override
                                public void handle(
                                        HttpServletRequest request,
                                        HttpServletResponse response,
                                        AccessDeniedException e)
                                        throws IOException, ServletException {
                                    String xRequestedWith = request.getHeader("x-requested-with");
                                    if ("XMLHttpRequest".equals(xRequestedWith)) {
                                        response.setContentType("application/plain;charset=utf-8");
                                        PrintWriter writer = response.getWriter();
                                        writer.write(CommunityUtil.getJSONString(403, "你没有访问此功能的权限!"));
                                    } else {
                                        response.sendRedirect(request.getContextPath() + "/denied");
                                    }
                                }
                            });
    
            // Security底层默认会拦截/logout请求,进行退出处理.
            // 覆盖它默认的逻辑,才能执行我们自己的退出代码.
            http.logout().logoutUrl("/securitylogout");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81

    3、认证方案

    在Security框架中,会把认证消息封装到token里,token会被一个filter获取到,并存入security context里。之后授权的时候,都是从security context中获取token,根据token判断权限。

    1、查询某用户的权限
    UserService.java 查询用户的type

        public Collection<? extends GrantedAuthority> getAuthorities(int userId) {
            User user = this.findUserById(userId);
    
            List<GrantedAuthority> list = new ArrayList<>();
            list.add(new GrantedAuthority() {
    
                @Override
                public String getAuthority() {
                    switch (user.getType()) {
                        case 1:
                            return AUTHORITY_ADMIN;
                        case 2:
                            return AUTHORITY_MODERATOR;
                        default:
                            return AUTHORITY_USER;
                    }
                }
            });
            return list;
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    2、LoginTicket拦截器在请求一开始就会判断凭证,可以在此时对用户进行认证,并构建用户认证的结果,存入SecurityContext,以便于Security进行授权。
    LoginTicketInterceptor.java

        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 从cookie中获取凭证
            String ticket = CookieUtil.getValue(request, "ticket");
    
            if (ticket != null) {
                // 查询凭证
                LoginTicket loginTicket = userService.findLoginTicket(ticket);
                // 检查凭证是否有效
                if (loginTicket != null && loginTicket.getStatus() == 0 && loginTicket.getExpired().after(new Date())) {
                    // 根据凭证查询用户
                    User user = userService.findUserById(loginTicket.getUserId());
                    // 在本次请求中持有用户
                    hostHolder.setUser(user);
                    // 构建用户认证的结果,并存入SecurityContext,以便于Security进行授权.
                    Authentication authentication = new UsernamePasswordAuthenticationToken(
                            user, user.getPassword(), userService.getAuthorities(user.getId()));
                    SecurityContextHolder.setContext(new SecurityContextImpl(authentication)); //SecurityContext是通过SecurityContextHolder处理
                }
            }
    
            return true;
        }
    
    	// 请求结束时,把保存的权限清理一下
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            hostHolder.clear();
            SecurityContextHolder.clearContext();
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    3、退出登录时也清理一下认证
    LoginController.java

        @RequestMapping(path = "/logout", method = RequestMethod.GET)
        public String logout(@CookieValue("ticket") String ticket) {
            userService.logout(ticket);
            SecurityContextHolder.clearContext();
            return "redirect:/login";
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    4、CSRF

    1、CSRF攻击原理
    某网站盗取了你(浏览器)的cookie凭证,模拟你的身份访问服务器,通常利用表单提交数据。
    2、防止CSRF攻击原理
    Security底层
    在这里插入图片描述
    Security会在每个表单中生成隐藏的token,防止CSRF攻击
    在这里插入图片描述
    3、异步请求时的处理
    发帖异步请求,没有表单

    A) 在 index.html 生成CSRF令牌

    	
    	<meta name="_csrf" th:content="${_csrf.token}">
    	<meta name="_csrf_header" th:content="${_csrf.headerName}">
    
    • 1
    • 2
    • 3

    异步请求由于没有表单,所以通过请求消息头将数据传给服务器。

    B) 在 index.js 里通过请求消息头将数据传给服务器

    $(function(){
    	$("#publishBtn").click(publish);
    });
    
    function publish() {
    	$("#publishModal").modal("hide");
    
        // 发送AJAX请求之前,将CSRF令牌设置到请求的消息头中.
        // 这样在提交数据时就会带有相应的token
        var token = $("meta[name='_csrf']").attr("content");
        var header = $("meta[name='_csrf_header']").attr("content");
        $(document).ajaxSend(function(e, xhr, options){
            xhr.setRequestHeader(header, token);
        });
    
    	// 获取标题和内容
    	var title = $("#recipient-name").val();
    	var content = $("#message-text").val();
    	// 发送异步请求(POST)
    	$.post(
    	    CONTEXT_PATH + "/discuss/add",
    	    {"title":title,"content":content},
    	    function(data) {
    	        data = $.parseJSON(data);
    	        // 在提示框中显示返回消息
    	        $("#hintBody").text(data.msg);
    	        // 显示提示框
                $("#hintModal").modal("show");
                // 2秒后,自动隐藏提示框
                setTimeout(function(){
                    $("#hintModal").modal("hide");
                    // 刷新页面
                    if(data.code == 0) {
                        window.location.reload();
                    }
                }, 2000);
    	    }
    	);
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    由于启用了防止CSRF攻击后,对每个异步请求都要进行这样的处理,否则服务器得不到该token会认为是被攻击,会禁止访问。

    附:获取json字符串工具

        public static String getJSONString(int code, String msg, Map<String, Object> map) {
            JSONObject json = new JSONObject();
            json.put("code", code);
            json.put("msg", msg);
            if (map != null) {
                for (String key : map.keySet()) {
                    json.put(key, map.get(key));
                }
            }
            return json.toJSONString();
        }
    
        public static String getJSONString(int code, String msg) {
            return getJSONString(code, msg, null);
        }
    
        public static String getJSONString(int code) {
            return getJSONString(code, null, null);
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
  • 相关阅读:
    css 自定义虚线
    Python Excel xlsx,xls,csv 格式互转
    Vue开发实战二十二:列表中表格动态合并的实现
    网页与chrome插件实现交互,网页通过插件调取接口获取数据
    智安网络|提升企业网络安全:避免成为勒索软件攻击的目标
    React+后端实现导出Excle表格的功能
    MySQL权限
    图标、图片、矢量图注册,通过名称来配置icon可以提高性能
    拾光者,云南白药!
    使用js实现安居客二级菜单及(注意事项和问题点演示)完整版
  • 原文地址:https://blog.csdn.net/nice___amusin/article/details/126072021